Skip to content

Commit b23659a

Browse files
Add per-node source ranges to SpecExpr (#810)
Add a `loc : SourceRange` field (required, no default) to every SpecExpr constructor so that each node in the expression tree carries its own source location from the Python AST. Changes: - Decls.lean: Add `loc : SourceRange` to all 17 constructors - DDM.lean: toDDM uses `loc` as the DDM annotation; fromDDM extracts it - Specs.lean: Construction sites pass `e.ann` (Python expr source range) where available. Structural wrappers pass the enclosing statement's source range: `pred.ann` for implies/not (from the if-condition), `s.ann` for forallList/forallDict (from the for-statement). `assumeCondition` now takes a `loc` parameter. - ToLaurel.lean: specExprToLaurel uses per-node `loc` to create metadata via a `nodeMd` helper, falling back to the function-level `md` when `loc` is default. This resolves the md-reuse concern from PR #804. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. --------- Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
1 parent f0007fd commit b23659a

4 files changed

Lines changed: 144 additions & 117 deletions

File tree

Strata/Languages/Python/Specs.lean

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -770,13 +770,13 @@ def extractKwargsField (e : expr SourceRange)
770770
partial def extractSubject (e : expr SourceRange)
771771
: SpecAssertionM (Option SpecExpr) := do
772772
match ← extractKwargsField e with
773-
| some (kn, fn) => return some (.getIndex (.var kn) fn)
773+
| some (kn, fn) => return some (.getIndex (.var kn (loc := e.ann)) fn (loc := e.ann))
774774
| none => pure ()
775775
match e with
776-
| .Name _ ⟨_, name⟩ (.Load _) => return some (.var name)
776+
| .Name _ ⟨_, name⟩ (.Load _) => return some (.var name (loc := e.ann))
777777
| .Subscript _ inner (.Constant _ (.ConString _ fieldName) _) (.Load _) =>
778778
match ← extractSubject inner with
779-
| some subj => return some (.getIndex subj fieldName.val)
779+
| some subj => return some (.getIndex subj fieldName.val (loc := e.ann))
780780
| none => return none
781781
| _ => return none
782782

@@ -790,7 +790,7 @@ def transCondition (e : expr SourceRange) : SpecAssertionM (Option SpecExpr) :=
790790
match ops.val[0] with
791791
| .In _ =>
792792
match ← extractSubject comparators.val[0] with
793-
| some subj => return some (.containsKey subj key.val)
793+
| some subj => return some (.containsKey subj key.val (loc := e.ann))
794794
| none => pure ()
795795
| _ => pure ()
796796
pure ()
@@ -800,7 +800,7 @@ def transCondition (e : expr SourceRange) : SpecAssertionM (Option SpecExpr) :=
800800
/-- Run an action that may produce assertions, then wrap each new assertion's
801801
formula with `implies cond ...` (or `implies (not cond) ...` for else branches).
802802
If `cond` is `none`, assertions pass through unchanged. -/
803-
def assumeCondition (cond : Option SpecExpr) (act : SpecAssertionM Unit)
803+
def assumeCondition (cond : Option SpecExpr) (loc : SourceRange) (act : SpecAssertionM Unit)
804804
: SpecAssertionM Unit := do
805805
let prevAssertions := (←get).assertions
806806
modify fun s => { s with assertions := #[] }
@@ -809,7 +809,7 @@ def assumeCondition (cond : Option SpecExpr) (act : SpecAssertionM Unit)
809809
match cond with
810810
| some c =>
811811
let wrapped := newAssertions.map fun a =>
812-
{ a with formula := .implies c a.formula }
812+
{ a with formula := .implies c a.formula loc }
813813
modify fun s => { s with assertions := prevAssertions ++ wrapped }
814814
| none =>
815815
modify fun s => { s with assertions := prevAssertions ++ newAssertions }
@@ -826,10 +826,10 @@ def transMessageExpr (e : expr SourceRange)
826826
| .Call _ (.Name _ funcName (.Load _)) args _ =>
827827
if funcName.val == "len" && args.val.size == 1 then
828828
match ← extractSubject args.val[0]! with
829-
| some subj => return .len subj
830-
| none => return .placeholder
831-
else return .placeholder
832-
| _ => return .placeholder
829+
| some subj => return .len subj (loc := e.ann)
830+
| none => return .placeholder (loc := e.ann)
831+
else return .placeholder (loc := e.ann)
832+
| _ => return .placeholder (loc := e.ann)
833833

834834
/-- Look up a field in a TypedDict SpecType, returning its type if found. -/
835835
def lookupTypedDictField (tp : SpecType) (field : String) : Option SpecType := do
@@ -951,24 +951,26 @@ private def makeComparison
951951
(isFloat isInt : Bool)
952952
(subj : SpecExpr) (bound : expr SourceRange)
953953
: SpecAssertionM (Option SpecExpr) := do
954+
let loc := bound.ann
954955
if isFloat then
955956
match extractFloatBound bound with
956-
| some s => return some (floatCtor subj (.floatLit s))
957+
| some s => return some (floatCtor subj (.floatLit s (loc := loc)))
957958
| none =>
958959
match extractIntBound bound with
959-
| some n => return some (floatCtor subj (.floatLit (toString n)))
960+
| some n => return some (floatCtor subj (.floatLit (toString n) (loc := loc)))
960961
| none => return none
961962
else if isInt then
962963
match extractIntBound bound with
963-
| some n => return some (intCtor subj (.intLit n))
964+
| some n => return some (intCtor subj (.intLit n (loc := loc)))
964965
| none => return none
965966
else
966967
match extractIntBound bound with
967-
| some n => return some (intCtor subj (.intLit n))
968+
| some n => return some (intCtor subj (.intLit n (loc := loc)))
968969
| none => return none
969970

970971
def transAssertExpr (e : expr SourceRange)
971972
: SpecAssertionM SpecExpr := do
973+
let loc := e.ann
972974
-- isinstance(subject, T)
973975
match e with
974976
| .Call _ (.Name _ funcName (.Load _)) args _ =>
@@ -978,10 +980,10 @@ def transAssertExpr (e : expr SourceRange)
978980
| some subj =>
979981
match args.val[1] with
980982
| .Name _ typeName (.Load _) =>
981-
return .isInstanceOf subj typeName.val
983+
return .isInstanceOf subj typeName.val (loc := loc)
982984
| _ =>
983985
specWarning e.ann s!"isinstance: unsupported type argument"
984-
return .placeholder
986+
return .placeholder (loc := loc)
985987
| none => pure () -- fall through
986988
if funcName.val == "len" && args.val.size == 1 then
987989
-- This is just len(x), not a comparison; fall through
@@ -998,8 +1000,8 @@ def transAssertExpr (e : expr SourceRange)
9981000
match ← extractSubject callArgs.val[0] with
9991001
| some subj =>
10001002
match ops.val[0], extractIntBound comparators.val[0] with
1001-
| .GtE _, some n => return .intGe (.len subj) (.intLit n)
1002-
| .LtE _, some n => return .intLe (.len subj) (.intLit n)
1003+
| .GtE _, some n => return .intGe (.len subj (loc := loc)) (.intLit n (loc := comparators.val[0].ann)) (loc := loc)
1004+
| .LtE _, some n => return .intLe (.len subj (loc := loc)) (.intLit n (loc := comparators.val[0].ann)) (loc := loc)
10031005
| _, _ => pure ()
10041006
| none => pure ()
10051007
| _ => pure ()
@@ -1014,8 +1016,8 @@ def transAssertExpr (e : expr SourceRange)
10141016
let isFloat := subjType.any isFloatType
10151017
let isInt := subjType.any isIntType
10161018
let cmp ← match ops.val[0] with
1017-
| .GtE _ => makeComparison .floatGe .intGe isFloat isInt subj comparators.val[0]
1018-
| .LtE _ => makeComparison .floatLe .intLe isFloat isInt subj comparators.val[0]
1019+
| .GtE _ => makeComparison (.floatGe · · (loc := loc)) (.intGe · · (loc := loc)) isFloat isInt subj comparators.val[0]
1020+
| .LtE _ => makeComparison (.floatLe · · (loc := loc)) (.intLe · · (loc := loc)) isFloat isInt subj comparators.val[0]
10191021
| _ => pure none
10201022
match cmp with
10211023
| some expr => return expr
@@ -1025,7 +1027,7 @@ def transAssertExpr (e : expr SourceRange)
10251027
-- subject == "A" or subject == "B" or ...
10261028
match ← collectEnumValues e with
10271029
| some (subj, vals) =>
1028-
return .enumMember subj vals
1030+
return .enumMember subj vals (loc := loc)
10291031
| none => pure ()
10301032
-- compile("pattern").search(subject) is not None
10311033
match e with
@@ -1044,14 +1046,14 @@ def transAssertExpr (e : expr SourceRange)
10441046
| some subj =>
10451047
match ops.val[0], comparators.val[0] with
10461048
| .IsNot _, .Constant _ (.ConNone _) _ =>
1047-
return .regexMatch subj pattern.val
1049+
return .regexMatch subj pattern.val (loc := loc)
10481050
| _, _ => pure ()
10491051
| none => pure ()
10501052
| _ => pure ()
10511053
| _ => pure ()
10521054
-- Fallback: unrecognized pattern
10531055
specWarning e.ann s!"unrecognized assert pattern: {eformat e.toAst}"
1054-
return .placeholder
1056+
return .placeholder (loc := loc)
10551057

10561058
mutual
10571059

@@ -1113,7 +1115,7 @@ def blockStmt (s : stmt SourceRange) : SpecAssertionM Unit := do
11131115
blockStmts body.val
11141116
let bodyAssertions := (←get).assertions
11151117
let wrapped := bodyAssertions.map fun a =>
1116-
{ a with formula := .forallList listExpr varName a.formula }
1118+
{ a with formula := .forallList listExpr varName a.formula s.ann }
11171119
modify fun s => { s with assertions := prevAssertions ++ wrapped }
11181120
| none =>
11191121
specWarning s.ann s!"For: cannot extract iterable expression"
@@ -1154,7 +1156,7 @@ def blockStmt (s : stmt SourceRange) : SpecAssertionM Unit := do
11541156
blockStmts body.val
11551157
let bodyAssertions := (←get).assertions
11561158
let wrapped := bodyAssertions.map fun a =>
1157-
{ a with formula := .forallDict dictSubj keyVar valVar a.formula }
1159+
{ a with formula := .forallDict dictSubj keyVar valVar a.formula s.ann }
11581160
modify fun st => { st with assertions := prevAssertions ++ wrapped }
11591161
| none =>
11601162
specWarning s.ann s!"For: cannot extract dict expression"
@@ -1166,9 +1168,9 @@ def blockStmt (s : stmt SourceRange) : SpecAssertionM Unit := do
11661168
let cond ← transCondition pred
11671169
if cond.isNone then
11681170
specWarning pred.ann s!"if: unrecognized condition pattern: {eformat pred.toAst}"
1169-
assumeCondition cond <| blockStmts t.val
1171+
assumeCondition cond pred.ann <| blockStmts t.val
11701172
if f.val.size > 0 then
1171-
assumeCondition (cond.map .not) <| blockStmts f.val
1173+
assumeCondition (cond.map (.not · pred.ann)) pred.ann <| blockStmts f.val
11721174
| .Pass _ =>
11731175
pure ()
11741176
| _ => specError s.ann s!"Unsupported statement: {eformat s.toAst}"

Strata/Languages/Python/Specs/DDM.lean

Lines changed: 45 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -243,31 +243,31 @@ private def Arg.toDDM (d : Arg) : DDM.ArgDecl SourceRange :=
243243

244244
protected def SpecExpr.toDDM (e : SpecExpr) : DDM.SpecExprDecl SourceRange :=
245245
match e with
246-
| .placeholder => .placeholderExpr .none
247-
| .var name => .varExpr .none ⟨.none, name⟩
248-
| .getIndex subj field => .getIndexExpr .none subj.toDDM ⟨.none, field⟩
249-
| .isInstanceOf subj tn => .isInstanceOfExpr .none subj.toDDM ⟨.none, tn⟩
250-
| .len subj => .lenExpr .none subj.toDDM
251-
| .intLit v => .intExpr .none (toDDMInt .none v)
252-
| .intGe subj bound => .intGeExpr .none subj.toDDM bound.toDDM
253-
| .intLe subj bound => .intLeExpr .none subj.toDDM bound.toDDM
254-
| .floatLit v => .floatExpr .none ⟨.none, v⟩
255-
| .floatGe subj bound => .floatGeExpr .none subj.toDDM bound.toDDM
256-
| .floatLe subj bound => .floatLeExpr .none subj.toDDM bound.toDDM
257-
| .enumMember subj values =>
258-
.enumMemberExpr .none subj.toDDM
259-
.none, values.map (⟨.none, ·⟩)⟩
260-
| .regexMatch subj pattern =>
261-
.regexMatchExpr .none subj.toDDM ⟨.none, pattern⟩
262-
| .containsKey container key =>
263-
.containsKeyExpr .none container.toDDM ⟨.none, key⟩
264-
| .implies cond body =>
265-
.impliesExpr .none cond.toDDM body.toDDM
266-
| .not e => .notExpr .none e.toDDM
267-
| .forallList list varName body =>
268-
.forallListExpr .none list.toDDM ⟨.none, varName⟩ body.toDDM
269-
| .forallDict dict keyVar valVar body =>
270-
.forallDictExpr .none dict.toDDM ⟨.none, keyVar⟩ ⟨.none, valVar⟩ body.toDDM
246+
| .placeholder loc => .placeholderExpr loc
247+
| .var name loc => .varExpr loc ⟨loc, name⟩
248+
| .getIndex subj field loc => .getIndexExpr loc subj.toDDM ⟨loc, field⟩
249+
| .isInstanceOf subj tn loc => .isInstanceOfExpr loc subj.toDDM ⟨loc, tn⟩
250+
| .len subj loc => .lenExpr loc subj.toDDM
251+
| .intLit v loc => .intExpr loc (toDDMInt loc v)
252+
| .intGe subj bound loc => .intGeExpr loc subj.toDDM bound.toDDM
253+
| .intLe subj bound loc => .intLeExpr loc subj.toDDM bound.toDDM
254+
| .floatLit v loc => .floatExpr loc ⟨loc, v⟩
255+
| .floatGe subj bound loc => .floatGeExpr loc subj.toDDM bound.toDDM
256+
| .floatLe subj bound loc => .floatLeExpr loc subj.toDDM bound.toDDM
257+
| .enumMember subj values loc =>
258+
.enumMemberExpr loc subj.toDDM
259+
loc, values.map (⟨loc, ·⟩)⟩
260+
| .regexMatch subj pattern loc =>
261+
.regexMatchExpr loc subj.toDDM ⟨loc, pattern⟩
262+
| .containsKey container key loc =>
263+
.containsKeyExpr loc container.toDDM ⟨loc, key⟩
264+
| .implies cond body loc =>
265+
.impliesExpr loc cond.toDDM body.toDDM
266+
| .not e loc => .notExpr loc e.toDDM
267+
| .forallList list varName body loc =>
268+
.forallListExpr loc list.toDDM ⟨loc, varName⟩ body.toDDM
269+
| .forallDict dict keyVar valVar body loc =>
270+
.forallDictExpr loc dict.toDDM ⟨loc, keyVar⟩ ⟨loc, valVar⟩ body.toDDM
271271

272272
def specExprFormatContext : FormatContext :=
273273
.ofDialects DDM.PythonSpecs_map
@@ -383,26 +383,26 @@ private def DDM.ArgDecl.fromDDM (d : DDM.ArgDecl SourceRange) : Specs.Arg :=
383383

384384
private def DDM.SpecExprDecl.fromDDM (d : DDM.SpecExprDecl SourceRange) : Specs.SpecExpr :=
385385
match d with
386-
| .placeholderExpr _ => .placeholder
387-
| .varExpr _ ⟨_, name⟩ => .var name
388-
| .getIndexExpr _ subj ⟨_, field⟩ => .getIndex subj.fromDDM field
389-
| .isInstanceOfExpr _ subj ⟨_, tn⟩ => .isInstanceOf subj.fromDDM tn
390-
| .lenExpr _ subj => .len subj.fromDDM
391-
| .intExpr _ i => .intLit i.ofDDM
392-
| .intGeExpr _ subj bound => .intGe subj.fromDDM bound.fromDDM
393-
| .intLeExpr _ subj bound => .intLe subj.fromDDM bound.fromDDM
394-
| .floatExpr _ ⟨_, v⟩ => .floatLit v
395-
| .floatGeExpr _ subj bound => .floatGe subj.fromDDM bound.fromDDM
396-
| .floatLeExpr _ subj bound => .floatLe subj.fromDDM bound.fromDDM
397-
| .enumMemberExpr _ subj ⟨_, values⟩ => .enumMember subj.fromDDM (values.map (·.2))
398-
| .regexMatchExpr _ subj ⟨_, pattern⟩ => .regexMatch subj.fromDDM pattern
399-
| .containsKeyExpr _ container ⟨_, key⟩ => .containsKey container.fromDDM key
400-
| .impliesExpr _ cond body => .implies cond.fromDDM body.fromDDM
401-
| .notExpr _ e => .not e.fromDDM
402-
| .forallListExpr _ list ⟨_, varName⟩ body =>
403-
.forallList list.fromDDM varName body.fromDDM
404-
| .forallDictExpr _ dict ⟨_, keyVar⟩ ⟨_, valVar⟩ body =>
405-
.forallDict dict.fromDDM keyVar valVar body.fromDDM
386+
| .placeholderExpr loc => .placeholder loc
387+
| .varExpr loc ⟨_, name⟩ => .var name loc
388+
| .getIndexExpr loc subj ⟨_, field⟩ => .getIndex subj.fromDDM field loc
389+
| .isInstanceOfExpr loc subj ⟨_, tn⟩ => .isInstanceOf subj.fromDDM tn loc
390+
| .lenExpr loc subj => .len subj.fromDDM loc
391+
| .intExpr loc i => .intLit i.ofDDM loc
392+
| .intGeExpr loc subj bound => .intGe subj.fromDDM bound.fromDDM loc
393+
| .intLeExpr loc subj bound => .intLe subj.fromDDM bound.fromDDM loc
394+
| .floatExpr loc ⟨_, v⟩ => .floatLit v loc
395+
| .floatGeExpr loc subj bound => .floatGe subj.fromDDM bound.fromDDM loc
396+
| .floatLeExpr loc subj bound => .floatLe subj.fromDDM bound.fromDDM loc
397+
| .enumMemberExpr loc subj ⟨_, values⟩ => .enumMember subj.fromDDM (values.map (·.2)) loc
398+
| .regexMatchExpr loc subj ⟨_, pattern⟩ => .regexMatch subj.fromDDM pattern loc
399+
| .containsKeyExpr loc container ⟨_, key⟩ => .containsKey container.fromDDM key loc
400+
| .impliesExpr loc cond body => .implies cond.fromDDM body.fromDDM loc
401+
| .notExpr loc e => .not e.fromDDM loc
402+
| .forallListExpr loc list ⟨_, varName⟩ body =>
403+
.forallList list.fromDDM varName body.fromDDM loc
404+
| .forallDictExpr loc dict ⟨_, keyVar⟩ ⟨_, valVar⟩ body =>
405+
.forallDict dict.fromDDM keyVar valVar body.fromDDM loc
406406

407407
private def DDM.MessagePart.fromDDM (d : DDM.MessagePart SourceRange) : Specs.MessagePart :=
408408
match d with

Strata/Languages/Python/Specs/Decls.lean

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -316,39 +316,39 @@ and `placeholder`; interior nodes represent operations like `len`, `getIndex`,
316316
inductive SpecExpr where
317317
/-- Stands in for an assert pattern not yet supported by the translator.
318318
The original Python expression is preserved in `Assertion.message`. -/
319-
| placeholder
320-
| var (name : String)
321-
| getIndex (subject : SpecExpr) (field : String)
322-
| isInstanceOf (subject : SpecExpr) (typeName : String)
323-
| len (subject : SpecExpr)
324-
| intLit (value : Int)
325-
| intGe (subject : SpecExpr) (bound : SpecExpr)
326-
| intLe (subject : SpecExpr) (bound : SpecExpr)
319+
| placeholder (loc : SourceRange)
320+
| var (name : String) (loc : SourceRange)
321+
| getIndex (subject : SpecExpr) (field : String) (loc : SourceRange)
322+
| isInstanceOf (subject : SpecExpr) (typeName : String) (loc : SourceRange)
323+
| len (subject : SpecExpr) (loc : SourceRange)
324+
| intLit (value : Int) (loc : SourceRange)
325+
| intGe (subject : SpecExpr) (bound : SpecExpr) (loc : SourceRange)
326+
| intLe (subject : SpecExpr) (bound : SpecExpr) (loc : SourceRange)
327327
/-- A floating-point literal, stored as a string to preserve precision. -/
328-
| floatLit (value : String)
329-
| floatGe (subject : SpecExpr) (bound : SpecExpr)
330-
| floatLe (subject : SpecExpr) (bound : SpecExpr)
331-
| enumMember (subject : SpecExpr) (values : Array String)
328+
| floatLit (value : String) (loc : SourceRange)
329+
| floatGe (subject : SpecExpr) (bound : SpecExpr) (loc : SourceRange)
330+
| floatLe (subject : SpecExpr) (bound : SpecExpr) (loc : SourceRange)
331+
| enumMember (subject : SpecExpr) (values : Array String) (loc : SourceRange)
332332
/-- `regexMatch subject pattern` asserts that `subject` matches the regular
333333
expression `pattern`. Corresponds to `compile(pattern).search(subject) is not None`
334334
in the Python source. -/
335-
| regexMatch (subject : SpecExpr) (pattern : String)
335+
| regexMatch (subject : SpecExpr) (pattern : String) (loc : SourceRange)
336336
/-- `containsKey container key` asserts that `key` is present in `container`.
337337
Corresponds to `"key" in container` in the Python source. -/
338-
| containsKey (container : SpecExpr) (key : String)
338+
| containsKey (container : SpecExpr) (key : String) (loc : SourceRange)
339339
/-- `implies condition body` asserts that if `condition` holds then `body` holds.
340340
Used to represent conditional assertions like `if "field" in kwargs: assert ...`. -/
341-
| implies (condition : SpecExpr) (body : SpecExpr)
341+
| implies (condition : SpecExpr) (body : SpecExpr) (loc : SourceRange)
342342
/-- Logical negation. Used for else-branch conditions. -/
343-
| not (e : SpecExpr)
343+
| not (e : SpecExpr) (loc : SourceRange)
344344
/-- `forallList list varName body` asserts that `body` holds for every element
345345
of `list`, with `varName` bound to each element in turn. Only `body` may
346346
refer to `varName`. Corresponds to `for varName in list: assert body`. -/
347-
| forallList (list : SpecExpr) (varName : String) (body : SpecExpr)
347+
| forallList (list : SpecExpr) (varName : String) (body : SpecExpr) (loc : SourceRange)
348348
/-- `forallDict dict keyVar valVar body` asserts that `body` holds for every
349349
key-value pair in `dict`. Both `keyVar` and `valVar` are bound in `body`.
350350
Corresponds to `for keyVar, valVar in dict.items(): assert body`. -/
351-
| forallDict (dict : SpecExpr) (keyVar : String) (valVar : String) (body : SpecExpr)
351+
| forallDict (dict : SpecExpr) (keyVar : String) (valVar : String) (body : SpecExpr) (loc : SourceRange)
352352
deriving Inhabited
353353

354354
inductive MessagePart where

0 commit comments

Comments
 (0)