Skip to content

Conversation

@keyboardDrummer
Copy link
Contributor

@keyboardDrummer keyboardDrummer commented Jan 30, 2026

Changes

  1. Fix bug in assignment lifting pass that showed when doing subsequent assignments to the same variable.
  2. Stop using globals in heap parameterization, and do heap parameterization before assignment lifting, so the assignment lifting pass can work on the heap assignments

Testing

  • For (1), add test-case to T2_ImpureExpressions
  • For (2), add test-case to T1_MutableFields

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@keyboardDrummer keyboardDrummer changed the title Enable extrancting heap mutations from expressions Fix a bug in assignment extraction logic, and enable heap mutations in expression positions Jan 30, 2026

def heapTransformProcedure (proc : Procedure) : TransformM Procedure := do
let heapName := "$heap"
let heapInName := "heap_in"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to use some names that aren't legal variable names for user-written code.

Copy link
Contributor Author

@keyboardDrummer keyboardDrummer Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, so then heap_in won't be a legal variable name?

Given that Laurel code is generally instantiated by being deserialized, the calling side will be able to choose any string they want for identifiers, including things with "$", regardless of what the Laurel parser allows.

If the calling side wants the Laurel to be parseable after printing, then they'll have to avoid using Laurel reserved keywords, in which we can include "heap_in".

But I see your point. It's easier to tell the calling side "don't use $ in identifiers" then to come with a list of disallowed words (although we'll have to do that anywhere for some syntactic things), and it's easier for us to check as well.


3. Procedure calls are transformed:
- Calls to heap-writing procedures in expressions:
`f()` => `(var freshVar: type; freshVar, heap := f(heap); freshVar)`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on what you wrote below, should this be heap, freshVar := f(heap)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I'll fix the comment, and let me also add the optimization that's shown in the second - of this list, but which wasn't part of the implementation.

| Assign (target : StmtExpr) (value : StmtExpr) (md : Imperative.MetaData Core.Expression)
/- Assign is only allowed in an impure context.
For single target assignments, use a single-element list.
Multiple targets are only allowed when the value is a StaticCall to a procedure
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eventually, this should be enforced by a WF predicate or similar.

Copy link
Contributor Author

@keyboardDrummer keyboardDrummer Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if we could already construct the proof when the AST node is constructed, and then store node specific parts with the nodes, but given that it checking that the number of return values matches the number of target values requires resolving which procedure is called, is seems difficult to do that at construction time.

If we don't store the proof with the AST, would the WF proof then be a separate tree with the same shape as the AST, but with proofs instead of values?

for target in targets do
match target with
| .Identifier varName =>
let snapshotName ← SequenceM.freshTempFor varName
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you ensure there is not already a variable with this name?

Copy link
Contributor Author

@keyboardDrummer keyboardDrummer Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add $ as a prefix. Currently there is no check that prevents front-ends from using $ in identifiers, but we can add that as part of the WF-ness check.

Would be cool if we can find name conflict bugs when we try to prove the transformation maintains semantics equivalence.

I think an alternative approach would be to let the declarations have both a displayName (for debugging) and a generated numeric identifier that's unique across the program, then we wouldn't have to add $.

Returns the transformed expression with assignments replaced by variable references.
-/
def transformExpr (expr : StmtExpr) : SequenceM StmtExpr := do
partial def transformExpr (expr : StmtExpr) : SequenceM StmtExpr := do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this partial now?

Copy link
Contributor Author

@keyboardDrummer keyboardDrummer Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes make it so that the termination proof is no longer automatic. I spent over 60 minutes trying to provide a proof, but didn't get very far. A big part is that I'm wholly unfamiliar with writing these Lean proofs.

When I add the termination metrics to the three involved recursive functions, it shows that it fails to prove some recursive calls inside processBlock. I have tried to add some have ... by sorrys there to try and get it to pass, but I end up with goals like this:

unsolved goals
expr : StmtExpr
metadata : Option Identifier
head : StmtExpr
tail : List StmtExpr
h : sizeOf tail < sizeOf (head :: tail)
⊢ sizeOf tail < sizeOf (head :: tail)

Note the last two lines. I would have thought that a tactic like

decreasing_by all_goals simp_wf <;> omega

Would take care of such trivial proofs, but apparently it does not.

There are similar very simple cases where it fails, like:

h : sizeOf head < sizeOf remStmts
⊢ sizeOf head < sizeOf (x✝ :: tail)

I think that x✝ :: tail results from pattern matching on remStmts, but for some reason it can't figure out that h is equal to the goal.

Could you help me figure out how to write this proof? Should be simple given that each recursive call is on a smaller argument.

| [] => return .Block [] metadata
| [last] => transformExpr last
| _ =>
-- Process all but the last statement
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am unclear on why you needed to change the let rec next ... structure. The head :: tail case there already handles all but the last statement (which matches the first case).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was unnecessary, although I prefer the for over the recursive call. Undid the change.

-- For assignments in block context, we need to set snapshots
-- so that subsequent expressions see the correct values
-- IMPORTANT: Use transformTarget for targets (no snapshot substitution)
-- and transformExpr for values (with snapshot substitution)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite understand what this comment means. What is a snapshot exactly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value of a mutable variable at a particular at the current execution point. I've updated the comment so it's hopefully more clear

Returns a list of statements (the original one may be split into multiple).
-/
def transformStmt (stmt : StmtExpr) : SequenceM (List StmtExpr) := do
partial def transformStmt (stmt : StmtExpr) : SequenceM (List StmtExpr) := do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing, we should avoid making things partial unless there is a good reason.

Copy link
Contributor

@MikaelMayer MikaelMayer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be worth it to prove the termination of the function you annotated as partial. We have other cases in the codebase which have proof templates for proving termination easily, I hope you can apply one of them here.

let postcond' ← heapTransformExpr heapInName postcond
let impl' ← impl.mapM (heapTransformExpr heapInName)
let modif' ← modif.mapM (heapTransformExpr heapInName)
pure (.Opaque postcond' impl' modif')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you ever wanted to avoid parentheses, you could use the following idiom

Suggested change
pure (.Opaque postcond' impl' modif')
return .Opaque postcond' impl' modif'

Copy link
Contributor Author

@keyboardDrummer keyboardDrummer Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return here would bind with the do of this line def heapTransformProcedure (proc : Procedure) : TransformM Procedure := do, so it changes the meaning, but good reminder, thanks!


def formatStmtExpr (s:StmtExpr) : Format :=
match h: s with
match s with
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I don't know if it applies here but if you ever need to do termination proofs and naming the matches causes a spurious "unused variable" despite the variable being used in automatic proofs, you could also use '_' for the variable name.

Suggested change
match s with
match _: s with

Returns a list of statements (the original one may be split into multiple).
-/
def transformStmt (stmt : StmtExpr) : SequenceM (List StmtExpr) := do
partial def transformStmt (stmt : StmtExpr) : SequenceM (List StmtExpr) := do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you try to prove termination?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants