Skip to content

Commit 9b93f9f

Browse files
committed
feat(laurel): add C-style compound assignment (+= -= *= /= %= ^=)
Compound assignment is the general case of which `++`/`--` is the degenerate `x op= 1` form. `x op= e` lowers to `x := x op e` and yields the new value, usable as a statement (`x += 2;`) or in expression position (`int y := (x += 2)`). * AST: new `StmtExpr.CompoundAssign (op : Operation) (target) (rhs)` ctor. The `op` is invariably one of Add/Sub/Mul/Div/Mod/StrConcat (enforced by the concrete-to-abstract translator); downstream sites treat any other as a bug. * Grammar: `+= -= *= /= %= ^=` at prec(10), rightassoc. Mixed `:=`/`op=` chaining is unsupported paren-free, matching `:=`'s existing behavior (plain `x := y := z` also does not parse); `a += b += c` groups right. * Lowering: `EliminateIncrDecr.lowerToAssign` is generalized to `lowerOpAssign` (arbitrary `Operation` + RHS), shared with the `++`/`--` path (rhs = 1). The `.PrimitiveOp` keeps `skipProof := false`, so `/=`/`%=` carry the same division-by-zero proof obligation as a hand-written `x := x / e`. Runs first in the pipeline, so no later pass observes `.CompoundAssign`. * Type checking is construct-keyed, by design: `++`/`--` reject `real` (the synthesized int `1` cannot type-check against a real under strict numeric typing), but `+= -= *= /=` accept `real` because the RHS is user-written; `%=` is int-only (`.Mod` has no real lowering); `^=` is string-only. The shared `compoundAssignAccepts` predicate drives both the resolution-time target check and the model-based RHS-type check (`compoundAssignRhsErrors`, run in the post-resolution validation phase). The RHS check is suppressed when the target type is already invalid, so one mistake yields one error. * All exhaustive `StmtExpr` matches updated (MapStmtExpr traverses the rhs even for Local/Declare targets, FilterPrelude, computeExprType, both grammar translators, the Core-translator defensive arm, the lift `contains*` helpers, resolution `collectStmtExpr` and the diamond-field walker). Tests: * T24_CompoundAssign: each operator on int; real for `+= -= *= /=`; string for `^=`; constrained-int (`nat`); expression position; nested side-effecting RHS; right-associative `a += b += c`. * T24b_CompoundAssignField: field target `c#n += e` and chained `o#inner#count += e`, both statement and expression position. * CompoundAssignTypeRejectionTest: per-operator target rejections; RHS-type mismatches; the intended `r += 1.0` accepts / `r++` rejects asymmetry; and a `/= 0` verification failure pinning `skipProof := false`. * CompoundAssignLiftTest: golden lowering for statement, expression, and nested-RHS forms.
1 parent 0abd9ef commit 9b93f9f

15 files changed

Lines changed: 755 additions & 6 deletions

Strata/Languages/Laurel/EliminateIncrDecr.lean

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,25 +46,38 @@ namespace Strata.Laurel
4646

4747
public section
4848

49-
/-- Reconstruct the read-side `StmtExprMd` for a `Variable`. -/
49+
/-- Reconstruct the read-side `StmtExprMd` for a `Variable`.
50+
51+
A `.Field` target duplicates the object subtree into the read operand (the lowering
52+
emits `target op rhs`). This is sound — and term-size cost only, not a correctness
53+
risk — because a field read lowers to the pure, obligation-free `readField` lookup,
54+
so both copies read the same `$heap` at the same point. (Revisit if field reads ever
55+
gain a precondition.) -/
5056
private def targetAsRead (target : VariableMd) : StmtExprMd :=
5157
let source := target.source
5258
match target.val with
5359
| .Local name => ⟨.Var (.Local name), source⟩
5460
| .Field tgt fieldName => ⟨.Var (.Field tgt fieldName), source⟩
5561
| .Declare param => ⟨.Var (.Local param.name), source⟩
5662

