Hoare Logic

3. Imp: Simple Imperative Programs🔗

Note to developers (before next release)

Needs some WORKINCLASSes and some quizzes

LATER: Another nice challenge exercise at some point would be to add C-style arrays (i.e., indirect read/write). This sets up some really nice challenge problems in Hoare (reasoning about arrays / aliasing / etc.).

SOONER: BCP 25: Maybe we should write / instead of && in assertions, to save a mismatch in the dec_minimum exercise in Hoare₂?

At some point we could consider moving material from the old HoareLists to this chapter (and into later files, as appropriate). We haven't done it yet because it's a shame to complicate the nice simple presentation here when it's used as the basis for applications like Xavier's static analysis lectures. Also, we now have a whole volume on real separation logic...

We concentrate here on defining the syntax and semantics of Imp; later in this volume we develop a theory of program equivalence and introduce Hoare Logic, a popular logic for reasoning about imperative programs.

3.1. Expressions With Variables🔗

3.1.1. States🔗

Since we'll want to look variables up to find out their current values, we'll use total maps from the Typeclasses chapter of Logical Foundations. A machine state (or just state) represents the current values of all variables at some point in the execution of a program.

We give the type of variable identifiers a name, Ident. For now it is just String; naming it makes the intent clearer.

open scoped MyGetElem abbrev Ident := String abbrev State := TotalMap Ident Nat

3.1.2. Syntax🔗

We can add variables to the arithmetic expressions we had before simply by including one more constructor. (This is a fresh Aexp, replacing the variable-free one from the Slang chapter.)

inductive Aexp where | num (n : Nat) | id (x : Ident) -- NEW | plus (a₁ a₂ : Aexp) | minus (a₁ a₂ : Aexp) | mult (a₁ a₂ : Aexp)

The Bexp definition is unchanged, except that it now refers to the new Aexp.

inductive Bexp where | bool (b : Bool) | eq (a₁ a₂ : Aexp) | neq (a₁ a₂ : Aexp) | le (a₁ a₂ : Aexp) | gt (a₁ a₂ : Aexp) | not (b : Bexp) | and (b₁ b₂ : Bexp)

Defining a few variable names as shorthands will make examples easier to read.

abbrev W : Ident := "W" abbrev X : Ident := "X" abbrev Y : Ident := "Y" abbrev Z : Ident := "Z"

3.1.3. Notations🔗

