Skip to content

Commit 1f0ee76

Browse files
aqjune-awsclaude
andauthored
Small-step Imperative semantics: loop/ite exit handling and invariant checking (#1052)
- Small-step `.exit` semantics: According to the previous small-step statement, `{ loop .. { exit ; } print "a"; } print "b" }` was exiting the outerblock as well and made this print "b", not "ab". This patch makes exit without destination block only escape the loop. For if { exit } else { exit }, this is consistently applied. - Loop invariants are now checked by `step_loop_{,nondet_}{enter,exit}`: each invariant must evaluate to `tt`/`ff`, and any `ff` result ORs a fresh `hasInvFailure` flag into the env's cumulative `hasFailure`, mirroring how `step_cmd` threads assert failures. (refer to: Strata/DL/Imperative/StmtSemantics.lean) - `Stmt.loop` invariants are now labeled pairs `List (String × P.Expr)` (distinct labels per invariant, like assert labels). Downstream frontends, backends, and passive-encoding transforms are updated to carry the labels through. (refer to: Strata/DL/Imperative/Stmt.lean, Strata/DL/Imperative/StmtSemantics.lean) - The Core syntax is extended to allow a label to invariants. ``` while (i < n) invariant [test]: 0 <= i { i := (i + 1); } ``` *Issue #, if available:* *Description of changes:* 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: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4f44a53 commit 1f0ee76

34 files changed

Lines changed: 992 additions & 433 deletions

Strata/Backends/CBMC/GOTO/CoreToGOTOPipeline.lean

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ private partial def unwrapCmdExt
8080
.ok (.ite (c.map (renameExpr rn)) t' e' md)
8181
| .loop g m i body md => do
8282
let body' ← body.mapM (unwrapCmdExt rn)
83-
.ok (.loop (g.map (renameExpr rn)) (m.map (renameExpr rn)) (i.map (renameExpr rn)) body' md)
83+
.ok (.loop (g.map (renameExpr rn)) (m.map (renameExpr rn))
84+
(i.map (fun (l, e) => (l, renameExpr rn e))) body' md)
8485
| .exit l md => .ok (.exit l md)
8586
| .funcDecl _d _md =>
8687
.error f!"[unwrapCmdExt] Unexpected funcDecl; should have been lifted by collectFuncDecls."
@@ -216,7 +217,7 @@ private partial def coreStmtsToGoto
216217
Imperative.emitCondGoto (CProverGOTO.Expr.not guard_expr) srcLoc trans
217218
let trans ← coreStmtsToGoto Env pname rn body trans
218219
let mut backGuard := CProverGOTO.Expr.true
219-
for inv in invariants do
220+
for (_, inv) in invariants do
220221
let inv_expr ← toExpr (renameExpr rn inv)
221222
backGuard := backGuard.setNamedField "#spec_loop_invariant" inv_expr
222223
match measure with
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/-
2+
Copyright Strata Contributors
3+
4+
SPDX-License-Identifier: Apache-2.0 OR MIT
5+
-/
6+
module
7+
8+
public import Strata.DL.Imperative.KleeneStmtSemantics
9+
import all Strata.DL.Imperative.CmdSemantics
10+
import all Strata.DL.Util.Relations
11+
12+
/-! # Properties of Kleene (nondeterministic) small-step semantics
13+
14+
Generic helpers for `StepKleene` / `StepKleeneStar` that are independent
15+
of any particular program transformation. -/
16+
17+
namespace Imperative
18+
19+
public section
20+
21+
variable {P : PureExpr} [HasFvar P] [HasBool P] [HasNot P] [HasVal P] [HasBoolVal P]
22+
23+
/-! ## Env helpers -/
24+
25+
omit [HasFvar P] [HasVal P] [HasBool P] [HasNot P] in
26+
theorem assume_env_eq (ρ : Env P) :
27+
({ ρ with store := ρ.store, hasFailure := ρ.hasFailure || false } : Env P) = ρ := by
28+
cases ρ; simp [Bool.or_false]
29+
30+
omit [HasFvar P] [HasNot P] in
31+
theorem eval_tt_is_tt
32+
(δ : SemanticEval P) (σ : SemanticStore P)
33+
(hwfv : WellFormedSemanticEvalVal δ) :
34+
δ σ HasBool.tt = some HasBool.tt :=
35+
hwfv.2 HasBool.tt σ HasBoolVal.bool_is_val.1
36+
37+
/-! ## Kleene small-step helpers -/
38+
39+
omit [HasVal P] [HasBoolVal P] in
40+
theorem kleene_seq_inner_star
41+
(inner inner' : KleeneConfig P (Cmd P)) (s2 : KleeneStmt P (Cmd P))
42+
(h : StepKleeneStar P (EvalCmd P) inner inner') :
43+
StepKleeneStar P (EvalCmd P) (.seq inner s2) (.seq inner' s2) := by
44+
induction h with
45+
| refl => exact .refl _
46+
| step _ mid _ hstep _ ih => exact .step _ _ _ (.step_seq_inner hstep) ih
47+
48+
omit [HasVal P] [HasBoolVal P] in
49+
theorem kleene_seq_terminal
50+
(s1 s2 : KleeneStmt P (Cmd P)) (ρ ρ₁ ρ' : Env P)
51+
(h1 : StepKleeneStar P (EvalCmd P) (.stmt s1 ρ) (.terminal ρ₁))
52+
(h2 : StepKleeneStar P (EvalCmd P) (.stmt s2 ρ₁) (.terminal ρ')) :
53+
StepKleeneStar P (EvalCmd P) (.stmt (.seq s1 s2) ρ) (.terminal ρ') :=
54+
.step _ _ _ .step_seq (ReflTrans_Transitive _ _ _ _
55+
(ReflTrans_Transitive _ _ _ _ (kleene_seq_inner_star _ _ s2 h1)
56+
(.step _ _ _ .step_seq_done (.refl _))) h2)
57+
58+
omit [HasVal P] [HasBoolVal P] in
59+
theorem kleene_assume_terminal
60+
{label : String} {expr : P.Expr} {md : MetaData P} {ρ₀ : Env P}
61+
(hcond : ρ₀.eval ρ₀.store expr = some HasBool.tt)
62+
(hwfb : WellFormedSemanticEvalBool ρ₀.eval) :
63+
StepKleeneStar P (EvalCmd P)
64+
(.stmt (.cmd (.assume label expr md)) ρ₀) (.terminal ρ₀) := by
65+
have raw : StepKleeneStar P (EvalCmd P)
66+
(.stmt (.cmd (.assume label expr md)) ρ₀)
67+
(.terminal { ρ₀ with store := ρ₀.store, hasFailure := ρ₀.hasFailure || false }) :=
68+
.step _ _ _ (.step_cmd (EvalCmd.eval_assume hcond hwfb)) (.refl _)
69+
rwa [assume_env_eq] at raw
70+
71+
end -- public section
72+
end Imperative

Strata/DL/Imperative/SemanticsProps.lean

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,14 @@ private theorem step_hasFailure_monotone
119119
| step_ite_false _ _ => exact hf
120120
| step_ite_nondet_true => exact hf
121121
| step_ite_nondet_false => exact hf
122-
| step_loop_enter _ _ => exact hf
123-
| step_loop_exit _ _ => exact hf
124-
| step_loop_nondet_enter => exact hf
125-
| step_loop_nondet_exit => exact hf
122+
| step_loop_enter _ _ _ _ =>
123+
simp [Config.getEnv]; left; exact hf
124+
| step_loop_exit _ _ _ _ =>
125+
simp [Config.getEnv]; left; exact hf
126+
| step_loop_nondet_enter _ _ =>
127+
simp [Config.getEnv]; left; exact hf
128+
| step_loop_nondet_exit _ _ =>
129+
simp [Config.getEnv]; left; exact hf
126130
| step_exit => exact hf
127131
| step_funcDecl => simp [Config.getEnv]; exact hf
128132
| step_typeDecl => exact hf
@@ -169,6 +173,18 @@ theorem EvalStmtsSmall_hasFailure_monotone
169173
| refl => exact hf
170174
| step _ _ _ hstep _ ih => exact ih (step_hasFailure_monotone hstep hf)
171175

176+
theorem StepStmtStar_hasFailure_monotone
177+
{P : PureExpr} {CmdT : Type} {EvalCmd : EvalCmdParam P CmdT}
178+
{extendEval : ExtendEval P}
179+
[HasBool P] [HasNot P]
180+
{c c' : Config P CmdT}
181+
(hstar : StepStmtStar P EvalCmd extendEval c c')
182+
(hf : c.getEnv.hasFailure = true) :
183+
c'.getEnv.hasFailure = true := by
184+
induction hstar with
185+
| refl => exact hf
186+
| step _ _ _ hstep _ ih => exact ih (step_hasFailure_monotone hstep hf)
187+
172188
theorem EvalStmtSmall_hasFailure_irrel
173189
{P : PureExpr} {CmdT : Type} {EvalCmd : EvalCmdParam P CmdT}
174190
{extendEval : ExtendEval P}

Strata/DL/Imperative/Stmt.lean

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ inductive Stmt (P : PureExpr) (Cmd : Type) : Type where
3737
is chosen non-deterministically. -/
3838
| ite (cond : ExprOrNondet P) (thenb : List (Stmt P Cmd)) (elseb : List (Stmt P Cmd)) (md : MetaData P)
3939
/-- An iterated execution statement. Includes an optional measure (for
40-
termination) and invariants. When `guard` is `.nondet`, the loop iterates
41-
a non-deterministic number of times. -/
42-
| loop (guard : ExprOrNondet P) (measure : Option P.Expr) (invariants : List P.Expr)
40+
termination) and labeled invariants. When `guard` is `.nondet`, the loop iterates
41+
a non-deterministic number of times. Each invariant carries a label string
42+
(expected to be distinct, like assert labels do). -/
43+
| loop (guard : ExprOrNondet P) (measure : Option P.Expr)
44+
(invariants : List (String × P.Expr))
4345
(body : List (Stmt P Cmd)) (md : MetaData P)
4446
/-- An exit statement that transfers control out of the nearest enclosing
4547
block with the given label. If no label is provided, exits the nearest
@@ -74,7 +76,7 @@ def Stmt.inductionOn {P : PureExpr} {Cmd : Type}
7476
(∀ s, s ∈ thenb → motive s) →
7577
(∀ s, s ∈ elseb → motive s) →
7678
motive (Stmt.ite cond thenb elseb md))
77-
(loop_case : ∀ (guard : ExprOrNondet P) (measure : Option P.Expr) (invariant : List P.Expr)
79+
(loop_case : ∀ (guard : ExprOrNondet P) (measure : Option P.Expr) (invariant : List (String × P.Expr))
7880
(body : List (Stmt P Cmd)) (md : MetaData P),
7981
(∀ s, s ∈ body → motive s) →
8082
motive (Stmt.loop guard measure invariant body md))
@@ -307,7 +309,11 @@ def formatStmt (P : PureExpr) (s : Stmt P C)
307309

308310
| .loop guard measure invariant body md =>
309311
let body := formatBlock P body
310-
let beforeBody := nestD f!"{line}{guard}{line}({measure}){line}{invariant}"
312+
-- Format each labeled invariant as `[lbl]: expr` (unlabeled ones just as `expr`).
313+
let invParts : List Format := invariant.map fun (l, e) =>
314+
if l.isEmpty then f!"{e}" else f!"[{l}]: {e}"
315+
let invFmt : Format := f!"[{Format.joinSep invParts f!", "}]"
316+
let beforeBody := nestD f!"{line}{guard}{line}({measure}){line}{invFmt}"
311317
let children := group f!"{beforeBody}{line}{body}"
312318
f!"{md}while{children}"
313319
| .exit label md => match label with
@@ -344,25 +350,29 @@ instance [ToFormat P.Ident] [ToFormat P.Expr] [ToFormat P.Ty] [ToFormat C]
344350
by an enclosing `block` — either within `s` itself or with a label in
345351
`labels` (representing blocks that enclose `s` externally).
346352
347-
When `s.exitsCoveredByBlocks []`, execution of `s` can never produce `.exiting`. -/
353+
When `s.exitsCoveredByBlocks []`, execution of `s` can never produce `.exiting`.
348354
349-
@[expose] def Stmt.exitsCoveredByBlocks : List String → Stmt P CmdT → Prop
355+
The labels have type `Option String` (not `String`) so that `exit` without
356+
destination block label can be considered as covered even when it is surrounded
357+
by unlabeled blocks (`[None]`). -/
358+
359+
@[expose] def Stmt.exitsCoveredByBlocks : List (Option String) → Stmt P CmdT → Prop
350360
| _, .cmd _ => True
351-
| labels, .block l ss _ => Block.exitsCoveredByBlocks (l :: labels) ss
361+
| labels, .block l ss _ => Block.exitsCoveredByBlocks (.some l :: labels) ss
352362
| labels, .ite _ tss ess _ => Block.exitsCoveredByBlocks labels tss ∧ Block.exitsCoveredByBlocks labels ess
353363
| labels, .loop _ _ _ body _ => Block.exitsCoveredByBlocks labels body
354364
| labels, .exit none _ => labels.length > 0
355-
| labels, .exit (some l) _ => l ∈ labels
365+
| labels, .exit (some l) _ => .some l ∈ labels
356366
| _, .funcDecl _ _ => True
357367
| _, .typeDecl _ _ => True
358368
where
359-
Block.exitsCoveredByBlocks : List String → List (Stmt P CmdT) → Prop
369+
Block.exitsCoveredByBlocks : List (Option String) → List (Stmt P CmdT) → Prop
360370
| _, [] => True
361371
| labels, s :: ss => Stmt.exitsCoveredByBlocks labels s ∧ Block.exitsCoveredByBlocks labels ss
362372

363373
theorem block_exitsCoveredByBlocks_append
364374
{P : PureExpr} {CmdT : Type}
365-
(labels : List String) (ss₁ ss₂ : List (Stmt P CmdT))
375+
(labels : List (Option String)) (ss₁ ss₂ : List (Stmt P CmdT))
366376
(h₁ : Stmt.exitsCoveredByBlocks.Block.exitsCoveredByBlocks labels ss₁)
367377
(h₂ : Stmt.exitsCoveredByBlocks.Block.exitsCoveredByBlocks labels ss₂) :
368378
Stmt.exitsCoveredByBlocks.Block.exitsCoveredByBlocks labels (ss₁ ++ ss₂) := by
@@ -374,7 +384,7 @@ theorem block_exitsCoveredByBlocks_append
374384
can only help. -/
375385
theorem exitsCoveredByBlocks_weaken
376386
{P : PureExpr} {CmdT : Type}
377-
(labels₁ labels₂ : List String)
387+
(labels₁ labels₂ : List (Option String))
378388
(hsub : ∀ l, l ∈ labels₁ → l ∈ labels₂) :
379389
(∀ (s : Stmt P CmdT),
380390
s.exitsCoveredByBlocks labels₁ → s.exitsCoveredByBlocks labels₂) ∧
@@ -399,8 +409,8 @@ theorem exitsCoveredByBlocks_weaken
399409
| cmd _ => intros; trivial
400410
| block l ss _ ih =>
401411
intro labels₁ labels₂ hsub h
402-
show Stmt.exitsCoveredByBlocks.Block.exitsCoveredByBlocks (l :: labels₂) ss
403-
exact ih (l :: labels₁) (l :: labels₂)
412+
show Stmt.exitsCoveredByBlocks.Block.exitsCoveredByBlocks (.some l :: labels₂) ss
413+
exact ih (.some l :: labels₁) (.some l :: labels₂)
404414
(fun x hx => by cases hx with
405415
| head => exact .head _
406416
| tail _ hm => exact .tail _ (hsub x hm))
@@ -418,7 +428,7 @@ theorem exitsCoveredByBlocks_weaken
418428
show labels₂.length > 0
419429
exact List.length_pos_iff_exists_mem.mpr
420430
(let ⟨x, hx⟩ := List.length_pos_iff_exists_mem.mp h; ⟨x, hsub x hx⟩)
421-
| some l => exact hsub l h
431+
| some l => exact hsub (.some l) h
422432
| funcDecl _ _ => intros; trivial
423433
| typeDecl _ _ => intros; trivial
424434
| nil => intros; trivial
@@ -430,7 +440,7 @@ theorem exitsCoveredByBlocks_weaken
430440
for any labels (since `.cmd` has no exit statements). -/
431441
theorem all_cmd_exitsCoveredByBlocks
432442
{P : PureExpr} {CmdT : Type}
433-
(labels : List String) (ss : List (Stmt P CmdT))
443+
(labels : List (Option String)) (ss : List (Stmt P CmdT))
434444
(h : ∀ s ∈ ss, ∃ c, s = Stmt.cmd c) :
435445
Stmt.exitsCoveredByBlocks.Block.exitsCoveredByBlocks labels ss := by
436446
induction ss with

0 commit comments

Comments
 (0)