63+
/-- Build `.Assign [target] (target ⊕ rhs)` where `⊕` is `primOp`, yielding the new
64+
value. Shared by the `IncrDecr` lowering (`rhs = 1`) and `CompoundAssign` (user's
65+
RHS). The `.PrimitiveOp` keeps the default `skipProof := false`, so `/=`/`%=` carry
66+
the same division-by-zero obligation as a hand-written `x := x / e`. -/
67+
private def lowerOpAssign (primOp : Operation) (target : VariableMd)
68+
(rhs : StmtExprMd) (source : Option FileRange) : StmtExprMd :=
69+
let read := targetAsRead target
70+
let updated : StmtExprMd := ⟨.PrimitiveOp primOp [read, rhs], source⟩
71+
⟨.Assign [target] updated, source⟩
72+
5773
/-- Build `.Assign [target] (target ⊕ 1)` where `⊕` is `Add` for `Incr` and
5874
`Sub` for `Decr`. The resulting assignment expression yields the new value. -/
5975
private def lowerToAssign (op : IncrDecrOp) (target : VariableMd)
6076
(source : Option FileRange) : StmtExprMd :=
6177
let primOp : Operation := match op with
6278
| .Incr => .Add
6379
| .Decr => .Sub
64-
let one : StmtExprMd := ⟨.LiteralInt 1, source⟩
65-
let read := targetAsRead target
66-
let updated : StmtExprMd := ⟨.PrimitiveOp primOp [read, one], source⟩
67-
⟨.Assign [target] updated, source⟩
80+
lowerOpAssign primOp target ⟨.LiteralInt 1, source⟩ source
6881

6982
/-- Lower a single `.IncrDecr` node to the expression form that yields the
7083
correct value for the given `mode` (Pre or Post). -/
@@ -86,6 +99,9 @@ private def lowerIncrDecr (mode : IncrDecrMode) (op : IncrDecrOp)
8699
private def rewriteNode (node : StmtExprMd) : StmtExprMd :=
87100
match node.val with
88101
| .IncrDecr mode op target => lowerIncrDecr mode op target node.source
102+
| .CompoundAssign op target rhs =>
103+
-- `x op= e ⇝ x := x op e` — always new-valued, so no Pre/Post wrapper.
104+
lowerOpAssign op target rhs node.source
89105
| _ => node
90106

91107
/-- Apply the rewrite to a procedure (body, preconditions, decreases, invokeOn). -/

Strata/Languages/Laurel/FilterPrelude.lean

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ private partial def collectExprNames (expr : StmtExprMd) : CollectM Unit := do
108108
| .Field tgt _ => collectExprNames tgt
109109
| .Local _ => pure ()
110110
| .Declare param => collectHighTypeNames param.type
111+
| .CompoundAssign _ target rhs =>
112+
(match target.val with
113+
| .Field tgt _ => collectExprNames tgt
114+
| .Local _ => pure ()
115+
| .Declare param => collectHighTypeNames param.type)
116+
collectExprNames rhs
111117
| .Var (.Field target _) => collectExprNames target
112118
| .Var (.Declare param) => collectHighTypeNames param.type
113119
| .PureFieldUpdate target _ newVal =>

Strata/Languages/Laurel/Grammar/AbstractToConcreteTreeTranslator.lean

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,20 @@ where
141141
| .Local name => laurelOp "identifier" #[ident name.text]
142142
| .Declare param => laurelOp "identifier" #[ident param.name.text]
143143
laurelOp opName #[targetArg]
144+
| .CompoundAssign op target rhs =>
145+
-- `op` is invariably Add/Sub/Mul/Div/Mod/StrConcat (the C2A translator only
146+
-- builds those); the fallback emits a non-reparsing sentinel so a future
147+
-- miswiring fails the round-trip instead of silently masquerading as `+=`.
148+
let opName := match op with
149+
| .Add => "addAssign" | .Sub => "subAssign" | .Mul => "mulAssign"
150+
| .Div => "divAssign" | .Mod => "modAssign" | .StrConcat => "strConcatAssign"
151+
| _ => "invalidCompoundAssign"
152+
let targetArg := match target.val with
153+
| .Field obj fieldName =>
154+
laurelOp "fieldAccess" #[stmtExprToArg obj, ident fieldName.text]
155+
| .Local name => laurelOp "identifier" #[ident name.text]
156+
| .Declare param => laurelOp "identifier" #[ident param.name.text]
157+
laurelOp opName #[targetArg, stmtExprToArg rhs]
144158
| .StaticCall callee args =>
145159
let calleeArg := laurelOp "identifier" #[ident callee.text]
146160
let argsArr := args.map stmtExprToArg |>.toArray

