@@ -14,7 +14,9 @@ assigned exactly once via `init`. At if-then-else join points, conditional
1414`init` expressions merge divergent variable versions.
1515
1616Preconditions: runs after `callElim` and `loopElim`.
17- SSA is semantics-preserving, so the pipeline phase uses `modelPreserving`.
17+
18+ Note: After transforming the body, the transform emits final assignments back
19+ to output/inout parameters so that the procedure's contract is preserved.
1820-/
1921
2022namespace Core
@@ -44,43 +46,27 @@ abbrev Env := Std.HashMap String VarInfo
4446/-- SSA name prefix for fresh variables. -/
4547def ssaVarPrefix (id : String) : String := s! "ssa_{ id} "
4648
49+ /-- SSA name prefix for phi (join-point merge) variables. -/
50+ def ssaPhiPrefix (id : String) : String := s! "ssa_phi_{ id} "
51+
4752/-- Generate a fresh SSA identifier using the CoreTransformM generator. -/
4853def genSSAIdent (baseName : String) : CoreTransformM Expression.Ident :=
4954 genIdent ⟨baseName, ()⟩ ssaVarPrefix
5055
56+ /-- Generate a fresh SSA phi identifier for join-point merges. -/
57+ def genSSAPhiIdent (baseName : String) : CoreTransformM Expression.Ident :=
58+ genIdent ⟨baseName, ()⟩ ssaPhiPrefix
59+
5160/-- Rewrite free variables in an expression according to the SSA environment. -/
5261def rewriteExpr (env : Env) (e : Expression.Expr) : Expression.Expr :=
53- if env.isEmpty then e
54- else
55- let sm : Map Expression.Ident Expression.Expr :=
56- env.fold (init := Map.empty) fun acc origName info =>
57- acc.insert ⟨origName, ()⟩ (createFvar info.ident)
58- Lambda.LExpr.substFvars e sm
62+ let sm : Map Expression.Ident Expression.Expr :=
63+ env.fold (init := Map.empty) fun acc origName info =>
64+ acc.insert ⟨origName, ()⟩ (createFvar info.ident)
65+ Lambda.LExpr.substFvars e sm
5966
6067/-- Rewrite free variables in an `ExprOrNondet`. -/
6168def rewriteExprOrNondet (env : Env) (e : ExprOrNondet Expression) : ExprOrNondet Expression :=
62- match e with
63- | .det expr => .det (rewriteExpr env expr)
64- | .nondet => .nondet
65-
66- /-- Helper to get the identifier from a VarInfo option, with fallback. -/
67- private def getIdOr (info fallback : Option VarInfo) (origName : String)
68- : Expression.Ident :=
69- match info with
70- | some i => i.ident
71- | none => match fallback with
72- | some i => i.ident
73- | none => ⟨origName, ()⟩
74-
75- /-- Helper to get the type from multiple VarInfo options. -/
76- private def getTyOr (a b c : Option VarInfo) : Expression.Ty :=
77- match a with
78- | some i => i.ty
79- | none => match b with
80- | some i => i.ty
81- | none => match c with
82- | some i => i.ty
83- | none => .forAll [] .bool
69+ e.map (rewriteExpr env ·)
8470
8571/-- Check if a variable changed between two environments. -/
8672private def varChanged (info pre : Option VarInfo) : Bool :=
@@ -89,6 +75,21 @@ private def varChanged (info pre : Option VarInfo) : Bool :=
8975 | some _, none => true
9076 | none, _ => false
9177
78+ /-- Compute a phi entry for a variable at a join point. Returns `none` if the
79+ variable doesn't need a merge (unchanged in both branches, or wasn't in
80+ scope before the ITE). Only merges variables present in `preEnv`. -/
81+ private def phiEntry (origName : String) (preEnv thenEnv elseEnv : Env)
82+ : Option (Expression.Ident × Expression.Ident × Expression.Ty) := do
83+ let preInfo ← preEnv.get? origName
84+ let thenInfo := thenEnv.get? origName
85+ let elseInfo := elseEnv.get? origName
86+ if !varChanged thenInfo (some preInfo) && !varChanged elseInfo (some preInfo) then
87+ .none
88+ else
89+ let thenId := (thenInfo.map (·.ident)).getD preInfo.ident
90+ let elseId := (elseInfo.map (·.ident)).getD preInfo.ident
91+ .some (thenId, elseId, preInfo.ty)
92+
9293/-- Transform a single command in SSA form. Returns updated env and new statements. -/
9394def transformCmd (env : Env) (cmd : Command) : CoreTransformM (Env × List Statement) := do
9495 match cmd with
@@ -127,69 +128,52 @@ def transformCmd (env : Env) (cmd : Command) : CoreTransformM (Env × List State
127128 return (env, [Statement.cover label (rewriteExpr env b) cmd'])
128129 | .call _ _ _ => throw (Strata.DiagnosticModel.fromMessage "SSA: unexpected call command (callElim should have run first)" )
129130
130- private def collectAllKeys (envs : List Env) : List String :=
131- let hs := envs.foldl (fun acc env =>
132- env.fold (init := acc) fun acc k _ => acc.insert k)
133- (Std.HashSet.emptyWithCapacity (α := String))
134- hs.toList
135-
136- /-- Emit conditional merge inits at a deterministic join point. -/
131+ /-- Emit conditional merge inits at a join point. Only merges variables that
132+ were in scope before the ITE (`preEnv`). The `condVar` determines which
133+ branch's value to select; for nondet branches it is itself nondet. -/
137134def emitJoinMerges (condVar : Expression.Ident)
138135 (preEnv thenEnv elseEnv : Env)
139136 (md : Imperative.MetaData Expression) : CoreTransformM (Env × List Statement) := do
140- let allKeys := collectAllKeys [preEnv, thenEnv, elseEnv]
141- let mut env := thenEnv -- start from one branch, will be overwritten for changed vars
137+ let mut env := preEnv
142138 let mut merges : List Statement := []
143- for origName in allKeys do
144- let preInfo := preEnv.get? origName
145- let thenInfo := thenEnv.get? origName
146- let elseInfo := elseEnv.get? origName
147- if varChanged thenInfo preInfo || varChanged elseInfo preInfo then
148- let thenId := getIdOr thenInfo preInfo origName
149- let elseId := getIdOr elseInfo preInfo origName
150- let ty := getTyOr thenInfo elseInfo preInfo
139+ -- Only iterate over variables that were in scope before the ITE.
140+ for (origName, _) in preEnv.toList do
141+ match phiEntry origName preEnv thenEnv elseEnv with
142+ | none => pure ()
143+ | some (thenId, elseId, ty) =>
151144 let some mty := LTy.toMonoType? ty
152145 | throw (Strata.DiagnosticModel.fromMessage s! "SSA: type of '{ origName} ' is not a monotype at join point" )
153- let freshId ← genSSAIdent origName
146+ let freshId ← genSSAPhiIdent origName
154147 let iteExpr : Expression.Expr :=
155148 Lambda.LExpr.ite () (createFvar condVar) (createFvar thenId) (createFvar elseId)
156149 merges := merges ++ [Statement.init freshId (.forAll [] mty) (.det iteExpr) md]
157150 env := env.insert origName { ident := freshId, ty := ty }
158151 incrementStat s! "{ Stats.joinPointMerges} "
159- else
160- -- Variable unchanged: keep the pre-branch version
161- match preInfo with
162- | some info => env := env.insert origName info
163- | none => pure ()
164152 return (env, merges)
165153
166- /-- Emit havoc inits at a nondet join point. -/
167- def emitNondetJoinHavocs (preEnv thenEnv elseEnv : Env)
168- (md : Imperative.MetaData Expression) : CoreTransformM (Env × List Statement) := do
169- let allKeys := collectAllKeys [preEnv, thenEnv, elseEnv]
170- let mut env := preEnv
171- let mut havoces : List Statement := []
172- for origName in allKeys do
173- let preInfo := preEnv.get? origName
174- let thenInfo := thenEnv.get? origName
175- let elseInfo := elseEnv.get? origName
176- if varChanged thenInfo preInfo || varChanged elseInfo preInfo then
177- let ty := getTyOr thenInfo elseInfo preInfo
178- let some mty := LTy.toMonoType? ty
179- | throw (Strata.DiagnosticModel.fromMessage s! "SSA: type of '{ origName} ' is not a monotype at nondet join" )
180- let freshId ← genSSAIdent origName
181- havoces := havoces ++ [Statement.init freshId (.forAll [] mty) .nondet md]
182- env := env.insert origName { ident := freshId, ty := ty }
183- incrementStat s! "{ Stats.joinPointMerges} "
184- return (env, havoces)
185-
186- /-- Initialize the SSA environment from a procedure's parameters. -/
154+ /-- Initialize the SSA environment from a procedure's parameters.
155+ Both inputs and outputs are seeded so that assignments to outputs
156+ get tracked. After transformation, `emitOutputAssignments` writes
157+ the final SSA values back to the original output identifiers. -/
187158def initEnvFromProcedure (proc : Procedure) : Env :=
188159 let env := (proc.header.inputs : List _).foldl (fun acc (id, mty) =>
189160 acc.insert id.name { ident := id, ty := .forAll [] mty }) {}
190161 (proc.header.outputs : List _).foldl (fun acc (id, mty) =>
191162 acc.insert id.name { ident := id, ty := .forAll [] mty }) env
192163
164+ /-- Emit final `set` statements to write SSA-renamed values back to the
165+ original output/inout parameter identifiers. This preserves the
166+ procedure's contract semantics. -/
167+ def emitOutputAssignments (proc : Procedure) (finalEnv : Env)
168+ (md : Imperative.MetaData Expression) : List Statement :=
169+ (proc.header.outputs : List _).filterMap fun (outId, _) =>
170+ match finalEnv.get? outId.name with
171+ | some info =>
172+ if info.ident != outId then
173+ some (Statement.set outId (createFvar info.ident) md)
174+ else none
175+ | none => none
176+
193177mutual
194178partial def transformStmt (env : Env) (s : Statement) : CoreTransformM (Env × List Statement) := do
195179 match s with
@@ -201,21 +185,26 @@ partial def transformStmt (env : Env) (s : Statement) : CoreTransformM (Env × L
201185 match cond with
202186 | .det condExpr =>
203187 let condExpr' := rewriteExpr env condExpr
204- let condVar ← genSSAIdent "$ssa_cond "
188+ let condVar ← genSSAIdent "cond "
205189 let condInit := Statement.init condVar (.forAll [] LMonoTy.bool) (.det condExpr') md
206190 let (thenEnv, thenStmts') ← transformBlock env thenStmts
207191 let (elseEnv, elseStmts') ← transformBlock env elseStmts
208192 let (mergedEnv, merges) ← emitJoinMerges condVar env thenEnv elseEnv md
209193 let iteStmt := Stmt.ite (.det (createFvar condVar)) thenStmts' elseStmts' md
210194 return (mergedEnv, [condInit, iteStmt] ++ merges)
211195 | .nondet =>
196+ -- For nondet branches, use a nondet condition variable so that
197+ -- emitJoinMerges produces ite expressions with a nondeterministic selector.
198+ let condVar ← genSSAIdent "nondet_cond"
199+ let condInit := Statement.init condVar (.forAll [] LMonoTy.bool) .nondet md
212200 let (thenEnv, thenStmts') ← transformBlock env thenStmts
213201 let (elseEnv, elseStmts') ← transformBlock env elseStmts
214- let (mergedEnv, havoces ) ← emitNondetJoinHavocs env thenEnv elseEnv md
215- return (mergedEnv, [Stmt.ite .nondet thenStmts' elseStmts' md] ++ havoces )
202+ let (mergedEnv, merges ) ← emitJoinMerges condVar env thenEnv elseEnv md
203+ return (mergedEnv, [condInit, Stmt.ite .nondet thenStmts' elseStmts' md] ++ merges )
216204 | .loop _ _ _ _ _ =>
217205 throw (Strata.DiagnosticModel.fromMessage "SSA: unexpected loop statement (loopElim should have run first)" )
218- | .exit label md => return (env, [Stmt.exit label md])
206+ | .exit _ _ =>
207+ throw (Strata.DiagnosticModel.fromMessage "SSA: unexpected exit statement" )
219208 | .funcDecl decl md => return (env, [Stmt.funcDecl decl md])
220209 | .typeDecl tc md => return (env, [Stmt.typeDecl tc md])
221210
@@ -230,12 +219,16 @@ partial def transformBlock (env : Env) (stmts : List Statement)
230219 return (curEnv, result)
231220end
232221
233- /-- Transform a single procedure into SSA form. -/
222+ /-- Transform a single procedure into SSA form. After transforming the body,
223+ emits final assignments back to output parameters so that the procedure's
224+ ensures clauses remain valid. -/
234225def transformProcedure (proc : Procedure) : CoreTransformM Procedure := do
235226 if proc.body.isEmpty then return proc
236227 let env := initEnvFromProcedure proc
237- let (_, body') ← transformBlock env proc.body
238- return { proc with body := body' }
228+ let (finalEnv, body') ← transformBlock env proc.body
229+ -- Emit final assignments: set each output param back from its SSA name.
230+ let outputAssigns := emitOutputAssignments proc finalEnv MetaData.empty
231+ return { proc with body := body' ++ outputAssigns }
239232
240233/-- SSA transformation on an entire program, using CoreTransformM. -/
241234def ssaTransform (p : Program) : CoreTransformM (Bool × Program) := do
@@ -246,7 +239,15 @@ def ssaTransform (p : Program) : CoreTransformM (Bool × Program) := do
246239 return (true , { decls := decls })
247240
248241/-- SSA pipeline phase: converts procedure bodies to SSA form.
249- SSA is semantics-preserving, so models are preserved. -/
242+
243+ Correctness status: The transform emits final assignments to output
244+ parameters to preserve procedure contracts. The `modelPreserving`
245+ annotation is justified because:
246+ - Every variable is assigned exactly once (SSA invariant)
247+ - Output parameters receive their final SSA value via explicit set
248+ - Phi merges only reference variables in scope before the ITE
249+
250+ TODO: formal proof of single-assignment, scoping, and output preservation. -/
250251def ssaPipelinePhase : PipelinePhase where
251252 transform := ssaTransform
252253 phase.name := "SSA"
0 commit comments