Notation encoding: arithmetic expressions/-- Arithmetic expressions of Imp -/ declare_syntax_cat imp_aexp /-- Numeric literal -/ syntax:max num : imp_aexp /-- `Ident` or Lean identifier -/ syntax:max ident : imp_aexp /-- Addition -/ syntax:65 imp_aexp:65 " + " imp_aexp:66 : imp_aexp /-- Subtraction -/ syntax:65 imp_aexp:65 " - " imp_aexp:66 : imp_aexp /-- Multiplication -/ syntax:70 imp_aexp:70 " * " imp_aexp:71 : imp_aexp /-- Parentheses for grouping -/ syntax:max "(" imp_aexp ")" : imp_aexp /-- Escape to Lean -/ syntax:max "~" term:max : imp_aexp /-- Embed an Imp arithmetic expression into a Lean term -/ syntax:80 "aexp " "{" imp_aexp "}" : term
namespace Imp.Elab open Lean Elab Term Meta def withSourceInfoOf {kind : Name} (ref : Syntax) (stx : TSyntax kind) (canonical := true) : TSyntax kind := let info := SourceInfo.fromRef ref (canonical := canonical) ⟨stx.raw.setInfo info⟩ def isGreek (c : Char) : Bool := let n := c.val.toNat decide ( (0x0370 ≤ n ∧ n ≤ 0x03ff) ∨ (0x1f00 ≤ n ∧ n ≤ 0x1fff)) inductive IdentKind where | object | metavar deriving BEq def classifyIdent? (id : Lean.Ident) : Option (IdentKind × String) := do let Name.str .anonymous s := id.getId.eraseMacroScopes | failure if s.isEmpty || s.contains '.' then failure let c := s.front if c.isUpper then return (.object, s) else if c.isLower || isGreek c then return (.metavar, s) else failure def mkObjectIdentFrom (ref : Syntax) (name : String) : Lean.Ident := mkIdentFrom ref (Name.mkSimple name) def elabMetavarOnlyIdent (what : String) (expectedType : Term) (x : Lean.Ident) : MacroM Term := do match classifyIdent? x with | some (.metavar, _) => `(($x : $expectedType)) | some (.object, name) => Macro.throwErrorAt x s!"no Imp {what} named `{name}`; capitalized bare names are always \ read as Imp identifiers — use a lowercase name or escape with `~` to refer to Lean name" | none => Macro.throwErrorAt x "invalid bare identifier" macro_rules | `(aexp { $exp:imp_aexp }) => do let stx ← match exp with | `(imp_aexp| $n:num) => ``(Aexp.num $n) | `(imp_aexp| ~$e:term) => ``(($e : Aexp)) | `(imp_aexp| $x:ident) => match classifyIdent? x with | some (.object, name) => let nameLit : Term := ⟨Syntax.mkStrLit name⟩ ``(Aexp.id $nameLit) | some (.metavar, _) => ``(($x : Aexp)) | none => Macro.throwErrorAt x "invalid bare identifier" | `(imp_aexp| $a + $b) => ``(Aexp.plus (aexp {$a}) (aexp {$b})) | `(imp_aexp| $a - $b) => ``(Aexp.minus (aexp {$a}) (aexp {$b})) | `(imp_aexp| $a * $b) => ``(Aexp.mult (aexp {$a}) (aexp {$b})) | `(imp_aexp| ($a)) => ``(aexp {$a}) | _ => Lean.Macro.throwUnsupported return withSourceInfoOf exp stx end Imp.Elab
Notation encoding: boolean expressions/-- Boolean expressions of Imp -/ declare_syntax_cat imp_bexp /-- Boolean literal (`true` or `false`) and Lean identifier -/ syntax:max ident : imp_bexp /-- Equality of arithmetic expressions -/ syntax:50 imp_aexp:51 " = " imp_aexp:51 : imp_bexp /-- Disequality of arithmetic expressions -/ syntax:50 imp_aexp:51 " ≠ " imp_aexp:51 : imp_bexp /-- Less than or equal -/ syntax:50 imp_aexp:51 " ≤ " imp_aexp:51 : imp_bexp /-- Greater than -/ syntax:50 imp_aexp:51 " > " imp_aexp:51 : imp_bexp /-- Boolean negation -/ syntax:70 "¬ " imp_bexp:70 : imp_bexp /-- Boolean conjunction (right associative) -/ syntax:35 imp_bexp:36 " ∧ " imp_bexp:35 : imp_bexp /-- Parentheses for grouping -/ syntax:max "(" imp_bexp ")" : imp_bexp /-- Escape to Lean -/ syntax:max "~" term:max : imp_bexp /-- Embed an Imp boolean expression into a Lean term -/ syntax:80 "bexp " "{" imp_bexp "}" : term
Notation encoding: boolean expressions, macro rulesnamespace Imp.Elab open Lean macro_rules | `(bexp { $exp:imp_bexp }) => do let stx ← match exp with | `(imp_bexp| true) => ``(Bexp.bool true) | `(imp_bexp| false) => ``(Bexp.bool false) | `(imp_bexp| $x:ident) => do elabMetavarOnlyIdent "boolean" (← ``(Bexp)) x | `(imp_bexp| ~$e:term) => ``(($e : Bexp)) | `(imp_bexp| $a:imp_aexp = $b:imp_aexp) => ``(Bexp.eq (aexp {$a}) (aexp {$b})) | `(imp_bexp| $a:imp_aexp ≠ $b:imp_aexp) => ``(Bexp.neq (aexp {$a}) (aexp {$b})) | `(imp_bexp| $a:imp_aexp ≤ $b:imp_aexp) => ``(Bexp.le (aexp {$a}) (aexp {$b})) | `(imp_bexp| $a:imp_aexp > $b:imp_aexp) => ``(Bexp.gt (aexp {$a}) (aexp {$b})) | `(imp_bexp| ¬ $b:imp_bexp) => ``(Bexp.not (bexp {$b})) | `(imp_bexp| $b₁:imp_bexp ∧ $b₂:imp_bexp) => ``(Bexp.and (bexp {$b₁}) (bexp {$b₂})) | `(imp_bexp| ($b:imp_bexp)) => ``(bexp {$b}) | _ => Macro.throwUnsupported return withSourceInfoOf exp stx end Imp.Elab
(Aexp.num 3).plus ((Aexp.id "X").mult (Aexp.num 2)) : Aexp#check aexp { 3 + (X * 2) } (Bexp.bool true).and (Bexp.le (Aexp.id "X") (Aexp.num 4)).not : Bexp#check bexp { true ∧ ¬(X ≤ 4) }

3.1.4. Delaborators🔗

Notation encoding: printing expressions backnamespace Imp.Delab open Lean PrettyPrinter Delaborator SubExpr Parenthesizer Imp.Elab @[category_parenthesizer imp_aexp] def imp_aexp.parenthesizer : CategoryParenthesizer := fun prec => do maybeParenthesize `imp_aexp true wrapParens prec <| parenthesizeCategoryCore `imp_aexp prec where wrapParens (stx : Syntax) : Syntax := Unhygienic.run do let stxInfo := SourceInfo.fromRef stx let stx := stx.setInfo .none let pstx ← `(imp_aexp| ($(⟨stx⟩))) return pstx.raw.setInfo stxInfo @[category_parenthesizer imp_bexp] def imp_bexp.parenthesizer : CategoryParenthesizer := fun prec => do Parenthesizer.maybeParenthesize `imp_bexp true wrapParens prec <| Parenthesizer.parenthesizeCategoryCore `imp_bexp prec where wrapParens (stx : Syntax) : Syntax := Unhygienic.run do let stxInfo := SourceInfo.fromRef stx let stx := stx.setInfo .none let pstx ← `(imp_bexp| ($(⟨stx⟩))) return pstx.raw.setInfo stxInfo
Notation encoding: registering the delaborators/-- Recognizes a term as being an `aexp { ... }` expression. -/ def getAexp (stx : Term) : TSyntax `imp_aexp := withSourceInfoOf (canonical := false) stx <| Unhygienic.run do match stx with | `(aexp { $e:imp_aexp }) => return e | `($id:ident) => match classifyIdent? id with | some (.metavar, _) => `(imp_aexp| $id:ident) | _ => `(imp_aexp| ~$stx) | _ => `(imp_aexp| ~$stx) @[app_unexpander Aexp.num] private def Aexp.unexpandNum : Unexpander | `($_ $n:num) => `(aexp { $n:num }) | _ => throw () @[app_unexpander Aexp.id] private def Aexp.unexpandId : Unexpander | `($_ $s:str) => do let id := mkObjectIdentFrom s.raw s.getString match classifyIdent? id with | some (.object, _) => `(aexp { $id:ident }) | _ => throw () | _ => throw () @[app_unexpander Aexp.plus] private def Aexp.unexpandPlus : Unexpander | `($_ $a $b) => `(aexp { $(getAexp a) + $(getAexp b) }) | _ => throw () @[app_unexpander Aexp.minus] private def Aexp.unexpandMinus : Unexpander | `($_ $a $b) => `(aexp { $(getAexp a) - $(getAexp b) }) | _ => throw () @[app_unexpander Aexp.mult] private def Aexp.unexpandMult : Unexpander | `($_ $a $b) => `(aexp { $(getAexp a) * $(getAexp b) }) | _ => throw () /-- Recognizes a term as being an `bexp { ... }` expression. -/ def getBexp (stx : Term) : TSyntax `imp_bexp := withSourceInfoOf (canonical := false) stx <| Unhygienic.run do match stx with | `(bexp { $e:imp_bexp }) => return e | `($id:ident) => match classifyIdent? id with | some (.metavar, _) => `(imp_bexp| $id:ident) | _ => `(imp_bexp| ~$stx) | _ => `(imp_bexp| ~$stx) /-- Delaborator for `Bexp.bool`. This is needed since we want to be sure we are matching on the actual `true`/`false` expressions, rather than matching on the delaborated identifiers `true`/`false` (which might not be accurate). -/ @[app_delab Bexp.bool] private def BExp.delabBool : Delab := whenPPOption getPPNotation do let e ← getExpr guard <| e.isAppOfArity ``Bexp.bool 1 match_expr e.appArg! with | true => `(bexp { $(mkIdent `true):ident }) | false => `(bexp { $(mkIdent `false):ident }) | _ => failure @[app_unexpander Bexp.eq] private def Bexp.unexpandEq : Unexpander | `($_ $a $b) => `(bexp { $(getAexp a):imp_aexp = $(getAexp b):imp_aexp }) | _ => throw () @[app_unexpander Bexp.neq] private def Bexp.unexpandNeq : Unexpander | `($_ $a $b) => `(bexp { $(getAexp a):imp_aexp ≠ $(getAexp b):imp_aexp }) | _ => throw () @[app_unexpander Bexp.le] private def Bexp.unexpandLe : Unexpander | `($_ $a $b) => `(bexp { $(getAexp a):imp_aexp ≤ $(getAexp b):imp_aexp }) | _ => throw () @[app_unexpander Bexp.gt] private def Bexp.unexpandGt : Unexpander | `($_ $a $b) => `(bexp { $(getAexp a):imp_aexp > $(getAexp b):imp_aexp }) | _ => throw () @[app_unexpander Bexp.not] private def Bexp.unexpandNot : Unexpander | `($_ $a) => `(bexp { ¬ $(getBexp a):imp_bexp }) | _ => throw () @[app_unexpander Bexp.and] private def Bexp.unexpandAnd : Unexpander | `($_ $a $b) => `(bexp { $(getBexp a):imp_bexp ∧ $(getBexp b):imp_bexp }) | _ => throw () end Imp.Delab
/-- info: aexp {3 + X * 2} : Aexp -/ #guard_msgs in #check aexp { 3 + (X * 2) } /-- info: bexp {true ∧ ¬ (X ≤ 4)} : Bexp -/ #guard_msgs in #check bexp { true ∧ ¬(X ≤ 4) }

3.1.5. Evaluation🔗

Now we need to add an st parameter to both evaluation functions:

def Aexp.eval (st : State) (a : Aexp) : Nat := match a with | num n => n | id x => st[x] -- NEW | plus a₁ a₂ => a₁.eval st + a₂.eval st | minus a₁ a₂ => a₁.eval st - a₂.eval st | mult a₁ a₂ => a₁.eval st * a₂.eval st def Bexp.eval (st : State) (b : Bexp) : Bool := match b with | bool b => b | eq a₁ a₂ => a₁.eval st == a₂.eval st | neq a₁ a₂ => a₁.eval st != a₂.eval st | le a₁ a₂ => a₁.eval st ≤ a₂.eval st | gt a₁ a₂ => a₁.eval st > a₂.eval st | not b₁ => !b₁.eval st | and b₁ b₂ => b₁.eval st && b₂.eval st @[simp] theorem Aexp.eval_num (st : State) (n : Nat) : (num n).eval st = n := rfl @[simp] theorem Aexp.eval_id (st : State) (x : Ident) : (Aexp.id x).eval st = st[x] := rfl @[simp] theorem Aexp.eval_plus (st : State) (a₁ a₂ : Aexp) : (plus a₁ a₂).eval st = a₁.eval st + a₂.eval st := rfl @[simp] theorem Aexp.eval_minus (st : State) (a₁ a₂ : Aexp) : (minus a₁ a₂).eval st = a₁.eval st - a₂.eval st := rfl @[simp] theorem Aexp.eval_mult (st : State) (a₁ a₂ : Aexp) : (mult a₁ a₂).eval st = a₁.eval st * a₂.eval st := rfl @[simp] theorem Bexp.eval_bool (st : State) (b : Bool) : (bool b).eval st = b := rfl @[simp] theorem Bexp.eval_eq (st : State) (a₁ a₂ : Aexp) : (eq a₁ a₂).eval st = (a₁.eval st == a₂.eval st) := rfl @[simp] theorem Bexp.eval_neq (st : State) (a₁ a₂ : Aexp) : (neq a₁ a₂).eval st = (a₁.eval st != a₂.eval st) := rfl @[simp] theorem Bexp.eval_le (st : State) (a₁ a₂ : Aexp) : (le a₁ a₂).eval st = (a₁.eval st ≤ a₂.eval st : Bool) := rfl @[simp] theorem Bexp.eval_gt (st : State) (a₁ a₂ : Aexp) : (gt a₁ a₂).eval st = (a₁.eval st > a₂.eval st : Bool) := rfl @[simp] theorem Bexp.eval_not (st : State) (b : Bexp) : (not b).eval st = !b.eval st := rfl @[simp] theorem Bexp.eval_and (st : State) (b₁ b₂ : Bexp) : (and b₁ b₂).eval st = (b₁.eval st && b₂.eval st) := rfl

We reuse the total-map notation (x →ₜ v etc.) for states.

example : aexp { 3 + (X * 2) }.eval (X →ₜ 5) = 13 := ⊢ Aexp.eval (X →ₜ 5) (aexp {3 + X * 2}) = 13 All goals completed! 🐙 example : aexp { Z + (X * Y) }.eval (X →ₜ 5 ; Y →ₜ 4) = 20 := ⊢ Aexp.eval (X →ₜ 5 ; Y →ₜ 4) (aexp {Z + X * Y}) = 20 All goals completed! 🐙 example : bexp { true ∧ ¬(X ≤ 4) }.eval (X →ₜ 5) = true := ⊢ Bexp.eval (X →ₜ 5) (bexp {true ∧ ¬ (X ≤ 4)}) = true All goals completed! 🐙

3.2. Commands🔗

inductive Com where | skip | asgn (x : Ident) (a : Aexp) | seq (c₁ c₂ : Com) | cond (b : Bexp) (c₁ c₂ : Com) | whileDo (b : Bexp) (c : Com)
Notation encoding: commands, macro rules/-- Imp commands -/ declare_syntax_cat imp_com /-- The command that does nothing (`skip`) -/ syntax:max ident : imp_com /-- Sequencing: one command after another (right associative. min + 1 = 11) -/ syntax:80 imp_com:11 Lean.Parser.semicolonOrLinebreak ppHardSpace imp_com:min : imp_com /-- Assignment -/ syntax:max ident ppHardSpace ":=" ppHardSpace imp_aexp : imp_com /-- Conditional -/ syntax:max "if " "(" imp_bexp ")" ppHardSpace "{" imp_com "}" ppHardSpace "else" ppHardSpace "{" imp_com "}" : imp_com /-- Loop -/ syntax:max "while " "(" imp_bexp ")" ppHardSpace "{" imp_com "}" : imp_com /-- Escape to Lean -/ syntax:max "~" term:max : imp_com /-- Include an Imp command in Lean code -/ syntax:80 "imp" ppHardSpace "{" imp_com "}" : term namespace Com open Lean Imp.Elab scoped macro_rules | `(imp { $s }) => do let stx ← match s with | `(imp_com| skip) => ``(Com.skip) | `(imp_com| $x:ident) => do elabMetavarOnlyIdent "command" (← ``(Com)) x | `(imp_com| $c₁ ; $c₂) => ``(Com.seq (imp {$c₁}) (imp {$c₂})) | `(imp_com| $x:ident := $a) => match classifyIdent? x with | some (.object, name) => let nameLit : Term := ⟨Syntax.mkStrLit name⟩ ``(Com.asgn $nameLit (aexp {$a})) | some (.metavar, _) => ``(Com.asgn $x (aexp {$a})) | none => Macro.throwErrorAt x "invalid bare identifier" | `(imp_com| if ($b) {$c₁} else {$c₂}) => ``(Com.cond (bexp {$b}) (imp {$c₁}) (imp {$c₂})) | `(imp_com| while ($b) {$c}) => ``(Com.whileDo (bexp {$b}) (imp {$c})) | `(imp_com| ~$c) => `(($c : Com)) | _ => Macro.throwUnsupported return withSourceInfoOf s stx end Com open scoped Com
Notation encoding: printing commands backnamespace Imp.Delab open Lean PrettyPrinter Delaborator SubExpr Imp.Elab /-- Recognizes a term as being an `imp { ... }` expression. -/ def getImp (stx : Term) : TSyntax `imp_com := withSourceInfoOf (canonical := false) stx <| Unhygienic.run do match stx with | `(imp { $e:imp_com }) => return e | `($id:ident) => match classifyIdent? id with | some (.metavar, _) => `(imp_com| $id:ident) | _ => `(imp_com| ~$stx) | _ => `(imp_com| ~$stx) @[app_unexpander Com.skip] def unexpandComSkip : Unexpander | _ => `(imp { $(mkIdent `skip):ident }) @[app_unexpander Com.asgn] def unexpandComAsgn : Unexpander | `($_ $x:ident $a) => `(imp { $x:ident := $(getAexp a) }) | `($_ $s:str $a) => do let id := mkObjectIdentFrom s.raw s.getString match classifyIdent? id with | some (.object, _) => `(imp { $id:ident := $(getAexp a) }) | _ => throw () | _ => throw () @[app_unexpander Com.seq] def unexpandComSeq : Unexpander | `($_ $a $b) => match a with | `(imp { $_ ; $_ }) => -- seq syntax is right associative, so need to quote `a` `(imp { ~$a ; $(getImp b):imp_com }) | _ => `(imp { $(getImp a):imp_com ; $(getImp b):imp_com }) | _ => throw () @[app_unexpander Com.cond] def unexpandComCond : Unexpander | `($_ $b $c₁ $c₂) => `(imp { if ($(getBexp b)) { $(getImp c₁) } else { $(getImp c₂) } }) | _ => throw () @[app_unexpander Com.whileDo] def unexpandComWhileDo : Unexpander | `($_ $b $c) => `(imp { while ($(getBexp b)) { $(getImp c) } }) | _ => throw () end Imp.Delab
def fact_in_lean : Com := imp { Z := X Y := 1 while (Z ≠ 0) { Y := Y * Z Z := Z - 1 } } def fact_in_lean : Com := imp {Z := X; Y := 1; while (Z ≠ 0) {Y := Y * Z; Z := Z - 1}}#print fact_in_lean
def fact_in_lean : Com :=
imp {Z := X; Y := 1; while (Z ≠ 0) {Y := Y * Z; Z := Z - 1}}

3.2.1. Desugaring Notations🔗

Even though the notations are useful for getting the high-level picture, it's sometimes helpful to turn off the notation to see the parsed structure as a plain term. This can be done with set_option pp.notation false (which we briefly mentioned in the Typeclasses chapter) as follows:

imp {X := X + 1} : Com#check imp { X := X + 1 }
imp {X := X + 1} : Com
set_option pp.notation false in Com.asgn "X" ((Aexp.id "X").plus (Aexp.num 1)) : Com#check imp { X := X + 1 }
Com.asgn "X" ((Aexp.id "X").plus (Aexp.num 1)) : Com

3.2.2. More Examples🔗

A few more examples.

Assignment:

def plus2 : Com := imp { X := X + 2 } def multXandYinZ : Com := imp { Z := X * Y }

Loops:

def subtract_slowly_body : Com := imp { Z := Z - 1; X := X - 1 } def subtract_slowly : Com := imp { while (X ≠ 0) { subtract_slowly_body } } def subtract_3_from_5_slowly : Com := imp { X := 3; Z := 5; subtract_slowly }

An infinite loop:

def loop : Com := imp { while (true) { skip } }

3.3. Evaluating Commands🔗

3.3.1. Evaluation as a Function (Failed Attempt)🔗

In a more conventional functional language like OCaml or Haskell, we could define the evaluation function as follows:

def fail to show termination for Com.eval with errors failed to infer structural recursion: Cannot use parameter st: the type TotalMap Ident Nat does not have a `.brecOn` recursor Cannot use parameter c: failed to eliminate recursive application eval st (imp {c; while (b) {c}}) failed to prove termination, possible solutions: - Use `have`-expressions to prove the remaining goals - Use `termination_by` to specify a different well-founded relation - Use `decreasing_by` to specify your own tactic for discharging this kind of goal st:Stateb:Bexpc:Comh✝:Bexp.eval st b = true⊢ 1 + sizeOf c + (1 + sizeOf b + sizeOf c) < 1 + sizeOf b + sizeOf cCom.eval (st : State) (c : Com) : State := match c with | imp {skip} => st | imp {x := a} => (x →ₜ a.eval st ; st) | imp {c₁; c₂} => let st' := eval st c₁ eval st' c₂ | imp {if (b) {c₁} else {c₂}} => if b.eval st then eval st c₁ else eval st c₂ | imp {while (b) {c}} => if b.eval st then eval st (imp { c; while (b) {c}}) -- ^-- recursive call without a decreasing argument else st
fail to show termination for
  Com.eval
with errors
failed to infer structural recursion:
Cannot use parameter st:
  the type TotalMap Ident Nat does not have a `.brecOn` recursor
Cannot use parameter c:
  failed to eliminate recursive application
    eval st (imp {c; while (b) {c}})


failed to prove termination, possible solutions:
  - Use `have`-expressions to prove the remaining goals
  - Use `termination_by` to specify a different well-founded relation
  - Use `decreasing_by` to specify your own tactic for discharging this kind of goal
st:Stateb:Bexpc:Comh✝:Bexp.eval st b = true⊢ 1 + sizeOf c + (1 + sizeOf b + sizeOf c) < 1 + sizeOf b + sizeOf c
Note to developers

Perhaps that discussion should be moved to -- or previewed in -- Logic.v? MRC'20: It's already in ProofObjects (which not everyone sees).

A nonterminating theorem loop_false (n : Nat) : False := loop_false n would make False provable, so Lean rejects it.

3.3.2. Evaluation as a Relation🔗

Here's a better way: define Com.eval as a relation rather than a function -- i.e., make its result a Prop rather than a State, similar to what we did for Aexp.EvalR in the Slang chapter.

Note to developers (Michael Hicks @mwhicks1)

I kind of hate this notation. Is there something more standard in Lean? CSLib precedent maybe?

3.3.3. Operational Semantics🔗

We'll use the notation st =[ c ]=> st' for the Com.EvalR relation: st =[ c ]=> st' means that executing program c in a starting state st results in an ending state st'. This can be pronounced "c takes state st to st'".

Here is an informal definition of evaluation, presented as inference rules for readability:

                      -----------------                  (skip)
                      st =[ skip ]=> st

                      a.eval st = n
              --------------------------------           (asgn)
              st =[ x := a ]=> (x →ₜ n ; st)

                      st  =[ c₁ ]=> st'
                      st' =[ c₂ ]=> st''
                    ---------------------                (seq)
                    st =[ c₁;c₂ ]=> st''

                     b.eval st = true
                      st =[ c₁ ]=> st'
           ---------------------------------------       (ifTrue)
           st =[ if (b) { c₁ } else { c₂ } ]=> st'

                    b.eval st = false
                      st =[ c₂ ]=> st'
           ---------------------------------------       (ifFalse)
           st =[ if (b) { c₁ } else { c₂ } ]=> st'

                    b.eval st = false
               ----------------------------              (whileFalse)
               st =[ while (b) { c } ]=> st

                     b.eval st = true
                      st =[ c ]=> st'
             st' =[ while (b) { c } ]=> st''
             -------------------------------             (whileTrue)
             st  =[ while (b) { c } ]=> st''

Here is the formal definition. Make sure you understand how it corresponds to the inference rules.

inductive Com.EvalR : Com → State → State → Prop where | skip {st : State} : EvalR (imp {skip}) st st | asgn {st : State} {a : Aexp} {n : Nat} {x : Ident} (h : a.eval st = n) : EvalR (imp {x := a}) st (x →ₜ n ; st) | seq {c₁ c₂ : Com} {st st' st'' : State} (h₁ : EvalR c₁ st st') (h₂ : EvalR c₂ st' st'') : EvalR (imp {c₁; c₂}) st st'' | ifTrue {st st' : State} {b : Bexp} {c₁ c₂ : Com} (hb : b.eval st = true) (hc : EvalR c₁ st st') : EvalR (imp {if (b) {c₁} else {c₂}}) st st' | ifFalse {st st' : State} {b : Bexp} {c₁ c₂ : Com} (hb : b.eval st = false) (hc : EvalR c₂ st st') : EvalR (imp {if (b) {c₁} else {c₂}}) st st' | whileFalse {b : Bexp} {st : State} {c : Com} (hb : b.eval st = false) : EvalR (imp {while (b) {c}}) st st | whileTrue {st st' st'' : State} {b : Bexp} {c : Com} (hb : b.eval st = true) (hc : EvalR c st st') (hloop : Com.EvalR (imp {while (b) {c}}) st' st'') : EvalR (imp {while (b) {c}}) st st''
Notation encoding: commandsclass HasEval (Com : Type) (In : outParam <| Type) (Out : outParam <| Type) where Eval : Com → In → Out → Prop namespace HasEval /-- Evaluation: `st =[ c ]=> st'` with `imp_com` command syntax -/ scoped syntax:lead term " =[ " imp_com:min " ]=> " term : term scoped macro_rules | `($st =[ $c:imp_com ]=> $st') => ``(HasEval.Eval (imp { $c }) $st $st') namespace Delab open Lean PrettyPrinter Delaborator SubExpr Imp.Delab @[delab app.HasEval.Eval] def delabTriple : Delab := whenPPOption getPPNotation do guard <| (← getExpr).isAppOfArity ``HasEval.Eval 7 let c ← withNaryArg 4 delab let st ← withNaryArg 5 delab let st' ← withNaryArg 6 delab ``($st =[ $(getImp c) ]=> $st') end Delab end HasEval open scoped HasEval instance : HasEval Com State State where Eval := Com.EvalR @[simp] theorem Com.evalR_eq {c : Com} {st st' : State} : EvalR c st st' ↔ st =[ c ]=> st' := c:Comst:Statest':State⊢ c.EvalR st st' ↔ st =[ c ]=> st' All goals completed! 🐙
∅ =[ skip ]=> ∅ : Prop#check ∅ =[ skip ]=> ∅

The cost of defining evaluation as a relation instead of a function is that we now need to construct a proof that some program evaluates to some result state, rather than letting Lean's computation mechanism do it for us.

open scoped KVPair open Com example : ∅ =[ X := 2; if (X ≤ 1) { Y := 3 } else { Z := 4 } ]=> {Z ↦ 4, X ↦ 2} := ⊢ ∅ =[ X := 2; if (X ≤ 1) {Y := 3} else {Z := 4} ]=> {Z ↦ 4, X ↦ 2} -- To supply the intermediate state to the `seq` rule, which is sometimes necessary, -- we can write `Com.EvalR.seq (st' := ...)`. ⊢ imp {X := 2}.EvalR ∅ {X ↦ 2}⊢ imp {if (X ≤ 1) {Y := 3} else {Z := 4}}.EvalR {X ↦ 2} {Z ↦ 4, X ↦ 2} ⊢ imp {X := 2}.EvalR ∅ {X ↦ 2} All goals completed! 🐙 ⊢ imp {if (X ≤ 1) {Y := 3} else {Z := 4}}.EvalR {X ↦ 2} {Z ↦ 4, X ↦ 2} ⊢ Bexp.eval {X ↦ 2} (bexp {X ≤ 1}) = false⊢ imp {Z := 4}.EvalR {X ↦ 2} {Z ↦ 4, X ↦ 2} ⊢ Bexp.eval {X ↦ 2} (bexp {X ≤ 1}) = false All goals completed! 🐙 ⊢ imp {Z := 4}.EvalR {X ↦ 2} {Z ↦ 4, X ↦ 2} All goals completed! 🐙
Note to developers (Niklas Halonen @xhalo32)

After apply EvalR.seq (st' := {X ↦ 2}), the infoview shows imp {X := 2}.EvalR ∅ {X ↦ 2} instead of ∅ =[ X := 2 ]=> {X ↦ 2}. It would be silly to use apply EvalR.seq (st' := {X ↦ 2}) <;> try simp only [evalR_eq] at *.

Since the total-map update notation (→ₜ) is difficult to type, we prefer to use the {}-notation with KVPairs.

In the above proof, using EvalR.asgn rfl is convenient because it computes the value of the right-hand side and can use it to determine st'.

Note the use of ~ here, since .num x is a Lean term that we want to splice into Imp.

example {x : Nat} : ∅ =[ X := ~(.num x) ]=> {X ↦ x} := x:Nat⊢ ∅ =[ X := ~(Aexp.num x) ]=> {X ↦ x} x:Nat⊢ Aexp.eval ∅ (Aexp.num x) = (X ↦ x).value -- `⊢ Aexp.eval ∅ x = (X ↦ x).value`, which we can prove with `simp` or `rfl` All goals completed! 🐙 example {x : Nat} : ∅ =[ X := ~(.num x) ]=> {X ↦ x} := x:Nat⊢ ∅ =[ X := ~(Aexp.num x) ]=> {X ↦ x} All goals completed! 🐙 example : ∅ =[ X := 2; Y := 3 ]=> {Y ↦ 3, X ↦ 2} := ⊢ ∅ =[ X := 2; Y := 3 ]=> {Y ↦ 3, X ↦ 2} ⊢ imp {X := 2}.EvalR ∅ ?st'⊢ imp {Y := 3}.EvalR ?st' {Y ↦ 3, X ↦ 2}⊢ State ⊢ imp {X := 2}.EvalR ∅ ?st' -- `⊢ imp {X := 2}.EvalR ∅ ?st'` All goals completed! 🐙 -- assigns the metavariable `?st'` to `{X ↦ 2}` (or equivalent) ⊢ imp {Y := 3}.EvalR ("X" →ₜ Aexp.eval ∅ (aexp {2})) {Y ↦ 3, X ↦ 2} ⊢ ("X" →ₜ 2) =[ Y := 3 ]=> {Y ↦ 3, X ↦ 2} All goals completed! 🐙

This is a case where rfl is more powerful than simp, because it can assign the ?st' metavariable. To demonstrate, here's a version with simp:

example : ∅ =[ X := 2; Y := 3 ]=> {Y ↦ 3, X ↦ 2} := ⊢ ∅ =[ X := 2; Y := 3 ]=> {Y ↦ 3, X ↦ 2} ⊢ imp {X := 2}.EvalR ∅ ?st'⊢ imp {Y := 3}.EvalR ?st' {Y ↦ 3, X ↦ 2}⊢ State unsolved goals ⊢ 2 = ?h₁.n ⊢ Nat⊢ imp {X := 2}.EvalR ∅ ?st' ⊢ Aexp.eval ∅ (aexp {2}) = ?h₁.n⊢ Nat ⊢ 2 = ?h₁.n⊢ Nat -- doesn't work because `simp` doesn't assign the metavariable ⊢ imp {Y := 3}.EvalR ("X" →ₜ sorry) {Y ↦ 3, X ↦ 2} All goals completed! 🐙

However, it's possible to use simp as long as we have assigned st' ourselves:

example : ∅ =[ X := 2; Y := 3 ]=> {Y ↦ 3, X ↦ 2} := ⊢ ∅ =[ X := 2; Y := 3 ]=> {Y ↦ 3, X ↦ 2} ⊢ imp {X := 2}.EvalR ∅ {X ↦ 2}⊢ imp {Y := 3}.EvalR {X ↦ 2} {Y ↦ 3, X ↦ 2} ⊢ imp {X := 2}.EvalR ∅ {X ↦ 2} ⊢ Aexp.eval ∅ (aexp {2}) = (X ↦ 2).value All goals completed! 🐙 ⊢ imp {Y := 3}.EvalR {X ↦ 2} {Y ↦ 3, X ↦ 2} ⊢ Aexp.eval {X ↦ 2} (aexp {3}) = (Y ↦ 3).value All goals completed! 🐙

What sorts of things might we want to prove using these definitions? Here are some simple examples...

Note to developers

PR: I phrased these quizzes with the following alternatives: (A) Not true (B) True and easily provable (C) True and takes more work to prove (D) True and cannot be proved without additional axioms

Quiz

Is the following proposition provable?

∀ (c : Com) (st st' : State),
  st =[ skip; c ]=> st' →
  st =[ c ]=> st'

(A) Yes (B) No (C) Not sure

Show solution
theorem quiz1_answer (c : Com) (st st' : State) (h : st =[ skip; c ]=> st') : st =[ c ]=> st' := c:Comst:Statest':Stateh:st =[ skip; c ]=> st'⊢ st =[ c ]=> st' inversion h with | seq smid h₁ h₂ => c:Comst:Statest':Stateh₂:c.EvalR st st'⊢ st =[ c ]=> st' All goals completed! 🐙
Quiz

Is the following proposition provable?

∀ (c₁ c₂ : Com) (st st' : State),
  st =[ c₁; c₂ ]=> st' →
  st =[ c₁ ]=> st →
  st =[ c₂ ]=> st'

(A) Yes (B) No (C) Not sure

Quiz

Is the following proposition provable?

∀ (b : Bexp) (c : Com) (st st' : State),
  st =[ if (b) { c } else { c } ]=> st' →
  st =[ c ]=> st'

(A) Yes (B) No (C) Not sure

Show solution
theorem quiz3_answer (b : Bexp) (c : Com) (st st' : State) (h : st =[ if (b) { c } else { c } ]=> st') : st =[ c ]=> st' := b:Bexpc:Comst:Statest':Stateh:st =[ if (b) {c} else {c} ]=> st'⊢ st =[ c ]=> st' inversion h with | ifTrue hb hc => All goals completed! 🐙 | ifFalse hb hc => All goals completed! 🐙
Quiz

Is the following proposition provable?

∀ (b : Bexp),
  (∀ st, b.eval st = true) →
  ∀ (c : Com) (st : State),
  ¬ ∃ st', st =[ while (b) { c } ]=> st'

(A) Yes (B) No (C) Not sure

Show solution
-- This one is tricky! theorem quiz4_answer (b : Bexp) (hbtrue : ∀ st, b.eval st = true) (c : Com) (st : State) : ¬ ∃ st', st =[ while (b) { c } ]=> st' := b:Bexphbtrue:∀ (st : State), Bexp.eval st b = truec:Comst:State⊢ ¬∃ st', st =[ while (b) {c} ]=> st' b:Bexphbtrue:∀ (st : State), Bexp.eval st b = truec:Comst:Statest':Statehev:st =[ while (b) {c} ]=> st'⊢ False b:Bexphbtrue:∀ (st : State), Bexp.eval st b = truec:Comst:Statest':Statehev:st =[ while (b) {c} ]=> st'key:∀ (cmd : Com) (s s' : State), (s =[ cmd ]=> s') → cmd = imp {while (b) {c}} → False⊢ False All goals completed! 🐙
Quiz

Is the following proposition provable?

∀ (b : Bexp) (c : Com) (st : State),
  (¬ ∃ st', st =[ while (b) { c } ]=> st') →
  ∀ st'', b.eval st'' = true

(A) Yes (B) No (C) Not sure

Show solution

This claim is false, so it cannot be proved -- the proof gets stuck immediately:

theorem quiz5_answer (b : Bexp) (c : Com) (st : State) (H : ¬ ∃ st', st =[ while (b) { c } ]=> st') : ∀ st'', b.eval st'' = true := unsolved goals b:Bexpc:Comst:StateH:¬∃ st', st =[ while (b) {c} ]=> st'st'':State⊢ Bexp.eval st'' b = trueb:Bexpc:Comst:StateH:¬∃ st', st =[ while (b) {c} ]=> st'⊢ ∀ (st'' : State), Bexp.eval st'' b = true b:Bexpc:Comst:StateH:¬∃ st', st =[ while (b) {c} ]=> st'st'':State⊢ Bexp.eval st'' b = true -- Can't make any progress -- the claim is false!

3.3.4. Determinism of Evaluation🔗

Finally, we should pause to check that our evaluation relation really is a (partial) function...

theorem ceval_deterministic {c : Com} {st st1 st2 : State} (e₁ : st =[ c ]=> st1) (e₂ : st =[ c ]=> st2) : st1 = st2 := c:Comst:Statest1:Statest2:Statee₁:st =[ c ]=> st1e₂:st =[ c ]=> st2⊢ st1 = st2 induction e₁ generalizing st2 with c:Comst:Statest1:Statest✝:Statest2:Statee₂:st✝ =[ skip ]=> st2⊢ st✝ = st2 c:Comst:Statest1:Statest✝:State⊢ st✝ = st✝ All goals completed! 🐙 c:Comst:Statest1:Statest✝:Statea✝:Aexpn✝:Natx✝:Identh✝:Aexp.eval st✝ a✝ = n✝st2:Statee₂:st✝ =[ x✝ := a✝ ]=> st2⊢ x✝ →ₜ n✝ ; st✝ = st2 inversion e₂ with | asgn h' => c:Comst:Statest1:Statest✝:Statea✝:Aexpx✝:Ident⊢ x✝ →ₜ Aexp.eval st✝ a✝ ; st✝ = x✝ →ₜ Aexp.eval st✝ a✝ ; st✝; All goals completed! 🐙 c:Comst:Statest1:Statec₁✝:Comc₂✝:Comst✝:Statest'✝:Statest''✝:Stateh₁:c₁✝.EvalR st✝ st'✝h₂:c₂✝.EvalR st'✝ st''✝ih₁:∀ {st2 : State}, (st✝ =[ c₁✝ ]=> st2) → st'✝ = st2ih₂:∀ {st2 : State}, (st'✝ =[ c₂✝ ]=> st2) → st''✝ = st2st2:Statee₂:st✝ =[ c₁✝; c₂✝ ]=> st2⊢ st''✝ = st2 inversion e₂ with | seq st2' h₁' h₂' => c:Comst:Statest1:Statec₁✝:Comc₂✝:Comst✝:Statest'✝:Statest''✝:Stateh₁:c₁✝.EvalR st✝ st'✝h₂:c₂✝.EvalR st'✝ st''✝ih₁:∀ {st2 : State}, (st✝ =[ c₁✝ ]=> st2) → st'✝ = st2ih₂:∀ {st2 : State}, (st'✝ =[ c₂✝ ]=> st2) → st''✝ = st2st2:Statest2':Stateh₂':c₂✝.EvalR st2' st2h₁':st'✝ = st2'⊢ st''✝ = st2; c:Comst:Statest1:Statec₁✝:Comc₂✝:Comst✝:Statest'✝:Statest''✝:Stateh₁:c₁✝.EvalR st✝ st'✝h₂:c₂✝.EvalR st'✝ st''✝ih₁:∀ {st2 : State}, (st✝ =[ c₁✝ ]=> st2) → st'✝ = st2ih₂:∀ {st2 : State}, (st'✝ =[ c₂✝ ]=> st2) → st''✝ = st2st2:Stateh₂':c₂✝.EvalR st'✝ st2⊢ st''✝ = st2 All goals completed! 🐙 c:Comst:Statest1:Statest✝:Statest'✝:Stateb✝:Bexpc₁✝:Comc₂✝:Comhb:Bexp.eval st✝ b✝ = truehc:c₁✝.EvalR st✝ st'✝ih:∀ {st2 : State}, (st✝ =[ c₁✝ ]=> st2) → st'✝ = st2st2:Statee₂:st✝ =[ if (b✝) {c₁✝} else {c₂✝} ]=> st2⊢ st'✝ = st2 inversion e₂ with | ifTrue hb' hc' => All goals completed! 🐙 | ifFalse hb' hc' => All goals completed! 🐙 c:Comst:Statest1:Statest✝:Statest'✝:Stateb✝:Bexpc₁✝:Comc₂✝:Comhb:Bexp.eval st✝ b✝ = falsehc:c₂✝.EvalR st✝ st'✝ih:∀ {st2 : State}, (st✝ =[ c₂✝ ]=> st2) → st'✝ = st2st2:Statee₂:st✝ =[ if (b✝) {c₁✝} else {c₂✝} ]=> st2⊢ st'✝ = st2 inversion e₂ with | ifTrue hb' hc' => All goals completed! 🐙 | ifFalse hb' hc' => All goals completed! 🐙 c:Comst:Statest1:Stateb✝:Bexpst✝:Statec✝:Comhb:Bexp.eval st✝ b✝ = falsest2:Statee₂:st✝ =[ while (b✝) {c✝} ]=> st2⊢ st✝ = st2 inversion e₂ with | whileFalse hb' => All goals completed! 🐙 | whileTrue hb' hc' hl' => All goals completed! 🐙 c:Comst:Statest1:Statest✝:Statest'✝:Statest''✝:Stateb✝:Bexpc✝:Comhb:Bexp.eval st✝ b✝ = truehc:c✝.EvalR st✝ st'✝hloop:imp {while (b✝) {c✝}}.EvalR st'✝ st''✝ih₁:∀ {st2 : State}, (st✝ =[ c✝ ]=> st2) → st'✝ = st2ih₂:∀ {st2 : State}, (st'✝ =[ while (b✝) {c✝} ]=> st2) → st''✝ = st2st2:Statee₂:st✝ =[ while (b✝) {c✝} ]=> st2⊢ st''✝ = st2 inversion e₂ with | whileFalse hb' => All goals completed! 🐙 | whileTrue st2' _ hc' hl' => c:Comst:Statest1:Statest✝:Statest'✝:Statest''✝:Stateb✝:Bexpc✝:Comhb:Bexp.eval st✝ b✝ = truehc:c✝.EvalR st✝ st'✝hloop:imp {while (b✝) {c✝}}.EvalR st'✝ st''✝ih₁:∀ {st2 : State}, (st✝ =[ c✝ ]=> st2) → st'✝ = st2ih₂:∀ {st2 : State}, (st'✝ =[ while (b✝) {c✝} ]=> st2) → st''✝ = st2st2:Statest2':Statehb✝:Bexp.eval st✝ b✝ = truehl':imp {while (b✝) {c✝}}.EvalR st2' st2hc':st'✝ = st2'⊢ st''✝ = st2; c:Comst:Statest1:Statest✝:Statest'✝:Statest''✝:Stateb✝:Bexpc✝:Comhb:Bexp.eval st✝ b✝ = truehc:c✝.EvalR st✝ st'✝hloop:imp {while (b✝) {c✝}}.EvalR st'✝ st''✝ih₁:∀ {st2 : State}, (st✝ =[ c✝ ]=> st2) → st'✝ = st2ih₂:∀ {st2 : State}, (st'✝ =[ while (b✝) {c✝} ]=> st2) → st''✝ = st2st2:Statehb✝:Bexp.eval st✝ b✝ = truehl':imp {while (b✝) {c✝}}.EvalR st'✝ st2⊢ st''✝ = st2 All goals completed! 🐙

3.4. Reasoning About Imp Programs🔗

theorem plus2_spec {st : State} {n : Nat} {st' : State} (hx : st[X] = n) (heval : st =[ plus2 ]=> st') : st'[X] = n + 2 := st:Staten:Natst':Statehx:st[X] = nheval:st =[ plus2 ]=> st'⊢ st'[X] = n + 2 -- Inverting `heval` forces one step of the evaluation relation: since -- `plus2` is an assignment, `st'` must be `st` extended at `X`. st:Staten:Natst':Statehx:st[X] = nheval:st =[ X := X + 2 ]=> st'⊢ st'[X] = n + 2 inversion heval with | asgn m h => st:Staten:Nathx:st[X] = nm:Nath:n + 2 = m⊢ m = n + 2 All goals completed! 🐙
Note to developers (Niklas Halonen @xhalo32)

We need to explain the generalize tactic. I've changed some Hoare proofs from have key to generalize but the tactic hasn't been explained yet.

Note to developers (One An @meluge)

At least currently, it looks like generalize is introduced in Automation.lean. Are we doing anything different here with generalize that is unexplained there?

3.5. Case Study (Optional)🔗

Recall the factorial program (broken up into smaller pieces this time, for convenience of proving things about it).

def factBody : Com := imp { Y := Y * Z; Z := Z - 1 } def factLoop : Com := imp { while (Z ≠ 0) { factBody } } def factCom : Com := imp { Z := X; Y := 1; factLoop }

Here is an alternative "mathematical" definition of the factorial function:

def realFact (n : Nat) : Nat := match n with | 0 => 1 | n' + 1 => (n' + 1) * realFact n'

We would like to show that they agree -- if we start factCom in a state where variable X contains some number n, then it will terminate in a state where variable Y contains the factorial of n.

To show this, we rely on the critical idea of a loop invariant.

def FactInvariant (n : Nat) (st : State) : Prop := st[Y] * realFact st[Z] = realFact n

We show that the body of the factorial loop preserves the invariant:

theorem factBody_preserves_invariant {st st' : State} {n : Nat} (hinv : FactInvariant n st) (hz : st[Z] ≠ 0) (heval : st =[ factBody ]=> st') : FactInvariant n st' := st:Statest':Staten:Nathinv:FactInvariant n sthz:st[Z] ≠ 0heval:st =[ factBody ]=> st'⊢ FactInvariant n st' st:Statest':Staten:Nathinv:st[Y] * realFact st[Z] = realFact nhz:st[Z] ≠ 0heval:st =[ factBody ]=> st'⊢ st'[Y] * realFact st'[Z] = realFact n st:Statest':Staten:Nathinv:st[Y] * realFact st[Z] = realFact nhz:st[Z] ≠ 0heval:st =[ Y := Y * Z; Z := Z - 1 ]=> st'⊢ st'[Y] * realFact st'[Z] = realFact n inversion heval with | seq _ h₁ h₂ => inversion h₁ with | asgn hy => inversion h₂ with | asgn hz' => st:Staten:Nathinv:st[Y] * realFact st[Z] = realFact nhz:st[Z] ≠ 0⊢ ("Z" →ₜ Aexp.eval ("Y" →ₜ Aexp.eval st (aexp {Y * Z}) ; st) (aexp {Z - 1}) ; "Y" →ₜ Aexp.eval st (aexp {Y * Z}) ; st)[Y] * realFact ("Z" →ₜ Aexp.eval ("Y" →ₜ Aexp.eval st (aexp {Y * Z}) ; st) (aexp {Z - 1}) ; "Y" →ₜ Aexp.eval st (aexp {Y * Z}) ; st)[Z] = realFact n st:Staten:Nathinv:st[Y] * realFact st[Z] = realFact nhz:st[Z] ≠ 0hyz:Y ≠ Z⊢ ("Z" →ₜ Aexp.eval ("Y" →ₜ Aexp.eval st (aexp {Y * Z}) ; st) (aexp {Z - 1}) ; "Y" →ₜ Aexp.eval st (aexp {Y * Z}) ; st)[Y] * realFact ("Z" →ₜ Aexp.eval ("Y" →ₜ Aexp.eval st (aexp {Y * Z}) ; st) (aexp {Z - 1}) ; "Y" →ₜ Aexp.eval st (aexp {Y * Z}) ; st)[Z] = realFact n st:Staten:Nathinv:st[Y] * realFact st[Z] = realFact nhz:st[Z] ≠ 0hyz:Y ≠ Zhzy:Z ≠ Y⊢ ("Z" →ₜ Aexp.eval ("Y" →ₜ Aexp.eval st (aexp {Y * Z}) ; st) (aexp {Z - 1}) ; "Y" →ₜ Aexp.eval st (aexp {Y * Z}) ; st)[Y] * realFact ("Z" →ₜ Aexp.eval ("Y" →ₜ Aexp.eval st (aexp {Y * Z}) ; st) (aexp {Z - 1}) ; "Y" →ₜ Aexp.eval st (aexp {Y * Z}) ; st)[Z] = realFact n st:Staten:Nathinv:st[Y] * realFact st[Z] = realFact nhz:st[Z] ≠ 0hyz:Y ≠ Zhzy:Z ≠ Y⊢ st["Y"] * st["Z"] * realFact (st["Z"] - 1) = realFact n -- Show that `st[Z] = z + 1` for some `z` cases hzz : st[Z] with st:Staten:Nathinv:st[Y] * realFact st[Z] = realFact nhz:st[Z] ≠ 0hyz:Y ≠ Zhzy:Z ≠ Yhzz:st[Z] = 0⊢ st["Y"] * 0 * realFact (0 - 1) = realFact n All goals completed! 🐙 st:Staten:Nathinv:st[Y] * realFact st[Z] = realFact nhz:st[Z] ≠ 0hyz:Y ≠ Zhzy:Z ≠ Yz:Nathzz:st[Z] = z + 1⊢ st["Y"] * (z + 1) * realFact (z + 1 - 1) = realFact n st:Staten:Nathz:st[Z] ≠ 0hyz:Y ≠ Zhzy:Z ≠ Yz:Nathinv:st[Y] * ((z + 1) * realFact z) = realFact nhzz:st[Z] = z + 1⊢ st["Y"] * (z + 1) * realFact (z + 1 - 1) = realFact n st:Staten:Nathz:st[Z] ≠ 0hyz:Y ≠ Zhzy:Z ≠ Yz:Nathinv:st[Y] * ((z + 1) * realFact z) = realFact nhzz:st[Z] = z + 1⊢ st["Y"] * ((z + 1) * realFact z) = realFact n All goals completed! 🐙

From this, we can show that the whole loop also preserves the invariant:

theorem factLoop_preserves_invariant {st st' : State} {n : Nat} (hinv : FactInvariant n st) (heval : st =[ factLoop ]=> st') : FactInvariant n st' := st:Statest':Staten:Nathinv:FactInvariant n stheval:st =[ factLoop ]=> st'⊢ FactInvariant n st' st:Statest':Staten:Nathinv:FactInvariant n stc:Comheq:factLoop = cheval:st =[ c ]=> st'⊢ FactInvariant n st' induction heval with st:Statest':Staten:Natc:Comb✝:Bexpst✝:Statec✝:Comhb:Bexp.eval st✝ b✝ = falsehinv:FactInvariant n st✝heq:factLoop = imp {while (b✝) {c✝}}⊢ FactInvariant n st✝ -- trivial when the loop doesn't run... All goals completed! 🐙 st✝:Statest'✝:Staten:Natc✝:Comst:Statest':Statest'':Stateb:Bexpc:Comhb:Bexp.eval st b = truehc:c.EvalR st st'hloop:imp {while (b) {c}}.EvalR st' st''ih₁:FactInvariant n st → factLoop = c → FactInvariant n st'ih₂:FactInvariant n st' → factLoop = imp {while (b) {c}} → FactInvariant n st''hinv:FactInvariant n stheq:factLoop = imp {while (b) {c}}⊢ FactInvariant n st'' -- if the loop does run, we know that `factBody` preserves -- `FactInvariant` -- we just need to assemble the pieces st✝:Statest'✝:Staten:Natc✝:Comst:Statest':Statest'':Stateb:Bexpc:Comhb:Bexp.eval st b = truehc:c.EvalR st st'hloop:imp {while (b) {c}}.EvalR st' st''ih₁:FactInvariant n st → factLoop = c → FactInvariant n st'ih₂:FactInvariant n st' → factLoop = imp {while (b) {c}} → FactInvariant n st''hinv:FactInvariant n stheq:imp {while (Z ≠ 0) {factBody}} = imp {while (b) {c}}⊢ FactInvariant n st'' st✝:Statest'✝:Staten:Natc✝:Comst:Statest':Statest'':Stateb:Bexpc:Comhb:Bexp.eval st b = truehc:c.EvalR st st'hloop:imp {while (b) {c}}.EvalR st' st''ih₁:FactInvariant n st → factLoop = c → FactInvariant n st'ih₂:FactInvariant n st' → factLoop = imp {while (b) {c}} → FactInvariant n st''hinv:FactInvariant n sthb':bexp {Z ≠ 0} = bhc':factBody = c⊢ FactInvariant n st'' st✝:Statest'✝:Staten:Natc:Comst:Statest':Statest'':Statehinv:FactInvariant n sthb:Bexp.eval st (bexp {Z ≠ 0}) = truehc:factBody.EvalR st st'ih₁:FactInvariant n st → factLoop = factBody → FactInvariant n st'hloop:imp {while (Z ≠ 0) {factBody}}.EvalR st' st''ih₂:FactInvariant n st' → factLoop = imp {while (Z ≠ 0) {factBody}} → FactInvariant n st''⊢ FactInvariant n st'' st✝:Statest'✝:Staten:Natc:Comst:Statest':Statest'':Statehinv:FactInvariant n sthb:Bexp.eval st (bexp {Z ≠ 0}) = truehc:factBody.EvalR st st'ih₁:FactInvariant n st → factLoop = factBody → FactInvariant n st'hloop:imp {while (Z ≠ 0) {factBody}}.EvalR st' st''ih₂:FactInvariant n st' → factLoop = imp {while (Z ≠ 0) {factBody}} → FactInvariant n st''hz:st[Z] ≠ 0⊢ FactInvariant n st'' All goals completed! 🐙 st:Statest':Staten:Natc:Comst✝:Statehinv:FactInvariant n st✝heq:factLoop = imp {skip}⊢ FactInvariant n st✝ st:Statest':Staten:Natc:Comst✝:Statea✝:Aexpn✝:Natx✝:Identh✝:Aexp.eval st✝ a✝ = n✝hinv:FactInvariant n st✝heq:factLoop = imp {x✝ := a✝}⊢ FactInvariant n (x✝ →ₜ n✝ ; st✝) st:Statest':Staten:Natc:Comc₁✝:Comc₂✝:Comst✝:Statest'✝:Statest''✝:Stateh₁✝:c₁✝.EvalR st✝ st'✝h₂✝:c₂✝.EvalR st'✝ st''✝h₁_ih✝:FactInvariant n st✝ → factLoop = c₁✝ → FactInvariant n st'✝h₂_ih✝:FactInvariant n st'✝ → factLoop = c₂✝ → FactInvariant n st''✝hinv:FactInvariant n st✝heq:factLoop = imp {c₁✝; c₂✝}⊢ FactInvariant n st''✝ st:Statest':Staten:Natc:Comst✝:Statest'✝:Stateb✝:Bexpc₁✝:Comc₂✝:Comhb✝:Bexp.eval st✝ b✝ = truehc✝:c₁✝.EvalR st✝ st'✝hc_ih✝:FactInvariant n st✝ → factLoop = c₁✝ → FactInvariant n st'✝hinv:FactInvariant n st✝heq:factLoop = imp {if (b✝) {c₁✝} else {c₂✝}}⊢ FactInvariant n st'✝ st:Statest':Staten:Natc:Comst✝:Statest'✝:Stateb✝:Bexpc₁✝:Comc₂✝:Comhb✝:Bexp.eval st✝ b✝ = falsehc✝:c₂✝.EvalR st✝ st'✝hc_ih✝:FactInvariant n st✝ → factLoop = c₂✝ → FactInvariant n st'✝hinv:FactInvariant n st✝heq:factLoop = imp {if (b✝) {c₁✝} else {c₂✝}}⊢ FactInvariant n st'✝ st:Statest':Staten:Natc:Comst✝:Statest'✝:Stateb✝:Bexpc₁✝:Comc₂✝:Comhb✝:Bexp.eval st✝ b✝ = falsehc✝:c₂✝.EvalR st✝ st'✝hc_ih✝:FactInvariant n st✝ → factLoop = c₂✝ → FactInvariant n st'✝hinv:FactInvariant n st✝heq:factLoop = imp {if (b✝) {c₁✝} else {c₂✝}}⊢ FactInvariant n st'✝st:Statest':Staten:Natc:Comst✝:Statest'✝:Stateb✝:Bexpc₁✝:Comc₂✝:Comhb✝:Bexp.eval st✝ b✝ = truehc✝:c₁✝.EvalR st✝ st'✝hc_ih✝:FactInvariant n st✝ → factLoop = c₁✝ → FactInvariant n st'✝hinv:FactInvariant n st✝heq:factLoop = imp {if (b✝) {c₁✝} else {c₂✝}}⊢ FactInvariant n st'✝st:Statest':Staten:Natc:Comc₁✝:Comc₂✝:Comst✝:Statest'✝:Statest''✝:Stateh₁✝:c₁✝.EvalR st✝ st'✝h₂✝:c₂✝.EvalR st'✝ st''✝h₁_ih✝:FactInvariant n st✝ → factLoop = c₁✝ → FactInvariant n st'✝h₂_ih✝:FactInvariant n st'✝ → factLoop = c₂✝ → FactInvariant n st''✝hinv:FactInvariant n st✝heq:factLoop = imp {c₁✝; c₂✝}⊢ FactInvariant n st''✝st:Statest':Staten:Natc:Comst✝:Statea✝:Aexpn✝:Natx✝:Identh✝:Aexp.eval st✝ a✝ = n✝hinv:FactInvariant n st✝heq:factLoop = imp {x✝ := a✝}⊢ FactInvariant n (x✝ →ₜ n✝ ; st✝)st:Statest':Staten:Natc:Comst✝:Statehinv:FactInvariant n st✝heq:factLoop = imp {skip}⊢ FactInvariant n st✝ All goals completed! 🐙

Next, we show that, for any loop, if the loop terminates, then the condition guarding the loop must be false at the end:

theorem guard_false_after_loop {b : Bexp} {c : Com} {st st' : State} (heval : st =[ while (b) {c} ]=> st') : b.eval st' = false := b:Bexpc:Comst:Statest':Stateheval:st =[ while (b) {c} ]=> st'⊢ Bexp.eval st' b = false b:Bexpc:Comst:Statest':Statecmd:Comheq:imp {while (b) {c}} = cmdheval:st =[ cmd ]=> st'⊢ Bexp.eval st' b = false induction heval with b:Bexpc:Comst:Statest':Statecmd:Comb✝:Bexpst✝:Statec✝:Comhb:Bexp.eval st✝ b✝ = falseheq:imp {while (b) {c}} = imp {while (b✝) {c✝}}⊢ Bexp.eval st✝ b = false b:Bexpc:Comst:Statest':Statecmd:Comb✝:Bexpst✝:Statec✝:Comhb:Bexp.eval st✝ b✝ = falsehb':b = b✝c_eq✝:c = c✝⊢ Bexp.eval st✝ b = false b:Bexpc:Comst:Statest':Statecmd:Comst✝:Statec✝:Comc_eq✝:c = c✝hb:Bexp.eval st✝ b = false⊢ Bexp.eval st✝ b = false All goals completed! 🐙 b:Bexpc:Comst:Statest':Statecmd:Comst✝:Statest'✝:Statest''✝:Stateb✝:Bexpc✝:Comhb✝:Bexp.eval st✝ b✝ = truehc✝:c✝.EvalR st✝ st'✝hloop✝:imp {while (b✝) {c✝}}.EvalR st'✝ st''✝hc_ih✝:imp {while (b) {c}} = c✝ → Bexp.eval st'✝ b = falseih₂:imp {while (b) {c}} = imp {while (b✝) {c✝}} → Bexp.eval st''✝ b = falseheq:imp {while (b) {c}} = imp {while (b✝) {c✝}}⊢ Bexp.eval st''✝ b = false All goals completed! 🐙 b:Bexpc:Comst:Statest':Statecmd:Comst✝:Stateheq:imp {while (b) {c}} = imp {skip}⊢ Bexp.eval st✝ b = false b:Bexpc:Comst:Statest':Statecmd:Comst✝:Statea✝:Aexpn✝:Natx✝:Identh✝:Aexp.eval st✝ a✝ = n✝heq:imp {while (b) {c}} = imp {x✝ := a✝}⊢ Bexp.eval (x✝ →ₜ n✝ ; st✝) b = false b:Bexpc:Comst:Statest':Statecmd:Comc₁✝:Comc₂✝:Comst✝:Statest'✝:Statest''✝:Stateh₁✝:c₁✝.EvalR st✝ st'✝h₂✝:c₂✝.EvalR st'✝ st''✝h₁_ih✝:imp {while (b) {c}} = c₁✝ → Bexp.eval st'✝ b = falseh₂_ih✝:imp {while (b) {c}} = c₂✝ → Bexp.eval st''✝ b = falseheq:imp {while (b) {c}} = imp {c₁✝; c₂✝}⊢ Bexp.eval st''✝ b = false b:Bexpc:Comst:Statest':Statecmd:Comst✝:Statest'✝:Stateb✝:Bexpc₁✝:Comc₂✝:Comhb✝:Bexp.eval st✝ b✝ = truehc✝:c₁✝.EvalR st✝ st'✝hc_ih✝:imp {while (b) {c}} = c₁✝ → Bexp.eval st'✝ b = falseheq:imp {while (b) {c}} = imp {if (b✝) {c₁✝} else {c₂✝}}⊢ Bexp.eval st'✝ b = false b:Bexpc:Comst:Statest':Statecmd:Comst✝:Statest'✝:Stateb✝:Bexpc₁✝:Comc₂✝:Comhb✝:Bexp.eval st✝ b✝ = falsehc✝:c₂✝.EvalR st✝ st'✝hc_ih✝:imp {while (b) {c}} = c₂✝ → Bexp.eval st'✝ b = falseheq:imp {while (b) {c}} = imp {if (b✝) {c₁✝} else {c₂✝}}⊢ Bexp.eval st'✝ b = false b:Bexpc:Comst:Statest':Statecmd:Comst✝:Statest'✝:Stateb✝:Bexpc₁✝:Comc₂✝:Comhb✝:Bexp.eval st✝ b✝ = falsehc✝:c₂✝.EvalR st✝ st'✝hc_ih✝:imp {while (b) {c}} = c₂✝ → Bexp.eval st'✝ b = falseheq:imp {while (b) {c}} = imp {if (b✝) {c₁✝} else {c₂✝}}⊢ Bexp.eval st'✝ b = falseb:Bexpc:Comst:Statest':Statecmd:Comst✝:Statest'✝:Stateb✝:Bexpc₁✝:Comc₂✝:Comhb✝:Bexp.eval st✝ b✝ = truehc✝:c₁✝.EvalR st✝ st'✝hc_ih✝:imp {while (b) {c}} = c₁✝ → Bexp.eval st'✝ b = falseheq:imp {while (b) {c}} = imp {if (b✝) {c₁✝} else {c₂✝}}⊢ Bexp.eval st'✝ b = falseb:Bexpc:Comst:Statest':Statecmd:Comc₁✝:Comc₂✝:Comst✝:Statest'✝:Statest''✝:Stateh₁✝:c₁✝.EvalR st✝ st'✝h₂✝:c₂✝.EvalR st'✝ st''✝h₁_ih✝:imp {while (b) {c}} = c₁✝ → Bexp.eval st'✝ b = falseh₂_ih✝:imp {while (b) {c}} = c₂✝ → Bexp.eval st''✝ b = falseheq:imp {while (b) {c}} = imp {c₁✝; c₂✝}⊢ Bexp.eval st''✝ b = falseb:Bexpc:Comst:Statest':Statecmd:Comst✝:Statea✝:Aexpn✝:Natx✝:Identh✝:Aexp.eval st✝ a✝ = n✝heq:imp {while (b) {c}} = imp {x✝ := a✝}⊢ Bexp.eval (x✝ →ₜ n✝ ; st✝) b = falseb:Bexpc:Comst:Statest':Statecmd:Comst✝:Stateheq:imp {while (b) {c}} = imp {skip}⊢ Bexp.eval st✝ b = false All goals completed! 🐙

Finally, we can patch it all together...

theorem factCom_correct {st st' : State} {n : Nat} (hx : st[X] = n) (heval : st =[ factCom ]=> st') : st'[Y] = realFact n := st:Statest':Staten:Nathx:st[X] = nheval:st =[ factCom ]=> st'⊢ st'[Y] = realFact n st:Statest':Staten:Nathx:st[X] = nheval:st =[ Z := X; Y := 1; factLoop ]=> st'⊢ st'[Y] = realFact n inversion heval with | seq _ h₁ h₂ => inversion h₁ with | asgn hz => inversion h₂ with | seq _ h₃ h₄ => inversion h₃ with | asgn hy => st:Statest':Staten:Nathx:st[X] = nh₄:factLoop.EvalR ("Y" →ₜ Aexp.eval ("Z" →ₜ Aexp.eval st (aexp {X}) ; st) (aexp {1}) ; "Z" →ₜ Aexp.eval st (aexp {X}) ; st) st'⊢ st'[Y] = realFact n -- The invariant is true before the loop runs... st:Statest':Staten:Nathx:st[X] = nh₄:factLoop.EvalR ("Y" →ₜ Aexp.eval ("Z" →ₜ Aexp.eval st (aexp {X}) ; st) (aexp {1}) ; "Z" →ₜ Aexp.eval st (aexp {X}) ; st) st'hinv:FactInvariant n (Y →ₜ 1 ; Z →ₜ st[X] ; st)⊢ st'[Y] = realFact n -- ...so when the loop is done running, the invariant -- is maintained st:Statest':Staten:Nathx:st[X] = nh₄:factLoop.EvalR ("Y" →ₜ Aexp.eval ("Z" →ₜ Aexp.eval st (aexp {X}) ; st) (aexp {1}) ; "Z" →ₜ Aexp.eval st (aexp {X}) ; st) st'hinv:FactInvariant n (Y →ₜ 1 ; Z →ₜ st[X] ; st)hinv':FactInvariant n st'⊢ st'[Y] = realFact n -- Finally, if the loop terminated, then `Z` is `0`; so `Y` must be -- factorial of `X` st:Statest':Staten:Nathx:st[X] = nh₄:imp {while (Z ≠ 0) {factBody}}.EvalR ("Y" →ₜ Aexp.eval ("Z" →ₜ Aexp.eval st (aexp {X}) ; st) (aexp {1}) ; "Z" →ₜ Aexp.eval st (aexp {X}) ; st) st'hinv:FactInvariant n (Y →ₜ 1 ; Z →ₜ st[X] ; st)hinv':FactInvariant n st'⊢ st'[Y] = realFact n st:Statest':Staten:Nathx:st[X] = nh₄:imp {while (Z ≠ 0) {factBody}}.EvalR ("Y" →ₜ Aexp.eval ("Z" →ₜ Aexp.eval st (aexp {X}) ; st) (aexp {1}) ; "Z" →ₜ Aexp.eval st (aexp {X}) ; st) st'hinv:FactInvariant n (Y →ₜ 1 ; Z →ₜ st[X] ; st)hinv':FactInvariant n st'hz:Bexp.eval st' (bexp {Z ≠ 0}) = false⊢ st'[Y] = realFact n st:Statest':Staten:Nathx:st[X] = nh₄:imp {while (Z ≠ 0) {factBody}}.EvalR ("Y" →ₜ Aexp.eval ("Z" →ₜ Aexp.eval st (aexp {X}) ; st) (aexp {1}) ; "Z" →ₜ Aexp.eval st (aexp {X}) ; st) st'hinv:FactInvariant n (Y →ₜ 1 ; Z →ₜ st[X] ; st)hinv':FactInvariant n st'hz:st'["Z"] = 0⊢ st'[Y] = realFact n st:Statest':Staten:Nathx:st[X] = nh₄:imp {while (Z ≠ 0) {factBody}}.EvalR ("Y" →ₜ Aexp.eval ("Z" →ₜ Aexp.eval st (aexp {X}) ; st) (aexp {1}) ; "Z" →ₜ Aexp.eval st (aexp {X}) ; st) st'hinv:FactInvariant n (Y →ₜ 1 ; Z →ₜ st[X] ; st)hinv':st'[Y] = realFact nhz:st'["Z"] = 0⊢ st'[Y] = realFact n All goals completed! 🐙

One might wonder whether all this work with poking at states and unfolding definitions could be ameliorated with some more powerful lemmas and/or more uniform reasoning principles... Indeed, this is exactly the point of the Hoare chapters!

3.6. Additional Exercises🔗

Exercise★★★(stack_compiler)

Old HP calculators, programming languages like Forth and Postscript, and abstract machines like the Java Virtual Machine all evaluate arithmetic expressions using a stack. For instance, the expression

(2*3)+(3*(4-2))

would be written as

      2 3 * 3 4 2 - * +

and evaluated like this (where we show the program being evaluated on the right and the contents of the stack on the left):

      [ ]           |    2 3 * 3 4 2 - * +
      [2]           |    3 * 3 4 2 - * +
      [3, 2]        |    * 3 4 2 - * +
      [6]           |    3 4 2 - * +
      [3, 6]        |    4 2 - * +
      [4, 3, 6]     |    2 - * +
      [2, 4, 3, 6]  |    - * +
      [2, 3, 6]     |    * +
      [6, 6]        |    +
      [12]          |

The goal of this exercise is to write a small compiler that translates Aexps into stack machine instructions.

The instruction set for our stack language will consist of the following instructions:

  • sPush n: Push the number n on the stack.

  • sLoad x: Load the identifier x from the store and push it on the stack.

  • sPlus: Pop the two top numbers from the stack, add them, and push the result onto the stack.

  • sMinus: Similar, but subtract the first number from the second.

  • sMult: Similar, but multiply.

namespace StackCompiler inductive Sinstr : Type where | sPush (n : Nat) | sLoad (x : String) | sPlus | sMinus | sMult open Sinstr

Write a function to evaluate programs in the stack language. It should take as input a state, a stack represented as a list of numbers (top stack item is the head of the list), and a program represented as a list of instructions, and it should return the stack after executing the program. Test your function on the examples below.

Note that it is unspecified what to do when encountering an sPlus, sMinus, or sMult instruction if the stack contains fewer than two elements. In a sense, it is immaterial what we do, since a correct compiler will never emit such a malformed program. But for the sake of later exercises, it would be best to skip the offending instruction and continue with the next one.

def declaration uses `sorry`sExecute (st : State) (stack : List Nat) (prog : List Sinstr) : List Nat := sorry -- FILL IN HERE theorem declaration uses `sorry`sExecute1 : sExecute ∅ [] [sPush 5, sPush 3, sPush 1, sMinus] = [2, 5] := ⊢ sExecute ∅ [] [sPush 5, sPush 3, sPush 1, sMinus] = [2, 5] All goals completed! 🐙 theorem declaration uses `sorry`sExecute2 : sExecute {X ↦ 3} [3, 4] [sPush 4, sLoad X, sMult, sPlus] = [15, 4] := ⊢ sExecute {X ↦ 3} [3, 4] [sPush 4, sLoad X, sMult, sPlus] = [15, 4] All goals completed! 🐙

Next, write a function that compiles an Aexp into a stack machine program. The effect of running the program should be the same as pushing the value of the expression on the stack.

def declaration uses `sorry`sCompile (a : Aexp) : List Sinstr := sorry -- FILL IN HERE

After you've defined sCompile, prove the following to test that it works.

theorem declaration uses `sorry`sCompile1 : sCompile (aexp { X - (2 * Y) }) = [sLoad X, sPush 2, sLoad Y, sMult, sMinus] := ⊢ sCompile (aexp {X - 2 * Y}) = [sLoad X, sPush 2, sLoad Y, sMult, sMinus] All goals completed! 🐙
Exercise★★★(execute_app)

Execution can be decomposed in the following sense: executing stack program p₁ ++ p₂ is the same as executing p₁, taking the resulting stack, and executing p₂ from that stack. Prove that fact.

theorem declaration uses `sorry`execute_app (st : State) (p₁ p₂ : List Sinstr) (stack : List Nat) : sExecute st stack (p₁ ++ p₂) = sExecute st (sExecute st stack p₁) p₂ := st:Statep₁:List Sinstrp₂:List Sinstrstack:List Nat⊢ sExecute st stack (p₁ ++ p₂) = sExecute st (sExecute st stack p₁) p₂ All goals completed! 🐙
Exercise★★★(compiler_correct)

Now we'll prove the correctness of the compiler implemented in the previous exercise. Begin by proving the following lemma. If it becomes difficult, consider whether your implementation of sExecute or sCompile could be simplified.

theorem declaration uses `sorry`sCompile_correct_aux (st : State) (a : Aexp) (stack : List Nat) : sExecute st stack (sCompile a) = Aexp.eval st a :: stack := st:Statea:Aexpstack:List Nat⊢ sExecute st stack (sCompile a) = Aexp.eval st a :: stack All goals completed! 🐙

The main theorem should be a very easy corollary of that lemma.

theorem declaration uses `sorry`sCompile_correct (st : State) (a : Aexp) : sExecute st [] (sCompile a) = [Aexp.eval st a] := st:Statea:Aexp⊢ sExecute st [] (sCompile a) = [Aexp.eval st a] All goals completed! 🐙 end StackCompiler
Source revision: e85fe77, committed 2026-10-06 21:16 UTC