Strata/Languages/Laurel/Grammar/ConcreteToAbstractTreeTranslator.lean

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,24 @@ partial def translateStmtExpr (arg : Arg) : TransM StmtExprMd := do
268268
| q`Laurel.postDecr, #[arg0] =>
269269
let target ← translateIncrDecrTarget arg0 "postDecr"
270270
return mkStmtExprMd (.IncrDecr .Post .Decr target) src
271+
| q`Laurel.addAssign, #[arg0, arg1] =>
272+
let target ← translateIncrDecrTarget arg0 "+="
273+
return mkStmtExprMd (.CompoundAssign .Add target (← translateStmtExpr arg1)) src
274+
| q`Laurel.subAssign, #[arg0, arg1] =>
275+
let target ← translateIncrDecrTarget arg0 "-="
276+
return mkStmtExprMd (.CompoundAssign .Sub target (← translateStmtExpr arg1)) src
277+
| q`Laurel.mulAssign, #[arg0, arg1] =>
278+
let target ← translateIncrDecrTarget arg0 "*="
279+
return mkStmtExprMd (.CompoundAssign .Mul target (← translateStmtExpr arg1)) src
280+
| q`Laurel.divAssign, #[arg0, arg1] =>
281+
let target ← translateIncrDecrTarget arg0 "/="
282+
return mkStmtExprMd (.CompoundAssign .Div target (← translateStmtExpr arg1)) src
283+
| q`Laurel.modAssign, #[arg0, arg1] =>
284+
let target ← translateIncrDecrTarget arg0 "%="
285+
return mkStmtExprMd (.CompoundAssign .Mod target (← translateStmtExpr arg1)) src
286+
| q`Laurel.strConcatAssign, #[arg0, arg1] =>
287+
let target ← translateIncrDecrTarget arg0 "^="
288+
return mkStmtExprMd (.CompoundAssign .StrConcat target (← translateStmtExpr arg1)) src
271289
| q`Laurel.multiAssign, #[targetsSeq, valueArg] =>
272290
let targets ← match targetsSeq with
273291
| .seq _ .comma args => args.toList.mapM fun targ => do

Strata/Languages/Laurel/Grammar/LaurelGrammar.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module
1212
-- Laurel dialect definition, loaded from LaurelGrammar.st
1313
-- NOTE: Changes to LaurelGrammar.st are not automatically tracked by the build system.
1414
-- Update this file (e.g. this comment) to trigger a recompile after modifying LaurelGrammar.st.
15-
-- Last grammar change: renamed strConcat token to `^`; added preIncr/preDecr/postIncr/postDecr; `return` value is now Option StmtExpr (supports a valueless return).
15+
-- Last grammar change: added compound assignment ops (`+=`, `-=`, `*=`, `/=`, `%=`, `^=`).
1616
public import StrataDDM.AST
1717
import StrataDDM.BuiltinDialects.Init
1818
import StrataDDM.Integration.Lean.HashCommands

Strata/Languages/Laurel/Grammar/LaurelGrammar.st

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,16 @@ op parenthesis (inner: StmtExpr): StmtExpr => "(" inner ")";
5050
// Assignment
5151
op assign (target: StmtExpr, value: StmtExpr): StmtExpr => @[prec(10)] target " := " value;
5252

53+
// Compound assignment (`x op= e`). Same precedence as `assign`; rightassoc so
54+
// `a += b += c` groups as `a += (b += c)`. Lvalue-ness of `target` is enforced in
55+
// the concrete-to-abstract translator, not the grammar.
56+
op addAssign (target: StmtExpr, value: StmtExpr): StmtExpr => @[prec(10), rightassoc] target " += " value;
57+
op subAssign (target: StmtExpr, value: StmtExpr): StmtExpr => @[prec(10), rightassoc] target " -= " value;
58+
op mulAssign (target: StmtExpr, value: StmtExpr): StmtExpr => @[prec(10), rightassoc] target " *= " value;
59+
op divAssign (target: StmtExpr, value: StmtExpr): StmtExpr => @[prec(10), rightassoc] target " /= " value;
60+
op modAssign (target: StmtExpr, value: StmtExpr): StmtExpr => @[prec(10), rightassoc] target " %= " value;
61+
op strConcatAssign (target: StmtExpr, value: StmtExpr): StmtExpr => @[prec(10), rightassoc] target " ^= " value;
62+
5363
// Multi-target assignment: assign var x: int, y, var z: int := call()
5464
// Uses the 'assign' keyword to avoid ambiguity with other comma-separated constructs.
5565
category AssignTarget;

