-
Notifications
You must be signed in to change notification settings - Fork 23
Fix a bug in assignment extraction logic, and enable heap mutations in expression positions #358
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Fix a bug in assignment extraction logic, and enable heap mutations in expression positions #358
Conversation
|
|
||
| def heapTransformProcedure (proc : Procedure) : TransformM Procedure := do | ||
| let heapName := "$heap" | ||
| let heapInName := "heap_in" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)` |
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
MikaelMayer
left a comment
There was a problem hiding this 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') |
There was a problem hiding this comment.
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
| pure (.Opaque postcond' impl' modif') | |
| return .Opaque postcond' impl' modif' |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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?
Changes
Testing
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.