Skip to content

Commit 235fc1f

Browse files
committed
feat(laurel): add Java-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 175269c commit 235fc1f

16 files changed

Lines changed: 674 additions & 6 deletions

Strata/Languages/Laurel/EliminateIncrDecr.lean

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,25 +46,40 @@ 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+
For a `.Field` target this duplicates the object subtree `tgt` into the read
52+
operand (the lowering emits `target op rhs`, so `target` appears on both sides).
53+
That is sound because a field read lowers (in `HeapParameterization`) to a pure,
54+
total, obligation-free `readField(heap, obj, field)` map lookup, and both copies
55+
read the same `$heap` snapshot at the same program point — so they are provably
56+
equal regardless of how complex `tgt` is (e.g. a chained `a#b#c`). The only cost
57+
is VC/term-size duplication, not a correctness risk. (If field reads ever gain a
58+
precondition — e.g. a null/allocated check — this duplication would need revisiting.) -/
5059
private def targetAsRead (target : VariableMd) : StmtExprMd :=
5160
let source := target.source
5261
match target.val with
5362
| .Local name => ⟨.Var (.Local name), source⟩
5463
| .Field tgt fieldName => ⟨.Var (.Field tgt fieldName), source⟩
5564
| .Declare param => ⟨.Var (.Local param.name), source⟩
5665