Strata/Languages/Laurel/LaurelAST.lean

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,15 @@ inductive StmtExpr : Type where
328328
yielded value is discarded.
329329
Eliminated by the `EliminateIncrDecr` pass before lifting imperative expressions. -/
330330
| IncrDecr (mode : IncrDecrMode) (op : IncrDecrOp) (target : AstNode Variable)
331+
/-- C-style compound assignment (`x += e`, `x -= e`, `x *= e`, `x /= e`, `x %= e`),
332+
plus `x ^= e` for string concatenation (Laurel uses `^` for concat, OCaml-style,
333+
not bitwise XOR). Lowers to `target := target op rhs` and yields the new value.
334+
The target must be a `Local` or `Field` `Variable`.
335+
Invariant: `op` is one of `Add`/`Sub`/`Mul`/`Div`/`Mod`/`StrConcat` — the only
336+
operators the concrete-to-abstract translator ever constructs here. Downstream
337+
sites may treat any other `Operation` as a `StrataBug`.
338+
Eliminated by the `EliminateIncrDecr` pass before lifting imperative expressions. -/
339+
| CompoundAssign (op : Operation) (target : AstNode Variable) (rhs : AstNode StmtExpr)
331340
/-- Update a field on a pure (value) type, producing a new value. -/
332341
| PureFieldUpdate (target : AstNode StmtExpr) (fieldName : Identifier) (newValue : AstNode StmtExpr)
333342
/-- Call a static procedure by name with the given arguments. -/
@@ -402,6 +411,7 @@ def StmtExpr.constrName : StmtExpr → String
402411
| .Assign .. => ":="
403412
| .IncrDecr _ .Incr .. => "++"
404413
| .IncrDecr _ .Decr .. => "--"
414+
| .CompoundAssign .. => "compound assignment"
405415
| .PureFieldUpdate .. => "field update"
406416
| .StaticCall .. => "call"
407417
| .PrimitiveOp op .. => toString op
@@ -741,6 +751,7 @@ def StmtExpr.constructorName (e : StmtExpr) : String :=
741751
| .All => "All"
742752
| .Hole .. => "Hole"
743753
| .IncrDecr .. => "IncrDecr"
754+
| .CompoundAssign .. => "CompoundAssign"
744755

745756
/-- Check whether a single modifies entry is the wildcard (`*`). -/
746757
def StmtExprMd.isWildcard (m : StmtExprMd) : Bool := match m.val with | .All => true | _ => false

Strata/Languages/Laurel/LaurelToCoreTranslator.lean

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,9 @@ def translateExpr (expr : StmtExprMd)
261261
| .IncrDecr _ _ _ =>
262262
throwExprDiagnostic $ diagnosticFromSource expr.source
263263
"IncrDecr should have been eliminated by EliminateIncrDecr pass" DiagnosticType.StrataBug
264+
| .CompoundAssign _ _ _ =>
265+
throwExprDiagnostic $ diagnosticFromSource expr.source
266+
"CompoundAssign should have been eliminated by EliminateIncrDecr pass" DiagnosticType.StrataBug
264267
| .While _ _ _ _ =>
265268
disallowed expr.source "loops are not supported in functions or contracts"
266269
| .Exit _ => disallowed expr.source "exit is not supported in expression position"

Strata/Languages/Laurel/LaurelTypes.lean

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ def computeExprType (model : SemanticModel) (expr : StmtExprMd) : HighTypeMd :=
8989
| .Local id => (model.get id).getType
9090
| .Field _ fieldName => (model.get fieldName).getType
9191
| .Declare _ => ⟨ .TVoid, source ⟩ -- shouldn't happen; rejected by translator
92+
| .CompoundAssign _ target _ =>
93+
-- Yields the new value, whose type is the target variable's type.
94+
match target.val with
95+
| .Local id => (model.get id).getType
96+
| .Field _ fieldName => (model.get fieldName).getType
97+
| .Declare _ => ⟨ .TVoid, source ⟩ -- shouldn't happen; rejected by translator
9298
| .Assert _ => ⟨ .TVoid, source ⟩
9399
| .Assume _ => ⟨ .TVoid, source ⟩
94100
-- Instance related

Strata/Languages/Laurel/LiftImperativeExpressions.lean

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ def containsAssignment (expr : StmtExprMd) : Bool :=
156156
match val with
157157
| .Assign .. => true
158158
| .IncrDecr .. => true
159+
| .CompoundAssign .. => true
159160
| .StaticCall _ args => args.attach.any (fun x => containsAssignment x.val)
160161
| .PrimitiveOp _ args _ => args.attach.any (fun x => containsAssignment x.val)
161162
| .Block stmts _ => stmts.attach.any (fun x => containsAssignment x.val)
@@ -175,6 +176,7 @@ def containsBareAssignment (expr : StmtExprMd) : Bool :=
175176
match val with
176177
| .Assign .. => true
177178
| .IncrDecr .. => true
179+
| .CompoundAssign .. => true
178180
| .StaticCall _ args => args.attach.any (fun x => containsBareAssignment x.val)
179181
| .PrimitiveOp _ args _ => args.attach.any (fun x => containsBareAssignment x.val)
180182
| .Block _ _ => false

0 commit comments

Comments
 (0)