-
Notifications
You must be signed in to change notification settings - Fork 52
pyspec: extend predicate bodies with strict comparisons, equality, and integer arithmetic #1206
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main2
Are you sure you want to change the base?
Changes from all commits
9f4d7b2
d3e5b3b
ef0df5a
8f31fda
6b11888
3e7ce90
5e79273
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -789,30 +789,51 @@ def assumeCondition (cond : SpecExpr) (loc : SourceRange) (act : SpecAssertionM | |
| { a with formula := .implies cond a.formula loc } | ||
| modify fun s => { s with assertions := prevAssertions ++ wrapped } | ||
|
|
||
| /-- Build a comparison expression dispatching to float or int variants based on type. -/ | ||
| /-- Build a comparison expression dispatching to float or int variants | ||
| based on type. Emits a `specWarning` when both operands are | ||
| `Any`-typed (e.g. unparameterised callees) and the recognizer has | ||
| to assume `int`: if the actual runtime values are floats the | ||
| generated SMT VC will have the wrong sort. -/ | ||
| private def makeComparison | ||
| (loc : SourceRange) | ||
| (floatCtor intCtor : SpecExpr → SpecExpr → SpecExpr) | ||
| (subj : SpecExpr) (subjType : SpecType) (bound : SpecExpr) (boundType : SpecType) | ||
| : Option SpecExpr := | ||
| : SpecAssertionM (Option SpecExpr) := | ||
| if subjType.isFloatType then | ||
| match bound with | ||
| | .floatLit .. => some (floatCtor subj bound) | ||
| | .intLit v loc => some (floatCtor subj (.floatLit (toString v) (loc := loc))) | ||
| | _ => none | ||
| | .floatLit .. => return some (floatCtor subj bound) | ||
| | .intLit v loc => return some (floatCtor subj (.floatLit (toString v) (loc := loc))) | ||
| | _ => return none | ||
| else if subjType.isIntType then | ||
| match bound with | ||
| | .intLit .. => some (intCtor subj bound) | ||
| | _ => none | ||
| | .intLit .. => return some (intCtor subj bound) | ||
| | _ => | ||
| -- subj is int, bound is int- or Any-typed (a parameter etc.) → | ||
| -- use the int comparison constructor. | ||
| if boundType.isIntType || boundType.isAnyType then | ||
| return some (intCtor subj bound) | ||
| else return none | ||
| else if boundType.isFloatType then | ||
| match bound with | ||
| | .floatLit .. => some (floatCtor subj bound) | ||
| | _ => none | ||
| | .floatLit .. => return some (floatCtor subj bound) | ||
| | _ => return none | ||
| else if boundType.isIntType then | ||
| match bound with | ||
| | .intLit .. => some (intCtor subj bound) | ||
| | _ => none | ||
| | .intLit .. => return some (intCtor subj bound) | ||
| | _ => | ||
| if subjType.isIntType || subjType.isAnyType then | ||
| return some (intCtor subj bound) | ||
| else return none | ||
| else if subjType.isAnyType && boundType.isAnyType then | ||
| -- Both sides Any-typed (e.g. two function parameters whose types | ||
| -- aren't seeded). Assume int — if the runtime values are floats, | ||
| -- the generated SMT VC will have the wrong sort, so warn. | ||
| do | ||
| specWarning loc | ||
| "comparison between Any-typed operands; assuming int — generated VC may be unsound if runtime values are floats" | ||
| return some (intCtor subj bound) | ||
| else | ||
| none | ||
| return none | ||
|
|
||
| private def transCompare (loc : SourceRange) | ||
| (lhsExpr : SpecExpr) (lhsType : SpecType) | ||
|
|
@@ -825,7 +846,7 @@ private def transCompare (loc : SourceRange) | |
| let .isTrue h₂ := decideProp (comparators.size = 1) | ||
| | return none | ||
| match lhsExpr with | ||
| -- len(subject) >= N / len(subject) <= N | ||
| -- len(subject) >= N / len(subject) <= N / len(subject) > N / len(subject) < N | ||
| | .stringLen _ _ => | ||
| let (clean, (boundExpr, _)) ← runNoWarn (transExpr comparators[0]) | ||
| if clean then | ||
|
|
@@ -834,6 +855,8 @@ private def transCompare (loc : SourceRange) | |
| match ops[0] with | ||
| | .GtE _ => return some (.intGe lhsExpr boundExpr (loc := loc)) | ||
| | .LtE _ => return some (.intLe lhsExpr boundExpr (loc := loc)) | ||
| | .Gt _ => return some (.intGt lhsExpr boundExpr (loc := loc)) | ||
| | .Lt _ => return some (.intLt lhsExpr boundExpr (loc := loc)) | ||
| | _ => pure () | ||
| | _ => pure () | ||
| -- compile("pattern").search(subject) is not None | ||
|
|
@@ -852,16 +875,24 @@ private def transCompare (loc : SourceRange) | |
| | _ => pure () | ||
| | _ => pure () | ||
|
|
||
| -- subject >= N / subject <= N (type-checked: int or float) | ||
| -- subject {>=, <=, >, <, ==, !=} N (type-checked: int or float) | ||
| let (clean, (boundExpr, boundType)) ← runNoWarn (transExpr comparators[0]) | ||
| if not clean then | ||
| return none | ||
|
|
||
| match ops[0] with | ||
| | .GtE _ => | ||
| return makeComparison (.floatGe · · (loc := loc)) (.intGe · · (loc := loc)) lhsExpr lhsType boundExpr boundType | ||
| makeComparison loc (.floatGe · · (loc := loc)) (.intGe · · (loc := loc)) lhsExpr lhsType boundExpr boundType | ||
| | .LtE _ => | ||
| return makeComparison (.floatLe · · (loc := loc)) (.intLe · · (loc := loc)) lhsExpr lhsType boundExpr boundType | ||
| makeComparison loc (.floatLe · · (loc := loc)) (.intLe · · (loc := loc)) lhsExpr lhsType boundExpr boundType | ||
| | .Gt _ => | ||
| makeComparison loc (.floatGt · · (loc := loc)) (.intGt · · (loc := loc)) lhsExpr lhsType boundExpr boundType | ||
| | .Lt _ => | ||
| makeComparison loc (.floatLt · · (loc := loc)) (.intLt · · (loc := loc)) lhsExpr lhsType boundExpr boundType | ||
| | .Eq _ => | ||
| makeComparison loc (.floatEq · · (loc := loc)) (.intEq · · (loc := loc)) lhsExpr lhsType boundExpr boundType | ||
| | .NotEq _ => | ||
| makeComparison loc (.floatNe · · (loc := loc)) (.intNe · · (loc := loc)) lhsExpr lhsType boundExpr boundType | ||
| | _ => | ||
| return none | ||
|
|
||
|
|
@@ -916,6 +947,54 @@ partial def transExpr (e : expr SourceRange) | |
| return (.intLit (Int.negOfNat n) (loc := loc), intType) | ||
| | .UnaryOp _ (.USub _) (.Constant _ (.ConFloat _ ⟨_, s⟩) _) => | ||
| return (.floatLit s!"-{s}" (loc := loc), floatType) | ||
| -- Integer arithmetic in predicate bodies. Both sides must translate | ||
| -- cleanly to int-typed (or Any-typed, e.g. function parameters whose | ||
| -- types weren't seeded into `localTypes`) SpecExprs; the result is | ||
| -- int-typed. Anything other than int / Any (float, str, …) falls | ||
| -- through to placeholder so transExpr doesn't claim a type it can't | ||
| -- justify. | ||
| | .BinOp _ left op right => | ||
| let (lhs, lhsTp) ← transExpr left | ||
| let (rhs, rhsTp) ← transExpr right | ||
| let okType (tp : SpecType) : Bool := tp.isIntType || tp.isAnyType | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Asymmetric coverage: this PR adds int |
||
| -- Python `//` and `%` round toward `-∞` and follow the divisor's | ||
| -- sign respectively; Laurel `Div`/`Mod` are Euclidean. The two | ||
| -- agree only when the divisor is positive, which we can confirm | ||
| -- statically just for positive integer literals. | ||
| let divisorPositive : Bool := match rhs with | ||
| | .intLit v _ => v > 0 | ||
| | _ => false | ||
| if okType lhsTp && okType rhsTp then | ||
| match op with | ||
| | .Add _ => return (.intAdd lhs rhs (loc := loc), intType) | ||
| | .Sub _ => return (.intSub lhs rhs (loc := loc), intType) | ||
| | .Mult _ => return (.intMul lhs rhs (loc := loc), intType) | ||
| | .FloorDiv _ => | ||
| unless divisorPositive do | ||
| specWarning loc | ||
| "Python `//` (floor) and Laurel `Div` (Euclidean) only agree when the divisor is a positive literal; generated VC may diverge from runtime semantics for negative or symbolic divisors" | ||
| return (.intDiv lhs rhs (loc := loc), intType) | ||
| | .Mod _ => | ||
| unless divisorPositive do | ||
| specWarning loc | ||
| "Python `%` (sign-of-divisor) and Laurel `Mod` (Euclidean) only agree when the divisor is a positive literal; generated VC may diverge from runtime semantics for negative or symbolic divisors" | ||
| return (.intMod lhs rhs (loc := loc), intType) | ||
| | _ => | ||
| specWarning loc s!"unsupported BinOp in predicate: {repr op}" | ||
| return placeholder | ||
| else if lhsTp.isFloatType || rhsTp.isFloatType then | ||
| -- Float arithmetic in predicate bodies is deliberately deferred: | ||
| -- Laurel `Real` arithmetic models exact mathematical reals, but | ||
| -- Python floats are IEEE-754 doubles, so a faithful encoding | ||
| -- needs a `Float64` lowering path that doesn't yet exist in | ||
| -- `ToLaurel.lean`. Fall through with an explicit warning rather | ||
| -- than silently producing a `Real`-typed VC. | ||
| specWarning loc | ||
| s!"float arithmetic in predicates is not yet supported (op={repr op}); use the int variant or restructure the assertion" | ||
| return placeholder | ||
| else | ||
| specWarning loc s!"BinOp with non-int operand: lhs={repr lhsTp}, rhs={repr rhsTp}" | ||
| return placeholder | ||
| -- String literal (extract value for use in messages) | ||
| | .Constant _ (.ConString _ ⟨_, _s⟩) _ => | ||
| return (.placeholder (loc := loc), SpecType.ident loc .builtinsStr) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The doc-comment is wrong: Python
//is floor division (rounds toward −∞), but LaurelPrimitiveOp.Divresolves toInt.ediv(Euclidean — non-negative remainder). They coincide only when the divisor is positive.Example:
-7 // -2is3in Python (floor of3.5) but4in Euclidean (since-7 = -2·4 + 1). Same divergence forintModat line 110: Python%matches the sign of the divisor, Euclideanmodis always non-negative.This is pre-existing behavior inherited from
Strata/Languages/Python/PythonToCore.lean:180, but predicate bodies will silently produce wrong VCs whenever a divisor can be negative. Two options:specWarninginSpecs.lean:952when the divisor sign isn't a positive literal.Minimum: don't claim the semantics is the same in the doc.