

Lecture 1: propositions are types; proofs are terms; the kernel checks everything.
Lecture 2: inductive types, type classes, macros; proofs about programs; generalize the invariant.
Lecture 3: simp, grind, bv_decide; AI writes proofs, the kernel
checks them.
Today: put the pieces together — a verification framework, built inside Lean, and the engineering required to make one scale.

Verification frameworks in industrial use, all built as Lean libraries.
A verification condition generator for a small imperative language — with a soundness theorem.
The same programs, shallowly embedded; symbolic execution as a metaprogram.
Scalability: where the time goes, and the SymM framework.

From the course abstract:
Lean's extensibility enables the construction of entire domain-specific verification frameworks, including custom specification languages, proof strategies, and automation pipelines, all within Lean itself, with no need for external tools or trusted code generators.

An open-source Lean library of formally verified differential privacy primitives.
The verified implementation replaced a previous one — and is twice as fast.
The correctness arguments need real mathematics: Fourier analysis, number theory, topology — supplied by Mathlib.
Deployed in AWS Clean Rooms Differential Privacy.
PLDI 2025: "Verified Foundations for Differential Privacy."
"I started using Lean because of Mathlib, but I realized that Lean isn't just an excellent proof assistant, it's also a very pleasant and efficient programming language with a great ecosystem." — Jean-Baptiste Tristan

CompCert-style verified compiler for AWS Trainium, Amazon's AI accelerator. ~500,000 lines of Lean.
The ISA specification changes several times per week — not a fixed target like x86.
The Lean model doubles as a simulator shared by hardware and verification engineers.
Used to find hardware and simulator bugs, and to prove the correctness of hardware optimizations.
Long-term goal: end-to-end compilation with a semantics-preservation proof.

Kraken: a formal model of x64 for verifying sequential machine code. Jonathan Protzenko and Andres Erbsen; started after the Lean@Google hackathon, December 2025.
This lecture's architecture, applied to an instruction set: embed the machine language, give it a semantics, verify against it.
The semantics is designed for proof automation, with lessons from Bedrock2: the interpreter either computes a weakest precondition or a final state.
x64 today; arm64 in progress; RISC-V on the roadmap.
Status: pre-alpha — the tactic layer still needs Lean-side support to prove non-trivial programs. Andres returns in the scalability section, where exactly that Lean-side support is the subject.

Veil: verification of distributed protocols, built on Lean via metaprogramming. Pîrlea, Gladshtein, Kinsbruner, Zhao, Sergey.
Push-button verification through SMT (cvc5/Z3) for decidable fragments; full interactive Lean proofs when automation falls short.
Foundational: Veil's VC generator is proven sound with respect to the specification language semantics.
16 distributed protocol case studies; all 16 verified (Ivy failed on 2); 87.5% verified in under 15 seconds.


Strata: an extensible platform for formalizing language syntax and semantics. Open source.
Organizing idea: dialects (inspired by MLIR) — composable building blocks for modeling programming constructs.
Pipeline example: Python/Java/JavaScript → Laurel → Strata Core → VC generation → SMT.
The dialect definition mechanism is an embedded DSL in Lean — the macro machinery from Lecture 2, at industrial scale.


Each of these systems has the same architecture:
Embed the object language (Rust, a protocol language, an ISA) in Lean.
Give it a semantics — a function or an inductive relation.
Compute verification conditions.
Discharge them — simp, grind, bv_decide, interactive proofs.
Signal Shot —
verifying the Signal protocol and its Rust implementation (Signal, the
Beneficial AI Foundation, and the Lean FRO) — is assembling exactly
these components: Aeneas, Mathlib and CSLib, grind and SymM, AI.
The code is in the exercises project.

def Assertion : Type := State → Prop
inductive Stmt where
| skip
| assign (x : String) (e : Expr)
| seq (s₁ s₂ : Stmt)
| ite (c : BExpr) (s₁ s₂ : Stmt)
| whileDo (c : BExpr) (inv : Assertion) (body : Stmt)
State, its get/set API and notation, and the expression
evaluator are Lecture 2's, unchanged; BExpr adds boolean tests.
Only Stmt is new.
whileDo carries an invariant annotation inv — input for the
verifier. The semantics will ignore it.