66+
/-- Build `.Assign [target] (target ⊕ rhs)` where `⊕` is `primOp`. The resulting
67+
assignment expression yields the new value. Shared by the `IncrDecr` lowering
68+
(with `rhs = 1`) and the `CompoundAssign` lowering (with the user's RHS). -/
69+
private def lowerOpAssign (primOp : Operation) (target : VariableMd)
70+
(rhs : StmtExprMd) (source : Option FileRange) : StmtExprMd :=
71+
let read := targetAsRead target
72+
let updated : StmtExprMd := ⟨.PrimitiveOp primOp [read, rhs], source⟩
73+
⟨.Assign [target] updated, source⟩
74+
5775
/-- Build `.Assign [target] (target ⊕ 1)` where `⊕` is `Add` for `Incr` and
5876
`Sub` for `Decr`. The resulting assignment expression yields the new value. -/
5977
private def lowerToAssign (op : IncrDecrOp) (target : VariableMd)
6078
(source : Option FileRange) : StmtExprMd :=
6179
let primOp : Operation := match op with
6280
| .Incr => .Add
6381
| .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⟩
82+
lowerOpAssign primOp target ⟨.LiteralInt 1, source⟩ source
6883

6984
/-- Lower a single `.IncrDecr` node to the expression form that yields the
7085
correct value for the given `mode` (Pre or Post). -/
@@ -86,6 +101,12 @@ private def lowerIncrDecr (mode : IncrDecrMode) (op : IncrDecrOp)
86101
private def rewriteNode (node : StmtExprMd) : StmtExprMd :=
87102
match node.val with
88103
| .IncrDecr mode op target => lowerIncrDecr mode op target node.source
104+
| .CompoundAssign op target rhs =>
105+
-- `x op= e ⇝ (x := x op e)`, yielding the new value. No Pre/Post dance:
106+
-- compound assignment is always new-valued. The `.PrimitiveOp` keeps the
107+
-- default `skipProof := false`, so `/=`/`%=` carry the same division-by-zero
108+
-- proof obligations as a hand-written `x := x / e`.
109+
lowerOpAssign op target rhs node.source
89110
| _ => node
90111

91112
/-- 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
@@ -109,6 +109,12 @@ private partial def collectExprNames (expr : StmtExprMd) : CollectM Unit := do
109109
| .Field tgt _ => collectExprNames tgt
110110
| .Local _ => pure ()
111111
| .Declare param => collectHighTypeNames param.type
112+
| .CompoundAssign _ target rhs =>
113+
(match target.val with
114+
| .Field tgt _ => collectExprNames tgt
115+
| .Local _ => pure ()
116+
| .Declare param => collectHighTypeNames param.type)
117+
collectExprNames rhs
112118
| .Var (.Field target _) => collectExprNames target
113119
| .Var (.Declare param) => collectHighTypeNames param.type
114120
| .PureFieldUpdate target _ newVal =>

Strata/Languages/Laurel/Grammar/AbstractToConcreteTreeTranslator.lean

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,22 @@ where
143143
| .Local name => laurelOp "identifier" #[ident name.text]
144144
| .Declare param => laurelOp "identifier" #[ident param.name.text]
145145
laurelOp opName #[targetArg]
146+
| .CompoundAssign op target rhs =>
147+
-- `op` is invariably one of Add/Sub/Mul/Div/Mod/StrConcat (enforced by the
148+
-- concrete-to-abstract translator); any other Operation is a bug upstream.
149+
let opName := match op with
150+
| .Add => "addAssign" | .Sub => "subAssign" | .Mul => "mulAssign"
151+
| .Div => "divAssign" | .Mod => "modAssign" | .StrConcat => "strConcatAssign"
152+
-- Unreachable by the ctor invariant. Emit a sentinel that will NOT re-parse
153+
-- (rather than silently masquerading as `+=`), so a future miswiring is caught
154+
-- by the round-trip rather than corrupting output.
155+
| _ => "invalidCompoundAssign"
156+
let targetArg := match target.val with
157+
| .Field obj fieldName =>
158+
laurelOp "fieldAccess" #[stmtExprToArg obj, ident fieldName.text]
159+
| .Local name => laurelOp "identifier" #[ident name.text]
160+
| .Declare param => laurelOp "identifier" #[ident param.name.text]
161+
laurelOp opName #[targetArg, stmtExprToArg rhs]
146162
| .StaticCall callee args =>
147163
let calleeArg := laurelOp "identifier" #[ident callee.text]
148164
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: fieldAccess now @[prec(95), leftassoc] (paren-free `c#n++` and chained `a#b#c`).
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
@@ -53,6 +53,16 @@ op parenthesis (inner: StmtExpr): StmtExpr => "(" inner ")";
5353
// Assignment
5454
op assign (target: StmtExpr, value: StmtExpr): StmtExpr => @[prec(10)] target " := " value;
5555

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

Strata/Languages/Laurel/LaurelAST.lean

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,14 @@ inductive StmtExpr : Type where
319319
yielded value is discarded.
320320
Eliminated by the `EliminateIncrDecr` pass before lifting imperative expressions. -/
321321
| IncrDecr (mode : IncrDecrMode) (op : IncrDecrOp) (target : AstNode Variable)
322+
/-- Java-style compound assignment (`x += e`, `x -= e`, `x *= e`, `x /= e`,
323+
`x %= e`, `x ^= e`). Lowers to `target := target op rhs` and yields the new
324+
value. The target must be a `Local` or `Field` `Variable`.
325+
Invariant: `op` is one of `Add`/`Sub`/`Mul`/`Div`/`Mod`/`StrConcat` — the only
326+
operators the concrete-to-abstract translator ever constructs here. Downstream
327+
sites may treat any other `Operation` as a `StrataBug`.
328+
Eliminated by the `EliminateIncrDecr` pass before lifting imperative expressions. -/
329+
| CompoundAssign (op : Operation) (target : AstNode Variable) (rhs : AstNode StmtExpr)
322330
/-- Update a field on a pure (value) type, producing a new value. -/
323331
| PureFieldUpdate (target : AstNode StmtExpr) (fieldName : Identifier) (newValue : AstNode StmtExpr)
324332
/-- Call a static procedure by name with the given arguments. -/
@@ -528,6 +536,7 @@ def StmtExpr.constructorName (e : StmtExpr) : String :=
528536
| .All => "All"
529537
| .Hole .. => "Hole"
530538
| .IncrDecr .. => "IncrDecr"
539+
| .CompoundAssign .. => "CompoundAssign"
531540

532541
/-- Check whether a single modifies entry is the wildcard (`*`). -/
533542
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
@@ -257,6 +257,9 @@ def translateExpr (expr : StmtExprMd)
257257
| .IncrDecr _ _ _ =>
258258
throwExprDiagnostic $ diagnosticFromSource expr.source
259259
"IncrDecr should have been eliminated by EliminateIncrDecr pass" DiagnosticType.StrataBug
260+
| .CompoundAssign _ _ _ =>
261+
throwExprDiagnostic $ diagnosticFromSource expr.source
262+
"CompoundAssign should have been eliminated by EliminateIncrDecr pass" DiagnosticType.StrataBug
260263
| .While _ _ _ _ =>
261264
disallowed expr.source "loops are not supported in functions or contracts"
262265
| .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
@@ -88,6 +88,12 @@ def computeExprType (model : SemanticModel) (expr : StmtExprMd) : HighTypeMd :=
8888
| .Local id => (model.get id).getType
8989
| .Field _ fieldName => (model.get fieldName).getType
9090
| .Declare _ => ⟨ .TVoid, source ⟩ -- shouldn't happen; rejected by translator
91+
| .CompoundAssign _ target _ =>
92+
-- Yields the new value, whose type is the target variable's type.
93+
match target.val with
94+
| .Local id => (model.get id).getType
95+
| .Field _ fieldName => (model.get fieldName).getType
96+
| .Declare _ => ⟨ .TVoid, source ⟩ -- shouldn't happen; rejected by translator
9197
| .Assert _ => ⟨ .TVoid, source ⟩
9298
| .Assume _ => ⟨ .TVoid, source ⟩
9399
-- 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)