Skip to content

Commit 52268e8

Browse files
Fix bug in handling of blocks that occur in expresssions (#672)
### Changes - Fix bug in handling of blocks that occur in expressions. The heart of the problem was that `transformStmt`, which does not update the substitution map, was being called from inside `transformExpr` when it was handling a block, leading to missing substitutions. A second but smaller bug was a statement ordering bug that occurred when handling calls. ### Testing - Added test-case 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: Josh Cohen <36058610+joscoh@users.noreply.github.com>
1 parent f9a01de commit 52268e8

3 files changed

Lines changed: 81 additions & 43 deletions

File tree

Strata/Languages/Laurel/LiftImperativeExpressions.lean

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,42 @@ private def freshCondVar : LiftM Identifier := do
104104
modify fun s => { s with condCounter := n + 1 }
105105
return s!"$c_{n}"
106106

107-
private def addPrepend (stmt : StmtExprMd) : LiftM Unit :=
107+
private def prepend (stmt : StmtExprMd) : LiftM Unit :=
108108
modify fun s => { s with prependedStmts := stmt :: s.prependedStmts }
109109

110+
private def onlyKeepSideEffectStmtsAndLast (stmts : List StmtExprMd) : LiftM (List StmtExprMd) := do
111+
match stmts with
112+
| [] => return []
113+
| _ =>
114+
let last := stmts.getLast!
115+
let nonLast ← stmts.dropLast.flatMapM (fun s =>
116+
match s.val with
117+
| .LocalVariable .. => do
118+
-- This addPrepend is a hack to work around Core not having let expressions
119+
-- Otherwise we could keep them in the block
120+
prepend s
121+
pure []
122+
| .Assert _ => do
123+
-- Hack to work around Core not supporting assert expressions
124+
-- Otherwise we could keep them in the block
125+
prepend s
126+
pure []
127+
| .Assume _ => do
128+
-- Hack to work around Core not supporting assume expressions
129+
-- Otherwise we could keep them in the block
130+
prepend s
131+
pure []
132+
133+
/-
134+
Any other impure StmtExpr, like .Assign, .Exit or .Return,
135+
should already have been processed by translateExpr,
136+
so we can assume this StmtExpr is pure and can be dropped.
137+
TODO: currently .Exit and .Return are not processed by translateExpr, this is a bug
138+
-/
139+
| _ => pure []
140+
)
141+
return nonLast ++ [last]
142+
110143
private def takePrepends : LiftM (List StmtExprMd) := do
111144
let stmts := (← get).prependedStmts
112145
modify fun s => { s with prependedStmts := [] }
@@ -171,15 +204,15 @@ and updates substitutions. The value should already be transformed by the caller
171204
private def liftAssignExpr (targets : List StmtExprMd) (seqValue : StmtExprMd)
172205
(md : Imperative.MetaData Core.Expression) : LiftM Unit := do
173206
-- Prepend the assignment itself
174-
addPrepend (⟨.Assign targets seqValue, md⟩)
207+
prepend (⟨.Assign targets seqValue, md⟩)
175208
-- Create a before-snapshot for each target and update substitutions
176209
for target in targets do
177210
match target.val with
178211
| .Identifier varName =>
179212
let snapshotName ← freshTempFor varName
180213
let varType ← computeType target
181214
-- Snapshot goes before the assignment (cons pushes to front)
182-
addPrepend (⟨.LocalVariable snapshotName varType (some (⟨.Identifier varName, md⟩)), md⟩)
215+
prepend (⟨.LocalVariable snapshotName varType (some (⟨.Identifier varName, md⟩)), md⟩)
183216
setSubst varName snapshotName
184217
| _ => pure ()
185218

@@ -200,7 +233,7 @@ def transformExpr (expr : StmtExprMd) : LiftM StmtExprMd := do
200233
| .Hole false (some holeType) =>
201234
-- Nondeterministic typed hole: lift to a fresh variable with no initializer (havoc)
202235
let holeVar ← freshCondVar
203-
addPrepend (bare (.LocalVariable holeVar holeType none))
236+
prepend (bare (.LocalVariable holeVar holeType none))
204237
return bare (.Identifier holeVar)
205238

206239
| .Assign targets value =>
@@ -235,12 +268,13 @@ def transformExpr (expr : StmtExprMd) : LiftM StmtExprMd := do
235268
return seqCall
236269
else
237270
-- Imperative call in expression position: lift it like an assignment
238-
-- Order matters: assign must be prepended first (it's newest-first),
239-
-- so that when reversed the var declaration comes before the call.
240271
let callResultVar ← freshCondVar
241272
let callResultType ← computeType expr
242-
addPrepend (⟨.Assign [bare (.Identifier callResultVar)] seqCall, md⟩)
243-
addPrepend (bare (.LocalVariable callResultVar callResultType none))
273+
let liftedCall := [
274+
⟨ (.LocalVariable callResultVar callResultType none), md ⟩,
275+
⟨.Assign [bare (.Identifier callResultVar)] seqCall, md⟩
276+
]
277+
modify fun s => { s with prependedStmts := s.prependedStmts ++ liftedCall}
244278
return bare (.Identifier callResultVar)
245279

246280
| .IfThenElse cond thenBranch elseBranch =>
@@ -277,8 +311,8 @@ def transformExpr (expr : StmtExprMd) : LiftM StmtExprMd := do
277311
let condType ← computeType thenBranch
278312
-- IfThenElse added first (cons puts it deeper), then declaration (cons puts it on top)
279313
-- Output order: declaration, then if-then-else
280-
addPrepend (⟨.IfThenElse seqCond thenBlock seqElse, md⟩)
281-
addPrepend (bare (.LocalVariable condVar condType none))
314+
prepend (⟨.IfThenElse seqCond thenBlock seqElse, md⟩)
315+
prepend (bare (.LocalVariable condVar condType none))
282316
return bare (.Identifier condVar)
283317
else
284318
-- No assignments in branches — recurse normally
@@ -289,21 +323,9 @@ def transformExpr (expr : StmtExprMd) : LiftM StmtExprMd := do
289323
| none => pure none
290324
return ⟨.IfThenElse seqCond seqThen seqElse, md⟩
291325

292-
| .Block stmts metadata =>
293-
-- Block in expression position: lift all but last to prepends
294-
match h_last : stmts.getLast? with
295-
| none => return bare (.Block [] metadata)
296-
| some last => do
297-
have := List.mem_of_getLast? h_last
298-
299-
-- Process all-but-last as statements and prepend them in order
300-
let mut blockStmts : List StmtExprMd := []
301-
for nonLastStatement in stmts.dropLast.attach do
302-
have := List.dropLast_subset stmts nonLastStatement.property
303-
blockStmts := blockStmts ++ (← transformStmt nonLastStatement)
304-
for s in blockStmts.reverse do addPrepend s
305-
-- Last element is the expression value
306-
transformExpr last
326+
| .Block stmts labelOption =>
327+
let newStmts := (← stmts.reverse.mapM transformExpr).reverse
328+
return ⟨ .Block (← onlyKeepSideEffectStmtsAndLast newStmts) labelOption, md ⟩
307329

308330
| .LocalVariable name ty initializer =>
309331
-- If the substitution map has an entry for this variable, it was
@@ -314,9 +336,9 @@ def transformExpr (expr : StmtExprMd) : LiftM StmtExprMd := do
314336
match initializer with
315337
| some initExpr =>
316338
let seqInit ← transformExpr initExpr
317-
addPrepend (⟨.LocalVariable name ty (some seqInit), expr.md⟩)
339+
prepend (⟨.LocalVariable name ty (some seqInit), expr.md⟩)
318340
| none =>
319-
addPrepend (⟨.LocalVariable name ty none, expr.md⟩)
341+
prepend (⟨.LocalVariable name ty none, expr.md⟩)
320342
return ⟨.Identifier (← getSubst name), expr.md⟩
321343
else
322344
return expr

StrataTest/Languages/Laurel/Examples/Fundamentals/T2_ImpureExpressions.lean

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,36 @@ procedure imperativeCallInConditionalExpression(b: bool) {
9797
assert result == 0
9898
}
9999
};
100+
101+
function add(x: int, y: int): int
102+
{
103+
x + y
104+
};
105+
106+
procedure repeatedBlockExpressions() {
107+
var x: int := 2;
108+
var y: int := { x := 1; x } + { x := x + 10; x };
109+
var z: int := add({ x := 1; x }, { x := x + 10; x });
110+
assert y == 1 + 11;
111+
assert z == 1 + 11
112+
};
113+
114+
procedure addProc(a: int, b: int) returns (r: int)
115+
ensures r == a + b {
116+
return a + b
117+
};
118+
119+
procedure addProcCaller(): int {
120+
var x: int := 0;
121+
var y: int := addProc({x := 1; x}, {x := x + 10; x});
122+
assert y == 11
123+
124+
// The next statement is not translated correctly.
125+
// I think it's a bug in the handling of StaticCall
126+
// Where a reference is substituted when it should not be
127+
// var z: int := addProc({x := 1; x}, {x := x + 10; x}) + (x := 3);
128+
// assert z == 14
129+
};
100130
"
101131

102132
#guard_msgs (error, drop all) in

StrataTest/Languages/Laurel/LiftExpressionAssignmentsTest.lean

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,6 @@ open Strata.Elab (parseStrataProgramFromDialect)
2222
namespace Strata.Laurel
2323

2424
def blockStmtLiftingProgram : String := r"
25-
composite Box {
26-
var value: int
27-
}
28-
29-
procedure heapUpdateInBlockExpr(b: Box)
30-
{
31-
var x: int := { b#value := b#value + 1; b#value };
32-
assert x == b#value
33-
};
34-
3525
procedure assertInBlockExpr()
3626
{
3727
var x: int := 0;
@@ -53,14 +43,10 @@ def parseLaurelAndLift (input : String) : IO Program := do
5343
pure (liftExpressionAssignments model program)
5444

5545
/--
56-
info: procedure heapUpdateInBlockExpr(b: Box) returns ⏎
57-
()
58-
deterministic
59-
{ b#value := b#value + 1; var x: int := b#value; assert x == b#value }
60-
procedure assertInBlockExpr() returns ⏎
46+
info: procedure assertInBlockExpr() returns ⏎
6147
()
6248
deterministic
63-
{ var x: int := 0; assert x == 0; x := 1; var y: int := x; assert y == 1 }
49+
{ var x: int := 0; assert x == 0; var $x_0: int := x; x := 1; var y: int := { x }; assert y == 1 }
6450
-/
6551
#guard_msgs in
6652
#eval! do

0 commit comments

Comments
 (0)