[Imp| ...] Grammardeclare_syntax_cat impStmt
syntax ident " := " term "; " : impStmt
syntax "if" " (" term ")" " {" impStmt* "}" " else" " {" impStmt* "}" : impStmt
syntax "while" " (" term ")" ppLine " {" impStmt* "}" : impStmt
syntax "while" " (" term ")" " invariant" " (" term ")" ppLine " {" impStmt* "}" : impStmt
syntax "[Imp|" impStmt* "]" : term
open Lean in
macro_rules
| `([Imp| ]) => `(Stmt.skip)
| `([Imp| $x:ident := $e:term;]) => `(Stmt.assign $(quote x.getId.toString) [Expr| $e])
| `([Imp| if ($c) { $ts* } else { $es* }]) => `(Stmt.ite [BExpr| $c] [Imp| $ts*] [Imp| $es*])
| `([Imp| while ($c) { $bs* }]) => `(Stmt.whileDo [BExpr| $c] (fun _ => True) [Imp| $bs*])
| `([Imp| while ($c) invariant ($I) { $bs* }]) => `(Stmt.whileDo [BExpr| $c] $I [Imp| $bs*])
| `([Imp| $s $ss*]) => `(Stmt.seq [Imp| $s] [Imp| $ss*])
declare_syntax_cat creates a new syntactic category: statements
are not terms — they get their own grammar, and impStmt* means "a
sequence of them". Lecture 2's [Expr| extended the term grammar;
a statement language needs its own.
[Imp| ...] is the bridge back: a term whose contents are parsed
with the statement grammar. ([Expr| and [BExpr| are analogous,
and hidden here.)

def factorial : Stmt := [Imp|
n := 10;
r := 1;
while (0 < n) {
r := r * n;
n := n - 1;
}
]
After elaboration the quotation is gone: factorial : Stmt,
ordinary data built from seq, assign, and whileDo.
σ⟦x⟧/σ⟦x := v⟧ are Lecture 2's notation. Unexpanders print
states and programs back in the same syntax — goals stay readable
while stepping.

inductive BigStep : State → Stmt → State → Prop where
| skip :
BigStep σ .skip σ
| assign :
BigStep σ (.assign x e) (σ.set x (e.eval σ))
| seq (h₁ : BigStep σ s₁ σ') (h₂ : BigStep σ' s₂ σ'') :
BigStep σ (.seq s₁ s₂) σ''
| ifTrue (hc : c.eval σ = true) (h : BigStep σ s₁ σ') :
BigStep σ (.ite c s₁ s₂) σ'
| ifFalse (hc : c.eval σ = false) (h : BigStep σ s₂ σ') :
BigStep σ (.ite c s₁ s₂) σ'
| whileTrue (hc : c.eval σ = true) (hbody : BigStep σ body σ')
(hrest : BigStep σ' (.whileDo c inv body) σ'') :
BigStep σ (.whileDo c inv body) σ''
| whileFalse (hc : c.eval σ = false) :
BigStep σ (.whileDo c inv body) σ
BigStep σ s σ': executing s from state σ terminates in σ'.
One constructor per rule; derivations are finite, so the relation captures terminating runs.
The invariant annotation is ignored — it has no semantic content.

def Stmt.runGet (s : Stmt) (x : String) (fuel : Nat := 1000) : Option Int :=
(s.run State.init fuel).map (·.get x)
#evalsome 3628800 factorial.runGet "r"
some 3628800#guard factorial.runGet "r" = some 3628800
#guard factorial.runGet "n" = some 0
A fuel-bounded interpreter (Lecture 2's pattern for possibly nonterminating recursion) makes programs executable.
#guard tests before proofs, as always.

#checkStmt.run_mono {s : Stmt} {σ : State} {fuel : Nat} {σ' : State} {fuel' : Nat} :
s.run σ fuel = some σ' → fuel ≤ fuel' → s.run σ fuel' = some σ' Stmt.run_mono
Stmt.run_mono {s : Stmt} {σ : State} {fuel : Nat} {σ' : State} {fuel' : Nat} :
s.run σ fuel = some σ' → fuel ≤ fuel' → s.run σ fuel' = some σ'#checkStmt.run_sound {s : Stmt} {σ : State} {fuel : Nat} {σ' : State} : s.run σ fuel = some σ' → BigStep σ s σ' Stmt.run_sound
Stmt.run_sound {s : Stmt} {σ : State} {fuel : Nat} {σ' : State} : s.run σ fuel = some σ' → BigStep σ s σ'#checkStmt.run_complete {σ σ' : State} {s : Stmt} (h : BigStep σ s σ') : ∃ fuel, s.run σ fuel = some σ' Stmt.run_complete
Stmt.run_complete {σ σ' : State} {s : Stmt} (h : BigStep σ s σ') : ∃ fuel, s.run σ fuel = some σ'#checkBigStep.deterministic {σ σ₁ σ₂ : State} {s : Stmt} (h₁ : BigStep σ s σ₁) (h₂ : BigStep σ s σ₂) : σ₁ = σ₂ BigStep.deterministic
BigStep.deterministic {σ σ₁ σ₂ : State} {s : Stmt} (h₁ : BigStep σ s σ₁) (h₂ : BigStep σ s σ₂) : σ₁ = σ₂
Sound: whatever the interpreter computes, the semantics derives.
Complete: whatever the semantics derives, some fuel computes.
Together: run and BigStep describe the same language, so every
#guard on the previous slide is a theorem about the semantics.
All of it is in the exercises, with hints; fuel monotonicity is the workhorse lemma.

def Triple (P : Assertion) (s : Stmt) (Q : Assertion) : Prop :=
∀ σ σ', P σ → BigStep σ s σ' → Q σ'
def Stmt.wlp (s : Stmt) (Q : Assertion) : Assertion :=
fun σ => Triple (· = σ) s Q
theorem wlp_iff (s : Stmt) (Q : Assertion) (σ : State)
: s.wlp Q σ ↔ ∀ σ', BigStep σ s σ' → Q σ' := bys:StmtQ:Assertionσ:State⊢ s.wlp Q σ ↔ ∀ (σ' : State), BigStep σ s σ' → Q σ'
unfold Stmt.wlp Triples:StmtQ:Assertionσ:State⊢ (∀ (σ_1 σ' : State), (fun x => x = σ) σ_1 → BigStep σ_1 s σ' → Q σ') ↔ ∀ (σ' : State), BigStep σ s σ' → Q σ'; simps:StmtQ:Assertionσ:State⊢ (∀ (σ_1 σ' : State), σ_1 = σ → BigStep σ_1 s σ' → Q σ') ↔ ∀ (σ' : State), BigStep σ s σ' → Q σ'; constructors:StmtQ:Assertionσ:State⊢ (∀ (σ_1 σ' : State), σ_1 = σ → BigStep σ_1 s σ' → Q σ') → ∀ (σ' : State), BigStep σ s σ' → Q σ's:StmtQ:Assertionσ:State⊢ (∀ (σ' : State), BigStep σ s σ' → Q σ') → ∀ (σ_1 σ' : State), σ_1 = σ → BigStep σ_1 s σ' → Q σ'
·s:StmtQ:Assertionσ:State⊢ (∀ (σ_1 σ' : State), σ_1 = σ → BigStep σ_1 s σ' → Q σ') → ∀ (σ' : State), BigStep σ s σ' → Q σ' intro h σ's:StmtQ:Assertionσ:Stateh:∀ (σ_1 σ' : State), σ_1 = σ → BigStep σ_1 s σ' → Q σ'σ':State⊢ BigStep σ s σ' → Q σ';
exact h σ σ' rflAll goals completed! 🐙
·s:StmtQ:Assertionσ:State⊢ (∀ (σ' : State), BigStep σ s σ' → Q σ') → ∀ (σ_1 σ' : State), σ_1 = σ → BigStep σ_1 s σ' → Q σ' intro h σ' σ'' he hbs:StmtQ:Assertionσ:Stateh:∀ (σ' : State), BigStep σ s σ' → Q σ'σ':Stateσ'':Statehe:σ' = σhb:BigStep σ' s σ''⊢ Q σ''
subst σ's:StmtQ:Assertionσ:Stateh:∀ (σ' : State), BigStep σ s σ' → Q σ'σ'':Statehb:BigStep σ s σ''⊢ Q σ''
apply h σ'' hbAll goals completed! 🐙
theorem Triple_iff_wlp (P : Assertion) (s : Stmt) (Q : Assertion)
: Triple P s Q ↔ ∀ σ, P σ → s.wlp Q σ := byP:Assertions:StmtQ:Assertion⊢ Triple P s Q ↔ ∀ (σ : State), P σ → s.wlp Q σ
simp [wlp_iff, Triple]P:Assertions:StmtQ:Assertion⊢ (∀ (σ σ' : State), P σ → BigStep σ s σ' → Q σ') ↔ ∀ (σ : State), P σ → ∀ (σ' : State), BigStep σ s σ' → Q σ'; grindAll goals completed! 🐙
Partial correctness: if P holds initially and s terminates,
then Q holds finally.
s.wlp Q is the safe region of s — every terminating run from σ establishes Q. This is Dijkstra's
weakest liberal precondition.
Not a new logic: Triple is a definition, and the "rules" of Hoare
logic will be ordinary theorems about it.

theorem Triple.skip (Q : Assertion) : Triple Q .skip Q := byQ:Assertion⊢ Triple Q Stmt.skip Q
intro σ σ' h hstepQ:Assertionσ:Stateσ':Stateh:Q σhstep:BigStep σ Stmt.skip σ'⊢ Q σ'
cases hstepQ:Assertionσ:Stateh:Q σ⊢ Q σ
exact hAll goals completed! 🐙
theorem Triple.assign (Q : Assertion) (x : String) (e : Expr) :
Triple (fun σ => Q (σ.set x (e.eval σ))) (.assign x e) Q := byQ:Assertionx:Stringe:Expr⊢ Triple (fun σ => Q (σ.set x (Expr.eval σ e))) (Stmt.assign x e) Q
intro σ σ' h hstepQ:Assertionx:Stringe:Exprσ:Stateσ':Stateh:Q (σ.set x (Expr.eval σ e))hstep:BigStep σ (Stmt.assign x e) σ'⊢ Q σ'
cases hstepQ:Assertionx:Stringe:Exprσ:Stateh:Q (σ.set x (Expr.eval σ e))⊢ Q (σ.set x (Expr.eval σ e))
exact hAll goals completed! 🐙
theorem Triple.consequence (h : Triple P s Q)
(hpre : ∀ σ, P' σ → P σ) (hpost : ∀ σ, Q σ → Q' σ) :
Triple P' s Q' := byP:Assertions:StmtQ:AssertionP':AssertionQ':Assertionh:Triple P s Qhpre:∀ (σ : State), P' σ → P σhpost:∀ (σ : State), Q σ → Q' σ⊢ Triple P' s Q'
intro σ σ' hP' hstepP:Assertions:StmtQ:AssertionP':AssertionQ':Assertionh:Triple P s Qhpre:∀ (σ : State), P' σ → P σhpost:∀ (σ : State), Q σ → Q' σσ:Stateσ':StatehP':P' σhstep:BigStep σ s σ'⊢ Q' σ'
exact hpost _ (h _ _ (hpre _ hP') hstep)All goals completed! 🐙
Proof method: unfold Triple, invert the derivation with cases —
for a fixed statement shape, only matching BigStep constructors
apply.
consequence is the structural rule: weaken the precondition,
strengthen the postcondition.
The assignment rule pushes the postcondition backwards through the update — nothing is computed forwards.

example (a : Int) :
Triple (fun σ => σ⟦x⟧ = a)
(.assign "y" (.const 0))
(fun σ => σ⟦x⟧ = a ∧ σ⟦y⟧ = 0) := bya:Int⊢ Triple (fun σ => σ⟦x⟧ = a) ([Imp|y := 0; ]) fun σ => σ⟦x⟧ = a ∧ σ⟦y⟧ = 0
apply Triple.assigna:Int⊢ Triple (fun σ => σ⟦x⟧ = a) ([Imp|y := 0; ]) fun σ => σ⟦x⟧ = a ∧ σ⟦y⟧ = 0Tactic `apply` failed: could not unify the conclusion of `Triple.assign`
Triple (fun σ => ?Q (σ.set ?x (Expr.eval σ ?e))) (Stmt.assign ?x ?e) ?Q
with the goal
Triple (fun σ => σ⟦x⟧ = a) ([Imp|y := 0; ]) fun σ => σ⟦x⟧ = a ∧ σ⟦y⟧ = 0
Note: The full type of `Triple.assign` is
∀ (Q : Assertion) (x : String) (e : Expr), Triple (fun σ => Q (σ.set x (Expr.eval σ e))) (Stmt.assign x e) Q
a:Int⊢ Triple (fun σ => σ⟦x⟧ = a) ([Imp|y := 0; ]) fun σ => σ⟦x⟧ = a ∧ σ⟦y⟧ = 0
theorem Triple.assign' (P Q : Assertion) (x : String) (e : Expr) :
(∀ σ, P σ → Q (σ.set x (e.eval σ))) →
Triple P (.assign x e) Q :=
fun h => Triple.consequence (Triple.assign Q x e) h (fun _ h => h)
Triple.assign's precondition has a fixed shape — the postcondition
pushed through the update. An arbitrary precondition does not unify
with it, so apply fails.
Triple.assign' fuses assign with consequence — the proof term
is the combination. It applies to any pre/postcondition, leaving an
ordinary implication.
The design rule: theorems meant for apply must be stated for
arbitrary goals.

theorem Triple.seq (h₁ : Triple P s₁ R) (h₂ : Triple R s₂ Q) :
Triple P (.seq s₁ s₂) Q := byP:Assertions₁:StmtR:Assertions₂:StmtQ:Assertionh₁:Triple P s₁ Rh₂:Triple R s₂ Q⊢ Triple P (s₁.seq s₂) Q
intro σ σ' hP hstepP:Assertions₁:StmtR:Assertions₂:StmtQ:Assertionh₁:Triple P s₁ Rh₂:Triple R s₂ Qσ:Stateσ':StatehP:P σhstep:BigStep σ (s₁.seq s₂) σ'⊢ Q σ'
cases hstep with
| seq hs₁ hs₂ =>P:Assertions₁:StmtR:Assertions₂:StmtQ:Assertionh₁:Triple P s₁ Rh₂:Triple R s₂ Qσ:Stateσ':StatehP:P σσ'✝:Statehs₁:BigStep σ s₁ σ'✝hs₂:BigStep σ'✝ s₂ σ'⊢ Q σ' exact h₂ _ _ (h₁ _ _ hP hs₁) hs₂All goals completed! 🐙
theorem Triple.seq' :
Triple P s₁ (fun σ => Triple (· = σ) s₂ Q) →
Triple P (.seq s₁ s₂) Q := byP:Assertions₁:Stmts₂:StmtQ:Assertion⊢ (Triple P s₁ fun σ => Triple (fun x => x = σ) s₂ Q) → Triple P (s₁.seq s₂) Q
intro h₁ σ₁ σ₃ hp hbP:Assertions₁:Stmts₂:StmtQ:Assertionh₁:Triple P s₁ fun σ => Triple (fun x => x = σ) s₂ Qσ₁:Stateσ₃:Statehp:P σ₁hb:BigStep σ₁ (s₁.seq s₂) σ₃⊢ Q σ₃
cases hbP:Assertions₁:Stmts₂:StmtQ:Assertionh₁:Triple P s₁ fun σ => Triple (fun x => x = σ) s₂ Qσ₁:Stateσ₃:Statehp:P σ₁σ'✝:Stateh₁✝:BigStep σ₁ s₁ σ'✝h₂✝:BigStep σ'✝ s₂ σ₃⊢ Q σ₃
next σ₂ h₂ h₃ =>P:Assertions₁:Stmts₂:StmtQ:Assertionh₁:Triple P s₁ fun σ => Triple (fun x => x = σ) s₂ Qσ₁:Stateσ₃:Statehp:P σ₁σ₂:Stateh₂:BigStep σ₁ s₁ σ'✝h₃:BigStep σ'✝ s₂ σ₃⊢ Q σ₃ exact h₁ σ₁ σ₂ hp h₂ σ₂ σ₃ rfl h₃All goals completed! 🐙
Applying seq invents a metavariable for the intermediate assertion
R, shared by two goals — filling it falls to unification and
elaboration order.
seq' produces one goal and no metavariables: its postcondition
is definitionally s₂.wlp Q — the safe region of s₂, from the
Triples slide.
Intermediate states later enter as intro-bound variables, not holes.

theorem Triple.whileDo {inv : Assertion}
(h : Triple (fun σ => I σ ∧ c.eval σ = true) body I) :
Triple I (.whileDo c inv body) (fun σ => I σ ∧ c.eval σ = false) := byI:Assertionbody:Stmtc:BExprinv:Assertionh:Triple (fun σ => I σ ∧ BExpr.eval σ c = true) body I⊢ Triple I (Stmt.whileDo c inv body) fun σ => I σ ∧ BExpr.eval σ c = false
intro σ σ' hI hstepI:Assertionbody:Stmtc:BExprinv:Assertionh:Triple (fun σ => I σ ∧ BExpr.eval σ c = true) body Iσ:Stateσ':StatehI:I σhstep:BigStep σ (Stmt.whileDo c inv body) σ'⊢ I σ' ∧ BExpr.eval σ' c = false
exact Triple.whileDo_aux h hstep rfl hIAll goals completed! 🐙
theorem Triple.whileDo' {inv : Assertion}
(hpre : ∀ σ, P σ → inv σ)
(hbody : Triple (fun σ => inv σ ∧ c.eval σ = true) body inv)
(hpost : ∀ σ, inv σ → c.eval σ = false → Q σ) :
Triple P (.whileDo c inv body) Q :=
Triple.consequence (Triple.whileDo hbody) hpre (fun σ h => hpost σ h.1 h.2)
The proof of whileDo needs the technique from Lecture 2:
generalize the statement before inducting on the derivation.
whileDo ignores its annotation (it has no semantic role), and its
fixed shape blocks apply — the assignment story again.
Triple.whileDo' fuses it with consequence and takes the invariant
from the annotation: it applies to any goal, with no metavariable —
and its three premises are entry, preservation, and exit: the loop's
verification conditions, before we ever define vc.

def swapProg := [Imp|
t := x;
x := y;
y := t;
]
theorem swap_correct (a b : Int) :
Triple (fun σ => σ⟦x⟧ = a ∧ σ⟦y⟧ = b)
swapProg
(fun σ => σ⟦x⟧ = b ∧ σ⟦y⟧ = a) := bya:Intb:Int⊢ Triple (fun σ => σ⟦x⟧ = a ∧ σ⟦y⟧ = b) swapProg fun σ => σ⟦x⟧ = b ∧ σ⟦y⟧ = a
have h := (Triple.assign _ "t" (.var "x")).seq
((Triple.assign _ "x" (.var "y")).seq
(Triple.assign (fun σ => σ⟦x⟧ = b ∧ σ⟦y⟧ = a) "y" (.var "t")))a:Intb:Inth:Triple
(fun σ =>
σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧⟦y :=
Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧ [Expr|t]⟧⟦x⟧ =
b ∧
σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧⟦y :=
Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧ [Expr|t]⟧⟦y⟧ =
a)
([Imp|t := x; x := y; y := t; ]) fun σ => σ⟦x⟧ = b ∧ σ⟦y⟧ = a⊢ Triple (fun σ => σ⟦x⟧ = a ∧ σ⟦y⟧ = b) swapProg fun σ => σ⟦x⟧ = b ∧ σ⟦y⟧ = a
refine Triple.consequence h ?_ (fun _ h => h)a:Intb:Inth:Triple
(fun σ =>
σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧⟦y :=
Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧ [Expr|t]⟧⟦x⟧ =
b ∧
σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧⟦y :=
Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧ [Expr|t]⟧⟦y⟧ =
a)
([Imp|t := x; x := y; y := t; ]) fun σ => σ⟦x⟧ = b ∧ σ⟦y⟧ = a⊢ ∀ (σ : State),
σ⟦x⟧ = a ∧ σ⟦y⟧ = b →
σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧⟦y :=
Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧ [Expr|t]⟧⟦x⟧ =
b ∧
σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧⟦y :=
Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧ [Expr|t]⟧⟦y⟧ =
a
intro σ hσa:Intb:Inth:Triple
(fun σ =>
σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧⟦y :=
Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧ [Expr|t]⟧⟦x⟧ =
b ∧
σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧⟦y :=
Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧ [Expr|t]⟧⟦y⟧ =
a)
([Imp|t := x; x := y; y := t; ]) fun σ => σ⟦x⟧ = b ∧ σ⟦y⟧ = aσ:Statehσ:σ⟦x⟧ = a ∧ σ⟦y⟧ = b⊢ σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧⟦y :=
Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧ [Expr|t]⟧⟦x⟧ =
b ∧
σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧⟦y :=
Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧ [Expr|t]⟧⟦y⟧ =
a
simp only [Expr.eval]a:Intb:Inth:Triple
(fun σ =>
σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧⟦y :=
Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧ [Expr|t]⟧⟦x⟧ =
b ∧
σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧⟦y :=
Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧⟦x := Expr.eval σ⟦t := Expr.eval σ [Expr|x]⟧ [Expr|y]⟧ [Expr|t]⟧⟦y⟧ =
a)
([Imp|t := x; x := y; y := t; ]) fun σ => σ⟦x⟧ = b ∧ σ⟦y⟧ = aσ:Statehσ:σ⟦x⟧ = a ∧ σ⟦y⟧ = b⊢ σ⟦t := σ⟦x⟧⟧⟦x := σ⟦t := σ⟦x⟧⟧⟦y⟧⟧⟦y := σ⟦t := σ⟦x⟧⟧⟦x := σ⟦t := σ⟦x⟧⟧⟦y⟧⟧⟦t⟧⟧⟦x⟧ = b ∧
σ⟦t := σ⟦x⟧⟧⟦x := σ⟦t := σ⟦x⟧⟧⟦y⟧⟧⟦y := σ⟦t := σ⟦x⟧⟧⟦x := σ⟦t := σ⟦x⟧⟧⟦y⟧⟧⟦t⟧⟧⟦y⟧ = a
grindAll goals completed! 🐙
Compose the rules backwards from the postcondition; consequence
closes the gap to the specification; grind handles the state
bookkeeping with the two [grind =] lemmas from Lecture 3.
Correct, but manual — one rule application per statement.

theorem swap_correct' (a b : Int) :
Triple (fun σ => σ⟦x⟧ = a ∧ σ⟦y⟧ = b)
swapProg
(fun σ => σ⟦x⟧ = b ∧ σ⟦y⟧ = a) := bya:Intb:Int⊢ Triple (fun σ => σ⟦x⟧ = a ∧ σ⟦y⟧ = b) swapProg fun σ => σ⟦x⟧ = b ∧ σ⟦y⟧ = a
unfold swapProga:Intb:Int⊢ Triple (fun σ => σ⟦x⟧ = a ∧ σ⟦y⟧ = b) ([Imp|t := x; x := y; y := t; ]) fun σ => σ⟦x⟧ = b ∧ σ⟦y⟧ = a
apply Triple.seq'a:Intb:Int⊢ Triple (fun σ => σ⟦x⟧ = a ∧ σ⟦y⟧ = b) ([Imp|t := x; ]) fun σ =>
Triple (fun x => x = σ) ([Imp|x := y; y := t; ]) fun σ => σ⟦x⟧ = b ∧ σ⟦y⟧ = a
apply Triple.assign'a:Intb:Int⊢ ∀ (σ : State),
σ⟦x⟧ = a ∧ σ⟦y⟧ = b →
Triple (fun x => x = σ⟦t := Expr.eval σ [Expr|x]⟧) ([Imp|x := y; y := t; ]) fun σ => σ⟦x⟧ = b ∧ σ⟦y⟧ = a
intro σ₁ h₁a:Intb:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ σ₁⟦y⟧ = b⊢ Triple (fun x => x = σ₁⟦t := Expr.eval σ₁ [Expr|x]⟧) ([Imp|x := y; y := t; ]) fun σ => σ⟦x⟧ = b ∧ σ⟦y⟧ = a
apply Triple.seq'a:Intb:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ σ₁⟦y⟧ = b⊢ Triple (fun x => x = σ₁⟦t := Expr.eval σ₁ [Expr|x]⟧) ([Imp|x := y; ]) fun σ =>
Triple (fun x => x = σ) ([Imp|y := t; ]) fun σ => σ⟦x⟧ = b ∧ σ⟦y⟧ = a
apply Triple.assign'a:Intb:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ σ₁⟦y⟧ = b⊢ ∀ (σ : State),
σ = σ₁⟦t := Expr.eval σ₁ [Expr|x]⟧ →
Triple (fun x => x = σ⟦x := Expr.eval σ [Expr|y]⟧) ([Imp|y := t; ]) fun σ => σ⟦x⟧ = b ∧ σ⟦y⟧ = a
intro σ₂ h₂a:Intb:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ σ₁⟦y⟧ = bσ₂:Stateh₂:σ₂ = σ₁⟦t := Expr.eval σ₁ [Expr|x]⟧⊢ Triple (fun x => x = σ₂⟦x := Expr.eval σ₂ [Expr|y]⟧) ([Imp|y := t; ]) fun σ => σ⟦x⟧ = b ∧ σ⟦y⟧ = a
apply Triple.assign'a:Intb:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ σ₁⟦y⟧ = bσ₂:Stateh₂:σ₂ = σ₁⟦t := Expr.eval σ₁ [Expr|x]⟧⊢ ∀ (σ : State),
σ = σ₂⟦x := Expr.eval σ₂ [Expr|y]⟧ → σ⟦y := Expr.eval σ [Expr|t]⟧⟦x⟧ = b ∧ σ⟦y := Expr.eval σ [Expr|t]⟧⟦y⟧ = a
intro σ₃ h₃a:Intb:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ σ₁⟦y⟧ = bσ₂:Stateh₂:σ₂ = σ₁⟦t := Expr.eval σ₁ [Expr|x]⟧σ₃:Stateh₃:σ₃ = σ₂⟦x := Expr.eval σ₂ [Expr|y]⟧⊢ σ₃⟦y := Expr.eval σ₃ [Expr|t]⟧⟦x⟧ = b ∧ σ₃⟦y := Expr.eval σ₃ [Expr|t]⟧⟦y⟧ = a
simp [Expr.eval] at *a:Intb:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ σ₁⟦y⟧ = bσ₂:Stateh₂:σ₂ = σ₁⟦t := σ₁⟦x⟧⟧σ₃:Stateh₃:σ₃ = σ₂⟦x := σ₂⟦y⟧⟧⊢ σ₃⟦y := σ₃⟦t⟧⟧⟦x⟧ = b ∧ σ₃⟦y := σ₃⟦t⟧⟧⟦y⟧ = a
grindAll goals completed! 🐙
Only the apply-friendly rules appear: seq' (no metavariables)
and assign' (any precondition) — the design from the previous
slides, in action.
The proof follows the structure of the program, one rule application
per statement, receiving each intermediate state with intro —
symbolic execution, in the deep embedding.
This style returns twice today: in the shallow embedding, and in
SymM, whose benchmark drives exactly this seq' rule.

def Stmt.pre : Stmt → Assertion → Assertion
| .skip, Q => Q
| .assign x e, Q => fun σ => Q (σ.set x (e.eval σ))
| .seq s₁ s₂, Q => s₁.pre (s₂.pre Q)
| .ite c s₁ s₂, Q => fun σ => if c.eval σ then s₁.pre Q σ else s₂.pre Q σ
| .whileDo _ inv _, _ => inv
The backwards composition of swap_correct, as a function — and
a loop's precondition is its annotated invariant: the annotation
we built into Stmt finally does its job.
This is the textbook construction: Nipkow & Klein, Concrete
Semantics, §12.2.2 defines pre and vc (same names) over annotated
commands; Gordon's Hoare-logic notes (ch. 3) and Software
Foundations (Hoare2) present the same generator.

def Stmt.vc : Stmt → Assertion → Prop
| .skip, _ => True
| .assign _ _, _ => True
| .seq s₁ s₂, Q => s₁.vc (s₂.pre Q) ∧ s₂.vc Q
| .ite _ s₁ s₂, Q => s₁.vc Q ∧ s₂.vc Q
| .whileDo c inv body, Q =>
(∀ σ, inv σ → c.eval σ = true → body.pre inv σ) ∧
(∀ σ, inv σ → c.eval σ = false → Q σ) ∧
body.vc inv
vc collects what remains to be proved: for each loop, preservation
of its invariant and the exit implication; skip and assign
contribute nothing.
On loop-free programs vc is a conjunction of Trues — no proof
obligations at all; pre alone carries the meaning.

#checkvcgen_sound (s : Stmt) (Q : Assertion) (h : s.vc Q) : Triple (s.pre Q) s Q vcgen_sound
vcgen_sound (s : Stmt) (Q : Assertion) (h : s.vc Q) : Triple (s.pre Q) s Qtheorem Stmt.verify (s : Stmt) (P Q : Assertion)
(hvc : s.vc Q) (hpre : ∀ σ, P σ → s.pre Q σ) : Triple P s Q :=
Triple.consequence (vcgen_sound s Q hvc) hpre (fun _ h => h)
vcgen_sound: discharge the VCs and the triple holds. Proved by
induction; the loop case is Triple.whileDo' applied to the
induction hypothesis.

swap, Revisitedmacro "vcg" : tactic => `(tactic|
apply Stmt.verify <;>
try simp [Stmt.vc, Stmt.pre, Expr.eval, BExpr.eval])
example (a b : Int) :
Triple (fun σ => σ⟦x⟧ = a ∧ σ⟦y⟧ = b)
swapProg
(fun σ => σ⟦x⟧ = b ∧ σ⟦y⟧ = a) := bya:Intb:Int⊢ Triple (fun σ => σ⟦x⟧ = a ∧ σ⟦y⟧ = b) swapProg fun σ => σ⟦x⟧ = b ∧ σ⟦y⟧ = a
unfold swapProga:Intb:Int⊢ Triple (fun σ => σ⟦x⟧ = a ∧ σ⟦y⟧ = b) ([Imp|t := x; x := y; y := t; ]) fun σ => σ⟦x⟧ = b ∧ σ⟦y⟧ = a
vcga:Intb:Int⊢ ∀ (σ : State),
σ⟦x⟧ = a →
σ⟦y⟧ = b →
σ⟦t := σ⟦x⟧⟧⟦x := σ⟦t := σ⟦x⟧⟧⟦y⟧⟧⟦y := σ⟦t := σ⟦x⟧⟧⟦x := σ⟦t := σ⟦x⟧⟧⟦y⟧⟧⟦t⟧⟧⟦x⟧ = b ∧
σ⟦t := σ⟦x⟧⟧⟦x := σ⟦t := σ⟦x⟧⟧⟦y⟧⟧⟦y := σ⟦t := σ⟦x⟧⟧⟦x := σ⟦t := σ⟦x⟧⟧⟦y⟧⟧⟦t⟧⟧⟦y⟧ = a <;>a:Intb:Int⊢ ∀ (σ : State),
σ⟦x⟧ = a →
σ⟦y⟧ = b →
σ⟦t := σ⟦x⟧⟧⟦x := σ⟦t := σ⟦x⟧⟧⟦y⟧⟧⟦y := σ⟦t := σ⟦x⟧⟧⟦x := σ⟦t := σ⟦x⟧⟧⟦y⟧⟧⟦t⟧⟧⟦x⟧ = b ∧
σ⟦t := σ⟦x⟧⟧⟦x := σ⟦t := σ⟦x⟧⟧⟦y⟧⟧⟦y := σ⟦t := σ⟦x⟧⟧⟦x := σ⟦t := σ⟦x⟧⟧⟦y⟧⟧⟦t⟧⟧⟦y⟧ = a grindAll goals completed! 🐙
vcg is a two-line tactic macro: apply Stmt.verify, then unfold the
computed vc and pre. swap is loop-free, so no verification
conditions survive; the one remaining goal is the entailment between
specification and computed precondition — grind discharges it.
On the loop-free fragment, pre computes Dijkstra's weakest liberal
precondition — weakest provably, not just by name (exercise
extension); liberal means partial correctness, where Dijkstra's
wp additionally requires termination.
On loops, pre returns the annotation: sound,
but only as weak as the invariant you wrote. Finding invariants is the
creative step.

def copyProg (a : Int) := [Imp|
y := 0;
while (0 < x) invariant (fun σ => σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧) {
x := x - 1;
y := y + 1;
}
]
theorem copy_correct (a : Int) :
Triple (fun σ => σ⟦x⟧ = a ∧ 0 ≤ a)
(copyProg a)
(fun σ => σ⟦y⟧ = a) := bya:Int⊢ Triple (fun σ => σ⟦x⟧ = a ∧ 0 ≤ a) (copyProg a) fun σ => σ⟦y⟧ = a
unfold copyProga:Int⊢ Triple (fun σ => σ⟦x⟧ = a ∧ 0 ≤ a)
([Imp|y := 0; while (0 < x) invariant (fun σ => σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧)
{x := x - 1; y := y + 1; }])
fun σ => σ⟦y⟧ = a
vcga:Int⊢ (∀ (σ : State),
σ⟦x⟧ + σ⟦y⟧ = a →
0 ≤ σ⟦x⟧ →
decide (0 < σ⟦x⟧) = true →
σ⟦x := σ⟦x⟧ - 1⟧⟦y := σ⟦x := σ⟦x⟧ - 1⟧⟦y⟧ + 1⟧⟦x⟧ + σ⟦x := σ⟦x⟧ - 1⟧⟦y := σ⟦x := σ⟦x⟧ - 1⟧⟦y⟧ + 1⟧⟦y⟧ = a ∧
0 ≤ σ⟦x := σ⟦x⟧ - 1⟧⟦y := σ⟦x := σ⟦x⟧ - 1⟧⟦y⟧ + 1⟧⟦x⟧) ∧
∀ (σ : State), σ⟦x⟧ + σ⟦y⟧ = a → 0 ≤ σ⟦x⟧ → decide (0 < σ⟦x⟧) = false → σ⟦y⟧ = aa:Int⊢ ∀ (σ : State), σ⟦x⟧ = a → 0 ≤ a → σ⟦y := 0⟧⟦x⟧ + σ⟦y := 0⟧⟦y⟧ = a ∧ 0 ≤ σ⟦y := 0⟧⟦x⟧ <;>a:Int⊢ (∀ (σ : State),
σ⟦x⟧ + σ⟦y⟧ = a →
0 ≤ σ⟦x⟧ →
decide (0 < σ⟦x⟧) = true →
σ⟦x := σ⟦x⟧ - 1⟧⟦y := σ⟦x := σ⟦x⟧ - 1⟧⟦y⟧ + 1⟧⟦x⟧ + σ⟦x := σ⟦x⟧ - 1⟧⟦y := σ⟦x := σ⟦x⟧ - 1⟧⟦y⟧ + 1⟧⟦y⟧ = a ∧
0 ≤ σ⟦x := σ⟦x⟧ - 1⟧⟦y := σ⟦x := σ⟦x⟧ - 1⟧⟦y⟧ + 1⟧⟦x⟧) ∧
∀ (σ : State), σ⟦x⟧ + σ⟦y⟧ = a → 0 ≤ σ⟦x⟧ → decide (0 < σ⟦x⟧) = false → σ⟦y⟧ = aa:Int⊢ ∀ (σ : State), σ⟦x⟧ = a → 0 ≤ a → σ⟦y := 0⟧⟦x⟧ + σ⟦y := 0⟧⟦y⟧ = a ∧ 0 ≤ σ⟦y := 0⟧⟦x⟧ grindAll goals completed! 🐙
The same vcg <;> grind as for swap — but this time obligations
survive: the loop's verification conditions. grind discharges them —
state bookkeeping by E-matching, arithmetic by cutsat.
Choosing the invariant is the only creative step.

theorem copy_correct' (a : Int) :
Triple (fun σ => σ⟦x⟧ = a ∧ 0 ≤ a)
(copyProg a)
(fun σ => σ⟦y⟧ = a) := bya:Int⊢ Triple (fun σ => σ⟦x⟧ = a ∧ 0 ≤ a) (copyProg a) fun σ => σ⟦y⟧ = a
unfold copyProga:Int⊢ Triple (fun σ => σ⟦x⟧ = a ∧ 0 ≤ a)
([Imp|y := 0; while (0 < x) invariant (fun σ => σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧)
{x := x - 1; y := y + 1; }])
fun σ => σ⟦y⟧ = a
apply Triple.seq'a:Int⊢ Triple (fun σ => σ⟦x⟧ = a ∧ 0 ≤ a) ([Imp|y := 0; ]) fun σ =>
Triple (fun x => x = σ)
([Imp|while (0 < x) invariant (fun σ => σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧)
{x := x - 1; y := y + 1; }])
fun σ => σ⟦y⟧ = a
apply Triple.assign'a:Int⊢ ∀ (σ : State),
σ⟦x⟧ = a ∧ 0 ≤ a →
Triple (fun x => x = σ⟦y := Expr.eval σ [Expr|0]⟧)
([Imp|while (0 < x) invariant (fun σ => σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧)
{x := x - 1; y := y + 1; }])
fun σ => σ⟦y⟧ = a
intro σ₁ h₁a:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ 0 ≤ a⊢ Triple (fun x => x = σ₁⟦y := Expr.eval σ₁ [Expr|0]⟧)
([Imp|while (0 < x) invariant (fun σ => σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧)
{x := x - 1; y := y + 1; }])
fun σ => σ⟦y⟧ = a
apply Triple.whileDo'a:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ 0 ≤ a⊢ ∀ (σ : State), σ = σ₁⟦y := Expr.eval σ₁ [Expr|0]⟧ → σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧a:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ 0 ≤ a⊢ Triple (fun σ => (σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧) ∧ BExpr.eval σ [BExpr|0 < x] = true) ([Imp|x := x - 1; y := y + 1; ])
fun σ => σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧a:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ 0 ≤ a⊢ ∀ (σ : State), σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧ → BExpr.eval σ [BExpr|0 < x] = false → σ⟦y⟧ = a
·a:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ 0 ≤ a⊢ ∀ (σ : State), σ = σ₁⟦y := Expr.eval σ₁ [Expr|0]⟧ → σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧ intro σ ha:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ 0 ≤ aσ:Stateh:σ = σ₁⟦y := Expr.eval σ₁ [Expr|0]⟧⊢ σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧
simp [Expr.eval] at *a:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ 0 ≤ aσ:Stateh:σ = σ₁⟦y := 0⟧⊢ σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧
grindAll goals completed! 🐙
·a:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ 0 ≤ a⊢ Triple (fun σ => (σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧) ∧ BExpr.eval σ [BExpr|0 < x] = true) ([Imp|x := x - 1; y := y + 1; ])
fun σ => σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧ apply Triple.seq'a:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ 0 ≤ a⊢ Triple (fun σ => (σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧) ∧ BExpr.eval σ [BExpr|0 < x] = true) ([Imp|x := x - 1; ]) fun σ =>
Triple (fun x => x = σ) ([Imp|y := y + 1; ]) fun σ => σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧
apply Triple.assign'a:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ 0 ≤ a⊢ ∀ (σ : State),
(σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧) ∧ BExpr.eval σ [BExpr|0 < x] = true →
Triple (fun x => x = σ⟦x := Expr.eval σ [Expr|x - 1]⟧) ([Imp|y := y + 1; ]) fun σ => σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧
intro σ₂ h₂a:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ 0 ≤ aσ₂:Stateh₂:(σ₂⟦x⟧ + σ₂⟦y⟧ = a ∧ 0 ≤ σ₂⟦x⟧) ∧ BExpr.eval σ₂ [BExpr|0 < x] = true⊢ Triple (fun x => x = σ₂⟦x := Expr.eval σ₂ [Expr|x - 1]⟧) ([Imp|y := y + 1; ]) fun σ => σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧
apply Triple.assign'a:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ 0 ≤ aσ₂:Stateh₂:(σ₂⟦x⟧ + σ₂⟦y⟧ = a ∧ 0 ≤ σ₂⟦x⟧) ∧ BExpr.eval σ₂ [BExpr|0 < x] = true⊢ ∀ (σ : State),
σ = σ₂⟦x := Expr.eval σ₂ [Expr|x - 1]⟧ →
σ⟦y := Expr.eval σ [Expr|y + 1]⟧⟦x⟧ + σ⟦y := Expr.eval σ [Expr|y + 1]⟧⟦y⟧ = a ∧
0 ≤ σ⟦y := Expr.eval σ [Expr|y + 1]⟧⟦x⟧
intro σ₃ h₃a:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ 0 ≤ aσ₂:Stateh₂:(σ₂⟦x⟧ + σ₂⟦y⟧ = a ∧ 0 ≤ σ₂⟦x⟧) ∧ BExpr.eval σ₂ [BExpr|0 < x] = trueσ₃:Stateh₃:σ₃ = σ₂⟦x := Expr.eval σ₂ [Expr|x - 1]⟧⊢ σ₃⟦y := Expr.eval σ₃ [Expr|y + 1]⟧⟦x⟧ + σ₃⟦y := Expr.eval σ₃ [Expr|y + 1]⟧⟦y⟧ = a ∧
0 ≤ σ₃⟦y := Expr.eval σ₃ [Expr|y + 1]⟧⟦x⟧
simp [Expr.eval, BExpr.eval] at *a:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ 0 ≤ aσ₂:Stateh₂:(σ₂⟦x⟧ + σ₂⟦y⟧ = a ∧ 0 ≤ σ₂⟦x⟧) ∧ decide (0 < σ₂⟦x⟧) = trueσ₃:Stateh₃:σ₃ = σ₂⟦x := σ₂⟦x⟧ - 1⟧⊢ σ₃⟦y := σ₃⟦y⟧ + 1⟧⟦x⟧ + σ₃⟦y := σ₃⟦y⟧ + 1⟧⟦y⟧ = a ∧ 0 ≤ σ₃⟦y := σ₃⟦y⟧ + 1⟧⟦x⟧
grindAll goals completed! 🐙
·a:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ 0 ≤ a⊢ ∀ (σ : State), σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧ → BExpr.eval σ [BExpr|0 < x] = false → σ⟦y⟧ = a intro σ hI hca:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ 0 ≤ aσ:StatehI:σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧hc:BExpr.eval σ [BExpr|0 < x] = false⊢ σ⟦y⟧ = a
simp [BExpr.eval, Expr.eval] at hca:Intσ₁:Stateh₁:σ₁⟦x⟧ = a ∧ 0 ≤ aσ:StatehI:σ⟦x⟧ + σ⟦y⟧ = a ∧ 0 ≤ σ⟦x⟧hc:decide (0 < σ⟦x⟧) = false⊢ σ⟦y⟧ = a
grindAll goals completed! 🐙
Removing the object language.

def Triple (P : S → Prop) (k : StateM S α) (Q : α → S → Prop) : Prop :=
∀ s, P s → Q (k s).1 (k s).2
def double : StateM Nat Unit := do
let s ← get
set (s + s)
Imp was a deep embedding: programs are data, semantics is a
relation. Shallow alternative: programs are ordinary monadic Lean
code in Lecture 2's StateM — the style of mvcgen, Velvet, and
Aeneas.
The judgment is the same Hoare triple; only the semantics changed. A
StateM program is a function, so "every terminating run" is the
run: quantifying over BigStep σ s σ' becomes running the program —
(k s).1 the returned value, (k s).2 the final state.
No inductive semantics needed, and partial and total correctness
coincide (StateM programs are total and deterministic).

theorem Triple.get (h : ∀ s, P s → Q s s) : Triple P get Q := byS✝:Type u_1P:S✝ → PropQ:S✝ → S✝ → Proph:∀ (s : S✝), P s → Q s s⊢ Triple P MonadState.get Q
intro s hPS✝:Type u_1P:S✝ → PropQ:S✝ → S✝ → Proph:∀ (s : S✝), P s → Q s ss:S✝hP:P s⊢ Q (MonadState.get s).fst (MonadState.get s).snd
simp [MonadState.get, getThe, MonadStateOf.get, StateT.get, Pure.pure]S✝:Type u_1P:S✝ → PropQ:S✝ → S✝ → Proph:∀ (s : S✝), P s → Q s ss:S✝hP:P s⊢ Q s s
exact h _ hPAll goals completed! 🐙
theorem Triple.bind (k₁ : StateM S α) (k₂ : α → StateM S β)
(Q : β → S → Prop)
(h : Triple P k₁ (fun a s => Triple (· = s) (k₂ a) Q)) : Triple P (k₁ >>= k₂) Q := byS:Type u_1α:Type u_1β:Type u_1P:S → Propk₁:StateM S αk₂:α → StateM S βQ:β → S → Proph:Triple P k₁ fun a s => Triple (fun x => x = s) (k₂ a) Q⊢ Triple P (k₁ >>= k₂) Q
intro s hPS:Type u_1α:Type u_1β:Type u_1P:S → Propk₁:StateM S αk₂:α → StateM S βQ:β → S → Proph:Triple P k₁ fun a s => Triple (fun x => x = s) (k₂ a) Qs:ShP:P s⊢ Q ((k₁ >>= k₂) s).fst ((k₁ >>= k₂) s).snd
exact h s hP (k₁ s).2 rflAll goals completed! 🐙
Part A's design carries over verbatim: every rule is primed —
arbitrary P, no metavariables. Triple.get has the shape of
assign'; Triple.bind is seq', with the result value a
passed to the continuation.
The proofs are all simple.
Rules for pure, set, modify, ite, and consequence complete
the set.

applyexample (a : Nat) : Triple (· = a) double (fun _ s => s = 2 * a) := bya:Nat⊢ Triple (fun x => x = a) double fun x s => s = 2 * a
unfold doublea:Nat⊢ Triple (fun x => x = a)
(do
let s ← get
set (s + s))
fun x s => s = 2 * a
apply Triple.binda:Nat⊢ Triple (fun x => x = a) get fun a_1 s => Triple (fun x => x = s) (set (a_1 + a_1)) fun x s => s = 2 * a
apply Triple.geta:Nat⊢ ∀ (s : Nat), s = a → Triple (fun x => x = s) (set (s + s)) fun x s => s = 2 * a
intro s ha:Nats:Nath:s = a⊢ Triple (fun x => x = s) (set (s + s)) fun x s => s = 2 * a
apply Triple.seta:Nats:Nath:s = a⊢ ∀ (s_1 : Nat), s_1 = s → s + s = 2 * a
intro s' h'a:Nats:Nath:s = as':Nath':s' = s⊢ s + s = 2 * a
grindAll goals completed! 🐙
The same stepping discipline as swap and copyProg in part A:
apply the rule for the head of the program, intro the reached
state.
The final goal is the verification condition s + s = 2 * a, with
h : s = a in context.
Entirely mechanical — the rule is determined by the head of the program.

open Lean Elab Tactic in
elab "sym_step" : tactic => do
let goal ← getMainGoal
let tgt ← goal.withContext do instantiateMVars (← goal.getType)
let_expr Triple _ _ _ k _ := tgt.headBeta
| throwError "sym_step: not a `Triple` goal"
let app (r : Name) : TacticM Unit := do
evalTactic (← `(tactic| apply $(mkIdent r) <;> intro _ _))
match k.getAppFn.constName? with
| some ``Bind.bind => evalTactic (← `(tactic| apply Triple.bind))
| some ``get => app ``Triple.get
| some ``set => app ``Triple.set
| some ``modify => app ``Triple.modify
| some ``pure => app ``Triple.pure
| some ``ite => evalTactic (← `(tactic| apply Triple.ite <;> intro _))
| _ => throwError "sym_step: no rule for{indentExpr k}"
macro "sym_run" : tactic => `(tactic| repeat' sym_step)
A tactic is a Lean program: read the goal, inspect the head, apply
the matching rule — twenty lines. (Trying the rules blindly with
apply would send the unifier unfolding >>= and StateT;
dispatching on the head avoids the search.)
This is the metaprogramming promised in the abstract, in its smallest useful form.

def addN : Nat → StateM Nat Unit
| 0 => pure ()
| n + 1 => do
modify (· + 1)
addN n
theorem addN_correct (n : Nat) (a : Nat) :
Triple (· = a) (addN n) (fun _ s' => s' = a + n) := byn:Nata:Nat⊢ Triple (fun x => x = a) (addN n) fun x s' => s' = a + n
induction n generalizing a with
| zero =>a:Nat⊢ Triple (fun x => x = a) (addN 0) fun x s' => s' = a + 0 simp only [addN]a:Nat⊢ Triple (fun x => x = a) (pure ()) fun x s' => s' = a + 0; sym_runa:Nats✝:Nata✝:s✝ = a⊢ s✝ = a + 0; grindAll goals completed! 🐙
| succ n ih =>n:Natih:∀ (a : Nat), Triple (fun x => x = a) (addN n) fun x s' => s' = a + na:Nat⊢ Triple (fun x => x = a) (addN (n + 1)) fun x s' => s' = a + (n + 1)
simp only [addN]n:Natih:∀ (a : Nat), Triple (fun x => x = a) (addN n) fun x s' => s' = a + na:Nat⊢ Triple (fun x => x = a)
(do
modify fun x => x + 1
addN n)
fun x s' => s' = a + (n + 1)
sym_runn:Natih:∀ (a : Nat), Triple (fun x => x = a) (addN n) fun x s' => s' = a + na:Nats✝:Nata✝:s✝ = a⊢ Triple (fun x => x = s✝ + 1) (addN n) fun x s' => s' = a + (n + 1)
exact Triple.consequence (ih _) (fun _ h => h) (byn:Natih:∀ (a : Nat), Triple (fun x => x = a) (addN n) fun x s' => s' = a + na:Nats✝:Nata✝:s✝ = a⊢ ∀ (a_1 : Unit) (s : Nat), s = s✝ + 1 + n → s = a + (n + 1) grindAll goals completed! 🐙)
sym_run executes up to the recursive call; the induction hypothesis
plays the role of the loop invariant. generalizing a — Lecture 2's
lesson, again: the recursive call starts from a different state.
Triple.consequence bridges the induction hypothesis to the goal,
the same role it played in part A.

A VC generator turns code and specifications into proof obligations. Lean-based examples of the idiom we just built:
Aeneas — Rust verification via translation to Lean.
Velvet — a Dafny-style verifier built in Lean.
mvcgen — Lean's VC generator for monadic programs.
Verification condition generation with proof assistants has historically not scaled — documented for Rocq by Chlipala's group, and the same held in Lean.

Andres Erbsen (Google; Bedrock2, Fiat
Cryptography) distilled the problem into a minimal challenge at the
Lean@Google hackathon: symbolically execute a generated n-step
program — the apply/simp loop from this lecture, at size n.
With Lean's general-purpose tactic framework (MetaM): superlinear —
2073 ms at n = 100, failure before n = 700.

The symbolic execution loop needs:
efficient apply — no unification search (the reason sym_step
dispatches on the head);
efficient metavariable management;
term sharing preserved by every operation — no copied trees;
simplifier results cached and reused across obligations and steps;
no repeated traversal of the same subterms.
General-purpose tactic frameworks pay for flexibility these loops do not need — arbitrary proof-state surgery, full definitional unfolding in matching.

SymMA monadic framework for symbolic simulation and VC generation, in the Lean sources.
The key invariant: the local context grows monotonically — no reverts, no deletions. Then pointer equality is a valid cache key, and automation state survives from one VC to the next.
Precompiled backward rules; mostly-syntactic matching in the hot path.
A new simplifier: pointer-keyed caches on maximally shared terms, binder traversal without quadratic cost.
grind state threaded through the VC loop: facts learned once are
reused across obligations.

SymM: Measurements


On the benchmark above: 16 ms at n = 100 (vs. 2073 ms) — about 100×; linear out to n = 700.

mvcgen (Sebastian Graf): linear out to n = 1000 on the challenge.
Velvet (V. Gladshtein): previously ~3× slower than Dafny; after the port, faster on 24 of 27 benchmarks. Dafny trusts its VC generator and an SMT solver; Velvet's chain is checked by the kernel.
DyLean (T. Wallez, cryptographic protocols): SymM + incremental
grind gave a 50× speedup in VC discharge.

Foundations: dependent type theory; propositions as types; the kernel as the single arbiter.
Programming and proving: inductive types, type classes, macros; design lemmas so proofs follow definitions.
Automation: simp, grind, bv_decide — inspectable,
extensible, kernel-checked; AI as a user of the same machinery.
Verification: Hoare logic as theorems, VC generation as a verified function, symbolic execution as a metaprogram, and the engineering that scales it — all inside Lean: one kernel checks programs, specifications, proofs, and the verifier itself.

Exercises: Lecture4.lean (the VC generator) and Lecture4b.lean
(shallow embedding + sym_step) — both with full solutions and notes.
Theorem Proving in Lean 4 · Functional Programming in Lean · the reference manual
CSLib — a growing library of computer science foundations; contributions welcome.
Mathlib — the mathematical library.
Iris Lean — concurrent separation logic, being ported to Lean.
