The Lean Language Reference

18.1. Verifying imperative programs using mvcgen

This section is a tutorial introducing the most important concepts of mvcgen top-down. Recall that you need to import Std.Tactic.Do and open Std.Do to run these examples.

Array Sum

As a “hello world” to mvcgen, the following example mySum computes the sum of an array using local mutability and a for loop. Then it proves mySum correct relative to Array.sum, requiring us to specify an invariant for the loop:

def mySum (l : Array Nat) : Nat := Id.run do let mut out := 0 for i in l do out := out + i return out theorem mySum_correct (l : Array Nat) : mySum l = l.sum := l:Array NatmySum l = l.sum -- Focus on the part of the program with the `do` block (`Id.run ...`) l:Array Natx:Nath:mySum l = xx = l.sum l:Array Natx:Nath:mySum l = x⊢ₛ wp⟦have out := 0; do let r forIn l out fun i r => have out := r; have out := out + i; do pure PUnit.unit pure (ForInStep.yield out) have out : Nat := r pure out (PostCond.noThrow fun a => { down := a = l.sum }) -- Break down into verification conditions l:Array Natx:Nath:mySum l = xInvariant l.toList Nat PostShape.purel:Array Natx:Nath:mySum l = xpref✝:List Natcur✝:Natsuff✝:List Nath✝¹:l.toList = pref✝ ++ cur✝ :: suff✝b✝:Nath✝:(Prod.fst ?inv1 ({ «prefix» := pref✝, suffix := cur✝ :: suff✝, property := }, b✝)).down(Prod.fst ?inv1 ({ «prefix» := pref✝ ++ [cur✝], suffix := suff✝, property := }, b✝ + cur✝)).downl:Array Natx:Nath:mySum l = x(Prod.fst ?inv1 ({ «prefix» := [], suffix := l.toList, property := }, 0)).downl:Array Natx:Nath:mySum l = xr✝:Nath✝:(Prod.fst ?inv1 ({ «prefix» := l.toList, suffix := [], property := }, r✝)).downr✝ = l.sum -- Specify the invariant which should hold throughout the loop -- * `out` refers to the current value of the `let mut` variable -- * `xs` is a `List.Cursor`, which is a data structure representing -- a list that is split into `xs.prefix` and `xs.suffix`. -- It tracks how far into the loop we have gotten. -- Our invariant is that `out` holds the sum of the prefix. -- The notation ⌜p⌝ embeds a `p : Prop` into the assertion language. case inv1 l:Array Natx:Nath:mySum l = xInvariant l.toList Nat PostShape.pure All goals completed! 🐙 -- After specifying the invariant, we can further simplify our goals -- by "leaving the proof mode". `mleave` is just -- `simp only [...] at *` with a stable simp subset. all_goals l:Array Natx:Nath:mySum l = xr✝:Nath✝:l.toList.sum = r✝r✝ = l.sum -- Prove that our invariant is preserved at each step of the loop case vc1 ih l:Array Natx:Nath:mySum l = xpref✝:List Natcur✝:Natsuff✝:List Nath✝:l.toList = pref✝ ++ cur✝ :: suff✝b✝:Natih:pref✝.sum = b✝(pref✝ ++ [cur✝]).sum = b✝ + cur✝ -- The goal here mentions `pref`, which binds the `prefix` field of -- the cursor passed to the invariant. Unpacking the -- (dependently-typed) cursor makes it easier for `grind`. All goals completed! 🐙 -- Prove that the invariant is true at the start case vc2 l:Array Natx:Nath:mySum l = x[].sum = 0 All goals completed! 🐙 -- Prove that the invariant at the end of the loop implies the -- property we wanted case vc3 h l:Array Natx:Nath✝:mySum l = xr✝:Nath:l.toList.sum = r✝r✝ = l.sum All goals completed! 🐙

Note that the case labels are actually unique prefixes of the full case labels. Whenever referring to cases, only this prefix should be used; the suffix is merely a poor man's hint to the user where that particular VC came from. For example,

  • vc1.step conveys that this VC proves the inductive step for the loop

  • vc2.a.pre is meant to prove that the hypotheses of a goal implies the precondition of a specification (of forIn).

  • vc3.a.post is meant to prove that the postcondition of a specification (of forIn) implies the target of the goal.

After specifying the loop invariant, the proof can be shortened to just all_goals mleave; grind (cf. mleave).

theorem mySum_correct_short (l : Array Nat) : mySum l = l.sum := l:Array NatmySum l = l.sum l:Array Natx:Nath:mySum l = xx = l.sum l:Array Natx:Nath:mySum l = x⊢ₛ wp⟦have out := 0; do let r forIn l out fun i r => have out := r; have out := out + i; do pure PUnit.unit pure (ForInStep.yield out) have out : Nat := r pure out (PostCond.noThrow fun a => { down := a = l.sum }) l:Array Natx:Nath:mySum l = xInvariant l.toList Nat PostShape.purel:Array Natx:Nath:mySum l = xpref✝:List Natcur✝:Natsuff✝:List Nath✝¹:l.toList = pref✝ ++ cur✝ :: suff✝b✝:Nath✝:(Prod.fst ?inv1 ({ «prefix» := pref✝, suffix := cur✝ :: suff✝, property := }, b✝)).down(Prod.fst ?inv1 ({ «prefix» := pref✝ ++ [cur✝], suffix := suff✝, property := }, b✝ + cur✝)).downl:Array Natx:Nath:mySum l = x(Prod.fst ?inv1 ({ «prefix» := [], suffix := l.toList, property := }, 0)).downl:Array Natx:Nath:mySum l = xr✝:Nath✝:(Prod.fst ?inv1 ({ «prefix» := l.toList, suffix := [], property := }, r✝)).downr✝ = l.sum case inv1 l:Array Natx:Nath:mySum l = xInvariant l.toList Nat PostShape.pure All goals completed! 🐙 all_goals l:Array Natx:Nath:mySum l = xr✝:Nath✝:l.toList.sum = r✝r✝ = l.sum; All goals completed! 🐙

This pattern is in fact so popular that mvcgen comes with special syntax for it:

theorem mySum_correct_shorter (l : Array Nat) : mySum l = l.sum := l:Array NatmySum l = l.sum l:Array Natx:Nath:mySum l = xx = l.sum l:Array Natx:Nath:mySum l = x⊢ₛ wp⟦have out := 0; do let r forIn l out fun i r => have out := r; have out := out + i; do pure PUnit.unit pure (ForInStep.yield out) have out : Nat := r pure out (PostCond.noThrow fun a => { down := a = l.sum }) mvcgen invariants · xs, out => xs.prefix.sum = out with All goals completed! 🐙

The mvcgen invariants ... with ... expands to the same syntax as the tactic sequence mvcgen; case inv1 => ...; all_goals mleave; grind above. It is the form that we will be using from now on.

It is helpful to compare the proof of mySum_correct_shorter to a traditional correctness proof:

theorem mySum_correct_vanilla (l : Array Nat) : mySum l = l.sum := l:Array NatmySum l = l.sum -- Turn the array into a list cases l with l:List NatmySum { toList := l } = { toList := l }.sum -- Unfold `mySum` and rewrite `forIn` to `foldl` l:List NatList.foldl (fun b a => b + a) 0 l = l.sum -- Generalize the inductive hypothesis suffices h : out, List.foldl (·+ ·) out l = out + l.sum l:List Nath: (out : Nat), List.foldl (fun x1 x2 => x1 + x2) out _fvar.53 = out + List.sum _fvar.53 := ?_mvar.2772List.foldl (fun b a => b + a) 0 l = l.sum All goals completed! 🐙 -- Grind away induction l with All goals completed! 🐙

This proof is similarly succinct as the proof in mySum_correct_shorter using mvcgen. However, the traditional approach relies on important properties of the program:

  • The for loop does not break or return early. Otherwise, the forIn could not be rewritten to a foldl.

  • The loop body (· + ·) is small enough to be repeated in the proof.

  • The loop body is pure, that is, does not perform any effects. (Indeed, all computations in the base monad Id factor through pure.) While forIn could still be rewritten to a foldlM, reasoning about the monadic loop body can be tough for grind.

In the following sections, we will go through several examples to learn about mvcgen and its support library, and also see where traditional proofs become difficult. This is usually caused by

  • do blocks using control flow constructs such as for loops, breaks and early return.

  • Effectful computations in non-Id monads. Such computations affect the implicit monadic context (state, exceptions) in ways that need to be reflected in loop invariants.

mvcgen scales to these challenges with reasonable effort.

18.1.1. Control flow

Let us consider another example that combines for loops with an early return.

Detecting Duplicates in a List

Recall that List.Nodup is a predicate that asserts that a given list does not contain any duplicates. The function nodup below decides this predicate:

def nodup (l : List Int) : Bool := Id.run do let mut seen : Std.HashSet Int := for x in l do if x seen then return false seen := seen.insert x return true theorem nodup_correct (l : List Int) : nodup l l.Nodup := l:List Intnodup l = true l.Nodup l:List Intr:Boolh:nodup l = rr = true l.Nodup l:List Intr:Boolh:nodup l = r⊢ₛ wp⟦have seen := ; do let r forIn l none, seen fun x r => have r := r.snd; have seen := r; have __do_jp := fun seen y => have seen := seen.insert x; do pure PUnit.unit pure (ForInStep.yield none, seen); if x seen then pure (ForInStep.done some false, seen) else do let y pure PUnit.unit __do_jp seen y have seen : Std.HashSet Int := r.snd have __do_jp : PUnit Id Bool := fun y => pure true match r.fst with | none => do let y pure PUnit.unit __do_jp y | some a => pure a (PostCond.noThrow fun a => { down := a = true l.Nodup }) mvcgen invariants · Invariant.withEarlyReturn (onReturn := fun ret seen => ret = false ¬l.Nodup) (onContinue := fun xs seen => ( x, x seen x xs.prefix) xs.prefix.Nodup) with All goals completed! 🐙

The proof has the same succinct structure as for the initial mySum example, because we again offload all proofs to grind and its existing framework around List.Nodup. Therefore, the only difference is in the invariant. Since our loop has an early return, we construct the invariant using the helper function Invariant.withEarlyReturn. This function allows us to specify the invariant in three parts:

  • onReturn ret seen holds after the loop was left through an early return with value ret. In case of nodup, the only value that is ever returned is false, in which case nodup has decided there is a duplicate in the list.

  • onContinue xs seen is the regular induction step that proves the invariant is preserved each loop iteration. The iteration state is captured by the cursor xs. The given example asserts that the set seen contains all the elements of previous loop iterations and asserts that there were no duplicates so far.

  • onExcept must hold when the loop throws an exception. There are no exceptions in Id, so we leave it unspecified to use the default. (Exceptions will be discussed at a later point.)

Now consider the following direct (and excessively golfed) proof without mvcgen:

theorem nodup_correct_directly (l : List Int) : nodup l l.Nodup := l:List Intnodup l = true l.Nodup l:List Int(have seen := ; do let r forIn l none, seen fun x r => have r := r.snd; have seen := r; have __do_jp := fun seen y => have seen := seen.insert x; do pure PUnit.unit pure (ForInStep.yield none, seen); if x seen then pure (ForInStep.done some false, seen) else do let y pure PUnit.unit __do_jp seen y have seen : Std.HashSet Int := r.snd have __do_jp : PUnit Id Bool := fun y => pure true match r.fst with | none => do let y pure PUnit.unit __do_jp y | some a => pure a).run = true l.Nodup l:List Intseen:Std.HashSet Inthseen: = seen(have seen := seen; do let r forIn l none, seen fun x r => have r := r.snd; have seen := r; have __do_jp := fun seen y => have seen := seen.insert x; do pure PUnit.unit pure (ForInStep.yield none, seen); if x seen then pure (ForInStep.done some false, seen) else do let y pure PUnit.unit __do_jp seen y have seen : Std.HashSet Int := r.snd have __do_jp : PUnit Id Bool := fun y => pure true match r.fst with | none => do let y pure PUnit.unit __do_jp y | some a => pure a).run = true l.Nodup l:List Intseen:Std.HashSet Inthseen: = seen(have seen := seen; do let r forIn l none, seen fun x r => have r := r.snd; have seen := r; have __do_jp := fun seen y => have seen := seen.insert x; do pure PUnit.unit pure (ForInStep.yield none, seen); if x seen then pure (ForInStep.done some false, seen) else do let y pure PUnit.unit __do_jp seen y have seen : Std.HashSet Int := r.snd have __do_jp : PUnit Id Bool := fun y => pure true match r.fst with | none => do let y pure PUnit.unit __do_jp y | some a => pure a).run = true l.Nodup suffices h : ?lhs l.Nodup x l, x seen l:List Intseen:Std.HashSet Inthseen: = seenh:(have seen := _fvar.249; do let r forIn _fvar.66 none, seen fun x r => have r := r.snd; have seen := r; have __do_jp := fun seen y => have seen := seen.insert x; do pure PUnit.unit pure (ForInStep.yield none, seen); if x seen then pure (ForInStep.done some false, seen) else do let y pure PUnit.unit __do_jp seen y have seen : Std.HashSet Int := r.snd have __do_jp : PUnit Id Bool := fun y => pure true match r.fst with | none => do let y pure PUnit.unit __do_jp y | some a => pure a).run = true List.Nodup _fvar.66 (x : Int), x _fvar.66 ¬x _fvar.249 := ?_mvar.327(have seen := seen; do let r forIn l none, seen fun x r => have r := r.snd; have seen := r; have __do_jp := fun seen y => have seen := seen.insert x; do pure PUnit.unit pure (ForInStep.yield none, seen); if x seen then pure (ForInStep.done some false, seen) else do let y pure PUnit.unit __do_jp seen y have seen : Std.HashSet Int := r.snd have __do_jp : PUnit Id Bool := fun y => pure true match r.fst with | none => do let y pure PUnit.unit __do_jp y | some a => pure a).run = true l.Nodup All goals completed! 🐙 l:List Intseen:Std.HashSet Int(have seen := seen; do let r forIn l none, seen fun x r => have r := r.snd; have seen := r; have __do_jp := fun seen y => have seen := seen.insert x; do pure PUnit.unit pure (ForInStep.yield none, seen); if x seen then pure (ForInStep.done some false, seen) else do let y pure PUnit.unit __do_jp seen y have seen : Std.HashSet Int := r.snd have __do_jp : PUnit Id Bool := fun y => pure true match r.fst with | none => do let y pure PUnit.unit __do_jp y | some a => pure a).run = true l.Nodup (x : Int), x l ¬x seen induction l generalizing seen with All goals completed! 🐙

Some observations:

  • The proof is even shorter than the one with mvcgen.

  • The use of generalize to generalize the accumulator relies on there being exactly one occurrence of to generalize. If that were not the case, we would have to copy parts of the program into the proof. This is a no go for larger functions.

  • grind splits along the control flow of the function and reasons about Id, given the right lemmas. While this works for Id.run_pure and Id.run_bind, it would not work for Id.run_seq, for example, because that lemma is not E-matchable. If grind would fail, we would be forced to do all the control flow splitting and monadic reasoning by hand until grind can pick up again.

The usual way to avoid replicating the control flow of a definition under proof is to use the fun_cases or fun_induction tactics. Unfortunately, fun_cases does not help with control flow inside a forIn application, for example. This is in contrast to mvcgen, which ships with support for many forIn implementations and is easily extended with @[spec] annotations for custom forIn implementations. Furthermore, a mvcgen-powered proof will never need to copy any part of the original program.

18.1.2. Compositional reasoning about effectful programs using Hoare triples

The previous examples reasoned about functions defined using Id.run do <prog> to make use of local mutability and early return in <prog>. However, real-world programs often use do notation and monads M to hide away state and failure conditions as implicit “effects”. In this use case, functions usually omit the M.run. Instead they have monadic return type M α and compose well with other functions of that return type.

Generating Fresh Numbers

Here is an example involving a stateful function mkFresh that returns auto-incremented counter values:

structure Supply where counter : Nat def mkFresh : StateM Supply Nat := do let n (·.counter) <$> get modify (fun s => {s with counter := s.counter + 1}) pure n def mkFreshN (n : Nat) : StateM Supply (List Nat) := do let mut acc := #[] for _ in [:n] do acc := acc.push ( mkFresh) pure acc.toList

mkFreshN n returns n “fresh” numbers, modifying the internal Supply state through mkFresh. Here, “fresh” refers to all previously generated numbers being distinct from the next generated number. We can formulate and prove a correctness property mkFreshN_correct in terms of List.Nodup:

theorem mkFreshN_correct (n : Nat) : ((mkFreshN n).run' s).Nodup := s:Supplyn:NatList.Nodup (StateT.run' (mkFreshN n) s) -- Focus on `(mkFreshN n).run' s`. s:Supplyn:Natx:Id (List Nat)h:StateT.run' (mkFreshN n) s = xList.Nodup x s:Supplyn:Natx:Id (List Nat)h:StateT.run' (mkFreshN n) s = x⊢ₛ wp⟦mkFreshN n (PostCond.noThrow fun a => a.Nodup) s -- Show something about monadic program `mkFresh n`. -- The `mkFreshN` and `mkFresh` arguments to `mvcgen` add to an -- internal `simp` set and makes `mvcgen` unfold these definitions. mvcgen [mkFreshN, mkFresh] invariants -- Invariant: The counter is larger than any accumulated number, -- and all accumulated numbers are distinct. -- Note that the invariant may refer to the state through function -- argument `state : Supply`. Since the next number to accumulate is -- the counter, it is distinct to all accumulated numbers. · xs, acc state => ( x acc, x < state.counter) acc.toList.Nodup with All goals completed! 🐙

18.1.3. Composing specifications

Nested unfolding of definitions as in mvcgen [mkFreshN, mkFresh] is quite blunt but effective for small programs. A more compositional way is to develop individual specification lemmas for each monadic function. These can either be passed as arguments to mvcgen or registered in a global (or scoped, or local) database of specifications using the @[spec] attribute:

Decomposing a Proof into Specifications
@[spec] theorem mkFresh_spec (c : Nat) : fun state => state.counter = c mkFresh r state => r = c c < state.counter := c:Natfun state => state.counter = c mkFresh PostCond.noThrow fun r state => r = c c < state.counter -- Unfold `mkFresh` and blast away: mvcgen [mkFresh] with All goals completed! 🐙 @[spec] theorem mkFreshN_spec (n : Nat) : True mkFreshN n r => r.Nodup := n:NatTrue mkFreshN n PostCond.noThrow fun r => r.Nodup -- `mvcgen [mkFreshN, mkFresh_spec]` if `mkFresh_spec` were not -- registered with `@[spec]` mvcgen [mkFreshN] invariants -- As before: · xs, acc state => ( x acc, x < state.counter) acc.toList.Nodup with All goals completed! 🐙 theorem mkFreshN_correct_compositional (n : Nat) : ((mkFreshN n).run' s).Nodup := -- The type `⦃⌜True⌝⦄ mkFreshN n ⦃⇓ r => ⌜r.Nodup⌝⦄` of `mkFreshN` is -- definitionally equal to -- `∀ (n : Nat) (s : Supply), True → ((mkFreshN n).run' s).Nodup` mkFreshN_spec n s True.intro

Here, the notation fun state => state.counter = c mkFresh r state => r = c c < state.counter denotes a Hoare triple (cf. Std.Do.Triple). Specifications for monadic functions always have such a Hoare triple result type.

18.1.4. Hoare triples

🔗def
Std.Do.Triple.{u, v} {m : Type u Type v} {ps : Std.Do.PostShape} [Std.Do.WP m ps] {α : Type u} (x : m α) (P : Std.Do.Assertion ps) (Q : Std.Do.PostCond α ps) : Prop
Std.Do.Triple.{u, v} {m : Type u Type v} {ps : Std.Do.PostShape} [Std.Do.WP m ps] {α : Type u} (x : m α) (P : Std.Do.Assertion ps) (Q : Std.Do.PostCond α ps) : Prop

A Hoare triple for reasoning about monadic programs. A proof for Triple x P Q is a specification for x: If assertion P holds before x, then postcondition Q holds after running x.

P x Q is convenient syntax for Triple x P Q.

syntaxHoare triple
term ::= ...
    | A Hoare triple for reasoning about monadic programs.
A proof for `Triple x P Q` is a *specification* for `x`:
If assertion `P` holds before `x`, then postcondition `Q` holds after running `x`.

`⦃P⦄ x ⦃Q⦄` is convenient syntax for `Triple x P Q`.
 term  term  term 
🔗theorem
Std.Do.Triple.and.{u, v} {m : Type u Type v} {ps : Std.Do.PostShape} {α : Type u} {P₁ : Std.Do.Assertion ps} {Q₁ : Std.Do.PostCond α ps} {P₂ : Std.Do.Assertion ps} {Q₂ : Std.Do.PostCond α ps} [Std.Do.WP m ps] (x : m α) (h₁ : P₁ x Q₁) (h₂ : P₂ x Q₂) : P₁ P₂ x Q₁ ∧ₚ Q₂
Std.Do.Triple.and.{u, v} {m : Type u Type v} {ps : Std.Do.PostShape} {α : Type u} {P₁ : Std.Do.Assertion ps} {Q₁ : Std.Do.PostCond α ps} {P₂ : Std.Do.Assertion ps} {Q₂ : Std.Do.PostCond α ps} [Std.Do.WP m ps] (x : m α) (h₁ : P₁ x Q₁) (h₂ : P₂ x Q₂) : P₁ P₂ x Q₁ ∧ₚ Q₂
🔗theorem
Std.Do.Triple.mp.{u, v} {m : Type u Type v} {ps : Std.Do.PostShape} {α : Type u} {P₁ : Std.Do.Assertion ps} {Q₁ : Std.Do.PostCond α ps} {P₂ : Std.Do.Assertion ps} {Q₂ : Std.Do.PostCond α ps} [Std.Do.WP m ps] (x : m α) (h₁ : P₁ x Q₁) (h₂ : P₂ x Q₁ →ₚ Q₂) : P₁ P₂ x Q₁ ∧ₚ Q₂
Std.Do.Triple.mp.{u, v} {m : Type u Type v} {ps : Std.Do.PostShape} {α : Type u} {P₁ : Std.Do.Assertion ps} {Q₁ : Std.Do.PostCond α ps} {P₂ : Std.Do.Assertion ps} {Q₂ : Std.Do.PostCond α ps} [Std.Do.WP m ps] (x : m α) (h₁ : P₁ x Q₁) (h₂ : P₂ x Q₁ →ₚ Q₂) : P₁ P₂ x Q₁ ∧ₚ Q₂

Given the meaning of Hoare triples above, the specification theorem for mkFresh says:

If c refers to the Supply.counter field of the Supply prestate, then running mkFresh returns c and modifies the Supply.counter of the poststate to be larger than c.

Note that this specification is lossy: mkFresh could increment its state by an arbitrary non-negative amount and still satisfy the specification. This is good, because specifications may abstract over uninteresting implementation details, ensuring resilient and small proofs.

Hoare triples are defined in terms of a logic of stateful predicates plus a weakest precondition semantics wp⟦prog that translates monadic programs into this logic:

def Triple [WP m ps] {α : Type u} (prog : m α) (P : Assertion ps) (Q : PostCond α ps) : Prop := P ⊢ₛ wp⟦prog Q

The WP type class maps a monad m to its PostShape ps, and this PostShape governs the exact shape of the Std.Do.Triple. Many of the standard monad transformers such as StateT, ReaderT and ExceptT come with a canonical WP instance. For example, StateT σ comes with a WP instance that adds a σ argument to every Assertion. Stateful entailment ⊢ₛ eta-expands through these additional σ arguments. For StateM programs, the following type is definitionally equivalent to Std.Do.Triple:

def StateMTriple {α σ : Type u} (prog : StateM σ α) (P : σ ULift Prop) (Q : (α σ ULift Prop) × PUnit) : Prop := s, (P s).down let (a, s') := prog.run s; (Q.1 a s').down example : @StateMTriple α σ = Std.Do.Triple (m := StateM σ) := rfl

The common postcondition notation ⇓ r => ... injects an assertion of type α → Assertion ps into PostCond ps (the is meant to be parsed like fun); in case of StateM by adjoining it with an empty tuple PUnit.unit. The shape of postconditions becomes more interesting once exceptions enter the picture. The notation p embeds a pure hypotheses p into a stateful assertion. Vice versa, any stateful hypothesis P is called pure if it is equivalent to p for some p. Pure, stateful hypotheses may be freely moved into the regular Lean context and back. (This can be done manually with the mpure tactic.)

18.1.4.1. An advanced note about pure preconditions and a notion of frame rule

This subsection is a bit of a digression and can be skipped on first reading.

Say the specification for some Aeneas-inspired monadic addition function x +? y : M UInt8 has the requirement that the addition won't overflow, that is, h : x.toNat + y.toNat ≤ UInt8.size. Should this requirement be encoded as a regular Lean hypothesis of the specification (add_spec_hyp) or should this requirement be encoded as a pure precondition of the Hoare triple, using ⌜·⌝ notation (add_spec_pre)?

theorem add_spec_hyp (x y : UInt8) (h : x.toNat + y.toNat ≤ UInt8.size) :
    ⦃⌜True⌝⦄ x +? y ⦃⇓ r => ⌜r.toNat = x.toNat + y.toNat⌝⦄ := ...

theorem add_spec_pre (x y : UInt8) :
    ⦃⌜x.toNat + y.toNat ≤ UInt8.size⌝⦄
    x +? y
    ⦃⇓ r => ⌜r.toNat = x.toNat + y.toNat⌝⦄ := ...

The first approach is advisable, although it should not make a difference in practice. The VC generator will move pure hypotheses from the stateful context into the regular Lean context, so the second form turns effectively into the first form. This is referred to as framing hypotheses (cf. the mpure and mframe tactics). Hypotheses in the Lean context are part of the immutable frame of the stateful logic, because in contrast to stateful hypotheses they survive the rule of consequence.

18.1.5. Monad transformer stacks

Real-world programs often orchestrate the interaction of independent subsystems through a stack of monad transformers. We can tweak the previous example to demonstrate this.

Transformer Stacks

Suppose that mkFresh is generalized to work in any base monad m under StateT Supply. Furthermore, mkFreshN is defined in terms of a concrete monad transformer stack AppM and lifts mkFresh into AppM. Then the mvcgen-based proof goes through unchanged:

def mkFresh [Monad m] : StateT Supply m Nat := do let n (·.counter) <$> get modify (fun s => {s with counter := s.counter + 1}) pure n abbrev AppM := StateT Bool (StateT Supply (ReaderM String)) abbrev liftCounterM : StateT Supply (ReaderM String) α AppM α := liftM def mkFreshN (n : Nat) : AppM (List Nat) := do let mut acc := #[] for _ in [:n] do let n liftCounterM mkFresh acc := acc.push n return acc.toList @[spec] theorem mkFresh_spec [Monad m] [WPMonad m ps] (c : Nat) : fun state => state.counter = c mkFresh (m := m) r state => r = c c < state.counter := m:Type Typeps:PostShapeinst✝¹:Monad minst✝:WPMonad m psc:Natfun state => state.counter = c mkFresh PostCond.noThrow fun r state => r = c c < state.counter mvcgen [mkFresh] with All goals completed! 🐙 @[spec] theorem mkFreshN_spec (n : Nat) : True mkFreshN n r => r.Nodup := n:NatTrue mkFreshN n PostCond.noThrow fun r => r.Nodup -- `liftCounterM` here ensures unfolding mvcgen [mkFreshN, liftCounterM] invariants · xs, acc _ state => ( n acc, n < state.counter) acc.toList.Nodup with All goals completed! 🐙

The WPMonad type class asserts that wp⟦prog distributes over the Monad operations (“monad morphism”). This proof might not look much more exciting than when only a single monad was involved. However, under the radar of the user the proof builds on a cascade of specifications for MonadLift instances.

18.1.6. Exceptions

If let mut is the first-order pendant to StateT, then early return is the first-order pendant to ExceptT. We have seen how the mvcgen copes with StateT; here we will look at the program logic's support for ExceptT.

Exceptions are the reason why the type of postconditions PostCond α ps is not simply a single condition of type α Assertion ps for the success case. To see why, suppose the latter was the case, and suppose that program prog throws an exception in a prestate satisfying P. Should we be able to prove P prog r => Q' r? (Recall that is grammatically similar to fun.) There is no result r, so it is unclear what this proof means for Q'!

So there are two reasonable options, inspired by non-termination in traditional program logics:

  1. Total correctness interpretation: P prog r => Q' r asserts that, given P holds, then prog terminates and Q' holds for the result.

  2. Partial correctness interpretation: P prog r => Q' r asserts that, given P holds, and if prog terminates then Q' holds for the result.

The notation r => Q' r has the total interpretation (cf. PostCond.noThrow), while ⇓? r => Q' r has the partial interpretation (cf. PostCond.mayThrow).

🔗def
Std.Do.PostCond.{u} (α : Type u) (ps : Std.Do.PostShape) : Type u
Std.Do.PostCond.{u} (α : Type u) (ps : Std.Do.PostShape) : Type u

A postcondition for the given predicate shape, with one Assertion for the terminating case and one Assertion for each .except layer in the predicate shape.

variable (α σ ε : Type)
example : PostCond α (.arg σ .pure) = ((α → σ → ULift Prop) × PUnit) := rfl
example : PostCond α (.except ε .pure) = ((α → ULift Prop) × (ε → ULift Prop) × PUnit) := rfl
example : PostCond α (.arg σ (.except ε .pure)) = ((α → σ → ULift Prop) × (ε → ULift Prop) × PUnit) := rfl
example : PostCond α (.except ε (.arg σ .pure)) = ((α → σ → ULift Prop) × (ε → σ → ULift Prop) × PUnit) := rfl
🔗def
Std.Do.PostCond.noThrow.{u} {ps : Std.Do.PostShape} {α : Type u} (p : α Std.Do.Assertion ps) : Std.Do.PostCond α ps
Std.Do.PostCond.noThrow.{u} {ps : Std.Do.PostShape} {α : Type u} (p : α Std.Do.Assertion ps) : Std.Do.PostCond α ps

A postcondition expressing total correctness. That is, it expresses that the asserted computation finishes without throwing an exception and the result satisfies the given predicate p.

🔗def
Std.Do.PostCond.mayThrow.{u} {ps : Std.Do.PostShape} {α : Type u} (p : α Std.Do.Assertion ps) : Std.Do.PostCond α ps
Std.Do.PostCond.mayThrow.{u} {ps : Std.Do.PostShape} {α : Type u} (p : α Std.Do.Assertion ps) : Std.Do.PostCond α ps

A postcondition expressing partial correctness. That is, it expresses that if the asserted computation finishes without throwing an exception then the result satisfies the given predicate p. Nothing is asserted when the computation throws an exception.

syntaxPostconditions
term ::= ...
    | A postcondition expressing total correctness.
That is, it expresses that the asserted computation finishes without throwing an exception
*and* the result satisfies the given predicate `p`.
 term* => term
term ::= ...
    | A postcondition expressing partial correctness.
That is, it expresses that *if* the asserted computation finishes without throwing an exception
*then* the result satisfies the given predicate `p`.
Nothing is asserted when the computation throws an exception.
⇓? term* => term

So in the running example, P prog r => Q' r is unprovable, but P prog ⇓? r => Q' r is trivially provable. However, the binary choice suggests that there is actually a spectrum of correctness properties to express. The notion of postconditions PostCond in Std.Do supports this spectrum.

For example, suppose that our Supply of fresh numbers is bounded and we want to throw an exception if the supply is exhausted. Then mkFreshN should throw an exception only if the supply is indeed exhausted. The following correctness property expresses this:

structure Supply where counter : Nat limit : Nat property : counter limit def mkFresh : EStateM String Supply Nat := do let supply get if h : supply.counter = supply.limit then throw s!"Supply exhausted: {supply.counter} = {supply.limit}" else let n := supply.counter have := supply.property set {supply with counter := n + 1, property := c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion pssupply:Supplyh:¬supply.counter = supply.limitn:Nat := [Error pretty printing expression: unknown free variable `_uniq.913`. Falling back to raw printer.] Exceptions.Supply.counter _uniq.913this:[Error pretty printing expression: unknown free variable `_uniq.913`. Falling back to raw printer.] LE.le.{0} Nat instLENat (Exceptions.Supply.counter _uniq.913) (Exceptions.Supply.limit _uniq.913) := [Error pretty printing expression: unknown free variable `_uniq.913`. Falling back to raw printer.] Exceptions.Supply.property _uniq.913n + 1 supply.limit All goals completed! 🐙} pure n def mkFreshN (n : Nat) : EStateM String Supply (List Nat) := do let mut acc := #[] for _ in [:n] do acc := acc.push ( mkFresh) pure acc.toList @[spec] theorem mkFresh_spec (c : Nat) : fun state => state.counter = c mkFresh post⟨fun r state => r = c c < state.counter, fun _ state => c = state.counter c = state.limit := c:Natfun state => state.counter = c mkFresh (fun r state => r = c c < state.counter, fun x state => c = state.counter c = state.limit, ()) mvcgen [mkFresh] with All goals completed! 🐙 @[spec] theorem mkFreshN_spec (n : Nat) : True mkFreshN n post⟨fun r => r.Nodup, fun _msg state => state.counter = state.limit := n:NatTrue mkFreshN n (fun r => r.Nodup, fun _msg state => state.counter = state.limit, ()) mvcgen [mkFreshN] invariants · post⟨fun xs, acc state => ( n acc, n < state.counter) acc.toList.Nodup, fun _msg state => state.counter = state.limit with All goals completed! 🐙 theorem mkFreshN_correct (n : Nat) : match (mkFreshN n).run s with | .ok l _ => l.Nodup | .error _ s' => s'.counter = s'.limit := s:Supplyn:Natmatch (mkFreshN n).run s with | EStateM.Result.ok l a => l.Nodup | EStateM.Result.error a s' => s'.counter = s'.limit s:Supplyn:Natx:EStateM.Result String Supply (List Nat)h:(mkFreshN n).run s = xmatch x with | EStateM.Result.ok l a => l.Nodup | EStateM.Result.error a s' => s'.counter = s'.limit s:Supplyn:Natx:EStateM.Result String Supply (List Nat)h:(mkFreshN n).run s = x⊢ₛ wp⟦mkFreshN n (fun a s' => match EStateM.Result.ok a s' with | EStateM.Result.ok l a => l.Nodup | EStateM.Result.error a s' => s'.counter = s'.limit, fun e s' => match EStateM.Result.error e s' with | EStateM.Result.ok l a => l.Nodup | EStateM.Result.error a s' => s'.counter = s'.limit, ()) s All goals completed! 🐙

Just as any StateT σ-like layer in the monad stack gives rise to a PostShape.arg σ layer in the ps that WP maps into, any ExceptT ε-like layer gives rise to a PostShape.except ε layer.

Every PostShape.arg σ adds another σ → ... layer to the language of Assertions. Every PostShape.except ε leaves the Assertion language unchanged, but adds another exception condition to the postcondition.

🔗def
Std.Do.Assertion.{u} (ps : Std.Do.PostShape) : Type u
Std.Do.Assertion.{u} (ps : Std.Do.PostShape) : Type u

An assertion on the .args in the given predicate shape.

example : Assertion (.arg ρ .pure) = (ρ → ULift Prop) := rfl
example : Assertion (.except ε .pure) = ULift Prop := rfl
example : Assertion (.arg σ (.except ε .pure)) = (σ → ULift Prop) := rfl
example : Assertion (.except ε (.arg σ .pure)) = (σ → ULift Prop) := rfl

This is an abbreviation for SPred under the hood, so all theorems about SPred apply.

Hence the WP instance for EStateM ε σ maps to the PostShape PostShape.except ε (.arg σ .pure), just as for ExceptT ε (StateM σ).

18.1.7. Extending mvcgen with support for custom monads

The mvcgen framework is designed to be extensible. None of the monads so far have in any way been hard-coded into mvcgen. Rather, mvcgen relies on instances of the WP and WPMonad type class and user-provided specifications to generate verification conditions.

The WP instance defines the weakest precondition interpretation of a monad m into a predicate transformer PredTrans ps, and the matching WPMonad instance asserts that this translation distributes over the Monad operations.

🔗structure
Std.Do.PredTrans.{u} (ps : Std.Do.PostShape) (α : Type u) : Type u
Std.Do.PredTrans.{u} (ps : Std.Do.PostShape) (α : Type u) : Type u

The type of predicate transformers for a given ps : PostShape and return type α : Type. A predicate transformer x : PredTrans ps α is a function that takes a postcondition Q : PostCond α ps and returns a precondition x.apply Q : Assertion ps, with the additional monotonicity property that the precondition is stronger the stronger the postcondition is: Q₁ ⊢ₚ Q₂ x.apply Q₁ ⊢ₛ x.apply Q₂.

Constructor

Std.Do.PredTrans.mk.{u}

Fields

apply : Std.Do.PostCond α ps  Std.Do.Assertion ps
conjunctive : Std.Do.PredTrans.Conjunctive self.apply
🔗type class
Std.Do.WP.{u, v} (m : Type u Type v) (ps : outParam Std.Do.PostShape) : Type (max (u + 1) v)
Std.Do.WP.{u, v} (m : Type u Type v) (ps : outParam Std.Do.PostShape) : Type (max (u + 1) v)

A weakest precondition interpretation of a monadic program x : m α in terms of a predicate transformer PredTrans ps α. The monad m determines ps : PostShape. See the module comment for more details.

Instance Constructor

Std.Do.WP.mk.{u, v}

Methods

wp : {α : Type u}  m α  Std.Do.PredTrans ps α
syntaxWeakest preconditions
term ::= ...
    | wp⟦ term 
🔗type class
Std.Do.WPMonad.{u, v} (m : Type u Type v) (ps : outParam Std.Do.PostShape) [Monad m] : Type (max (u + 1) v)
Std.Do.WPMonad.{u, v} (m : Type u Type v) (ps : outParam Std.Do.PostShape) [Monad m] : Type (max (u + 1) v)

A WP that is also a monad morphism, preserving pure and bind. (They all are.)

Instance Constructor

Std.Do.WPMonad.mk.{u, v}

Extends

Methods

map_const :  {α β : Type u}, Functor.mapConst = Functor.map  Function.const β
Inherited from
  1. LawfulMonad m
  2. Std.Do.WP m ps
id_map :  {α : Type u} (x : m α), id <$> x = x
Inherited from
  1. LawfulMonad m
  2. Std.Do.WP m ps
comp_map :  {α β γ : Type u} (g : α  β) (h : β  γ) (x : m α), (h  g) <$> x = h <$> g <$> x
Inherited from
  1. LawfulMonad m
  2. Std.Do.WP m ps
seqLeft_eq :  {α β : Type u} (x : m α) (y : m β), x <* y = Function.const β <$> x <*> y
Inherited from
  1. LawfulMonad m
  2. Std.Do.WP m ps
seqRight_eq :  {α β : Type u} (x : m α) (y : m β), x *> y = Function.const α id <$> x <*> y
Inherited from
  1. LawfulMonad m
  2. Std.Do.WP m ps
pure_seq :  {α β : Type u} (g : α  β) (x : m α), pure g <*> x = g <$> x
Inherited from
  1. LawfulMonad m
  2. Std.Do.WP m ps
map_pure :  {α β : Type u} (g : α  β) (x : α), g <$> pure x = pure (g x)
Inherited from
  1. LawfulMonad m
  2. Std.Do.WP m ps
seq_pure :  {α β : Type u} (g : m (α  β)) (x : α), g <*> pure x = (fun h => h x) <$> g
Inherited from
  1. LawfulMonad m
  2. Std.Do.WP m ps
seq_assoc :  {α β γ : Type u} (x : m α) (g : m (α  β)) (h : m (β  γ)), h <*> (g <*> x) = Function.comp <$> h <*> g <*> x
Inherited from
  1. LawfulMonad m
  2. Std.Do.WP m ps
bind_pure_comp :  {α β : Type u} (f : α  β) (x : m α),
  (do
      let a  x
      pure (f a)) =
    f <$> x
Inherited from
  1. LawfulMonad m
  2. Std.Do.WP m ps
bind_map :  {α β : Type u} (f : m (α  β)) (x : m α),
  (do
      let x_1  f
      x_1 <$> x) =
    f <*> x
Inherited from
  1. LawfulMonad m
  2. Std.Do.WP m ps
pure_bind :  {α β : Type u} (x : α) (f : α  m β), pure x >>= f = f x
Inherited from
  1. LawfulMonad m
  2. Std.Do.WP m ps
bind_assoc :  {α β γ : Type u} (x : m α) (f : α  m β) (g : β  m γ), x >>= f >>= g = x >>= fun x => f x >>= g
Inherited from
  1. LawfulMonad m
  2. Std.Do.WP m ps
wp : {α : Type u}  m α  Std.Do.PredTrans ps α
Inherited from
  1. LawfulMonad m
  2. Std.Do.WP m ps
wp_pure :  {α : Type u} (a : α), Std.Do.wp (pure a) = pure a
wp_bind :  {α β : Type u} (x : m α) (f : α  m β),
  (Std.Do.wp do
      let a  x
      f a) =
    do
    let a  Std.Do.wp x
    Std.Do.wp (f a)
Adding mvcgen support for Aeneas

Suppose one wants to use mvcgen to generate verification conditions for programs generated by Aeneas. Aeneas translates Rust programs into Lean programs in the following Result monad:

inductive Error where | integerOverflow: Error -- ... more error kinds ... inductive Result (α : Type u) where | ok (v: α): Result α | fail (e: Error): Result α | div instance Result.instMonad : Monad Result where pure x := .ok x bind x f := match x with | .ok v => f v | .fail e => .fail e | .div => .div instance declaration uses 'sorry'Result.instLawfulMonad : LawfulMonad Result := c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion psLawfulMonad Result -- TODO: Replace sorry with grind when it no longer introduces section -- variables c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion ps {α : Type u_1} (x : Result α), id <$> x = xc:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion ps {α β : Type u_1} (x : α) (f : α Result β), pure x >>= f = f xc:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion ps {α β γ : Type u_1} (x : Result α) (f : α Result β) (g : β Result γ), x >>= f >>= g = x >>= fun x => f x >>= gc:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion psautoParam (∀ {α β : Type u_1} (x : α) (y : Result β), Functor.mapConst x y = Function.const β x <$> y) _auto✝c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion psautoParam (∀ {α β : Type u_1} (x : Result α) (y : Result β), x <* y = do let a x let _ y pure a) _auto✝c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion psautoParam (∀ {α β : Type u_1} (x : Result α) (y : Result β), x *> y = do let _ x y) _auto✝c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion psautoParam (∀ {α β : Type u_1} (f : α β) (x : Result α), (do let y x pure (f y)) = f <$> x) _auto✝c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion psautoParam (∀ {α β : Type u_1} (f : Result (α β)) (x : Result α), (do let x_1 f x_1 <$> x) = f <*> x) _auto✝ c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion ps {α : Type u_1} (x : Result α), id <$> x = xc:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion ps {α β : Type u_1} (x : α) (f : α Result β), pure x >>= f = f xc:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion ps {α β γ : Type u_1} (x : Result α) (f : α Result β) (g : β Result γ), x >>= f >>= g = x >>= fun x => f x >>= gc:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion psautoParam (∀ {α β : Type u_1} (x : α) (y : Result β), Functor.mapConst x y = Function.const β x <$> y) _auto✝c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion psautoParam (∀ {α β : Type u_1} (x : Result α) (y : Result β), x <* y = do let a x let _ y pure a) _auto✝c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion psautoParam (∀ {α β : Type u_1} (x : Result α) (y : Result β), x *> y = do let _ x y) _auto✝c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion psautoParam (∀ {α β : Type u_1} (f : α β) (x : Result α), (do let y x pure (f y)) = f <$> x) _auto✝c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion psautoParam (∀ {α β : Type u_1} (f : Result (α β)) (x : Result α), (do let x_1 f x_1 <$> x) = f <*> x) _auto✝ (c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion psautoParam (∀ {α β : Type u_1} (f : Result (α β)) (x : Result α), True) _auto✝; All goals completed! 🐙)

Support for this monad is a matter of

  1. Adding a WP and WPMonad instance for Result

  2. Registering specification lemmas for the translation of basic Rust primitives such as addition etc.

The first part is straightforward:

instance Result.instWP : WP Result (.except Error .pure) where wp x := match x with | .ok v => wp (pure v : Except Error _) | .fail e => wp (throw e : Except Error _) | .div => PredTrans.const False -- set_option trace.Meta.synthInstance true in instance Result.instWPMonad : WPMonad Result (.except Error .pure) where wp_pure := c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion ps {α : Type} (a : α), wp (pure a) = pure a c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion psα✝:Typea✝:α✝wp (pure a✝) = pure a✝ c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ✝:PostCond α psprog:m αp:PropQ':α Assertion psα✝:Typea✝:α✝Q:PostCond α✝ (PostShape.except Error PostShape.pure)(wp⟦pure a✝ Q).down ((pure a✝).apply Q).down All goals completed! 🐙 wp_bind x f := c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion psα✝:Typeβ✝:Typex:Result α✝f:α✝ Result β✝(wp do let a x f a) = do let a wp x wp (f a) c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion psα✝:Typeβ✝:Typex:Result α✝f:α✝ Result β✝(match match x with | ok v => f v | fail e => fail e | div => div with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False) = (match x with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).bind fun a => match f a with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ✝:PostCond α psprog:m αp:PropQ':α Assertion psα✝:Typeβ✝:Typex:Result α✝f:α✝ Result β✝Q:PostCond β✝ (PostShape.except Error PostShape.pure)((match match x with | ok v => f v | fail e => fail e | div => div with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).apply Q).down (((match x with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).bind fun a => match f a with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).apply Q).down c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ✝:PostCond α psprog:m αp:PropQ':α Assertion psα✝:Typeβ✝:Typef:α✝ Result β✝Q:PostCond β✝ (PostShape.except Error PostShape.pure)v✝:α✝((match match ok v✝ with | ok v => f v | fail e => fail e | div => div with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).apply Q).down (((match ok v✝ with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).bind fun a => match f a with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).apply Q).downc:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ✝:PostCond α psprog:m αp:PropQ':α Assertion psα✝:Typeβ✝:Typef:α✝ Result β✝Q:PostCond β✝ (PostShape.except Error PostShape.pure)e✝:Error((match match fail e✝ with | ok v => f v | fail e => fail e | div => div with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).apply Q).down (((match fail e✝ with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).bind fun a => match f a with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).apply Q).downc:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ✝:PostCond α psprog:m αp:PropQ':α Assertion psα✝:Typeβ✝:Typef:α✝ Result β✝Q:PostCond β✝ (PostShape.except Error PostShape.pure)((match match div with | ok v => f v | fail e => fail e | div => div with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).apply Q).down (((match div with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).bind fun a => match f a with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).apply Q).down c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ✝:PostCond α psprog:m αp:PropQ':α Assertion psα✝:Typeβ✝:Typef:α✝ Result β✝Q:PostCond β✝ (PostShape.except Error PostShape.pure)v✝:α✝((match match ok v✝ with | ok v => f v | fail e => fail e | div => div with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).apply Q).down (((match ok v✝ with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).bind fun a => match f a with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).apply Q).downc:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ✝:PostCond α psprog:m αp:PropQ':α Assertion psα✝:Typeβ✝:Typef:α✝ Result β✝Q:PostCond β✝ (PostShape.except Error PostShape.pure)e✝:Error((match match fail e✝ with | ok v => f v | fail e => fail e | div => div with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).apply Q).down (((match fail e✝ with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).bind fun a => match f a with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).apply Q).downc:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ✝:PostCond α psprog:m αp:PropQ':α Assertion psα✝:Typeβ✝:Typef:α✝ Result β✝Q:PostCond β✝ (PostShape.except Error PostShape.pure)((match match div with | ok v => f v | fail e => fail e | div => div with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).apply Q).down (((match div with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).bind fun a => match f a with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).apply Q).down All goals completed! 🐙 theorem Result.of_wp {α} {x : Result α} (P : Result α Prop) : (⊢ₛ wp⟦x post⟨fun a => P (.ok a), fun e => P (.fail e)) P x := α:Typex:Result αP:Result α Prop(⊢ₛ wp⟦x (fun a => P (ok a), fun e => P (fail e), ())) P x α:Typex:Result αP:Result α Prophspec:⊢ₛ wp⟦x (fun a => P (ok a), fun e => P (fail e), ())P x α:Typex:Result αP:Result α Prophspec:⊢ₛ (match x with | ok v => wp (pure v) | fail e => wp (throw e) | div => PredTrans.const False).apply (fun a => P (ok a), fun e => P (fail e), ())P x α:TypeP:Result α Propx✝:Result αv✝:αhspec:⊢ₛ wp⟦pure v✝ (fun a => P (ok a), fun e => P (fail e), ())P (ok v✝)α:TypeP:Result α Propx✝:Result αe✝:Errorhspec:⊢ₛ wp⟦throw e✝ (fun a => P (ok a), fun e => P (fail e), ())P (fail e✝)α:TypeP:Result α Propx✝:Result αhspec:⊢ₛ (PredTrans.const False).apply (fun a => P (ok a), fun e => P (fail e), ())P div α:TypeP:Result α Propx✝:Result αv✝:αhspec:⊢ₛ wp⟦pure v✝ (fun a => P (ok a), fun e => P (fail e), ())P (ok v✝)α:TypeP:Result α Propx✝:Result αe✝:Errorhspec:⊢ₛ wp⟦throw e✝ (fun a => P (ok a), fun e => P (fail e), ())P (fail e✝)α:TypeP:Result α Propx✝:Result αhspec:⊢ₛ (PredTrans.const False).apply (fun a => P (ok a), fun e => P (fail e), ())P div All goals completed! 🐙

The WP instance above translates programs in Result α to predicate transformers in PredTrans ps α. That is, a function in PostCond α ps Assertion ps, mapping a postcondition to its weakest precondition. Note that this definition of the WP instance determines what properties can be derived from proved specifications via Result.of_wp. This lemma defines what “weakest precondition” means.

To exemplify the second part, here is an example definition of UInt32 addition in Result that models integer overflow:

instance : MonadExcept Error Result where throw e := .fail e tryCatch x h := match x with | .ok v => pure v | .fail e => h e | .div => .div def addOp (x y : UInt32) : Result UInt32 := if x.toNat + y.toNat UInt32.size then throw .integerOverflow else pure (x + y)

There are two relevant specification lemmas to register:

@[spec] theorem Result.throw_spec {α Q} (e : Error) : Q.2.1 e throw (m := Result) (α := α) e Q := id @[spec] theorem addOp_ok_spec {x y} (h : x.toNat + y.toNat < UInt32.size) : True addOp x y r => r = x + y (x + y).toNat = x.toNat + y.toNat := x:UInt32y:UInt32h:x.toNat + y.toNat < UInt32.sizeTrue addOp x y PostCond.noThrow fun r => r = x + y (x + y).toNat = x.toNat + y.toNat mvcgen [addOp] with (All goals completed! 🐙; try All goals completed! 🐙)

This is already enough to prove the following example:

example : True do let mut x addOp 1 3 for _ in [:4] do x addOp x 5 return x r => r.toNat = 24 := c:Natm:Type u Type vps:PostShapeinst✝:WP m psα:Type uσ:Type uε:Type uP:Assertion psQ:PostCond α psprog:m αp:PropQ':α Assertion psTrue do let x addOp 1 3 let r forIn [:4] x fun x r => let x := r; do let r addOp x 5 let x : UInt32 := r pure PUnit.unit pure (ForInStep.yield x) let x : UInt32 := r pure x PostCond.noThrow fun r => r.toNat = 24 mvcgen invariants · xs, x => x.toNat = 4 + 5 * xs.prefix.length with (All goals completed! 🐙; try All goals completed! 🐙)

18.1.8. Proof mode for stateful goals

It is a priority of mvcgen to break down monadic programs into VCs that are straightforward to understand. For example, when the monad stack is monomorphic and all loop invariants have been instantiated, an invocation of all_goals mleave should simplify away any Std.Do.SPred specific constructs and leave behind a goal that is easily understood by humans and grind. This all_goals mleave step is carried out automatically by mvcgen after loop invariants have been instantiated.

However, there are times when mleave will be unable to remove all Std.Do.SPred constructs. In this case, verification conditions of the form H ⊢ₛ T will be left behind. The assertion language Assertion translates into an Std.Do.SPred as follows:

abbrev PostShape.args : PostShape.{u} List (Type u) | .pure => [] | .arg σ s => σ :: PostShape.args s | .except _ s => PostShape.args s abbrev Assertion (ps : PostShape.{u}) : Type u := SPred (PostShape.args ps)
🔗def
Std.Do.SPred.{u} (σs : List (Type u)) : Type u
Std.Do.SPred.{u} (σs : List (Type u)) : Type u

A predicate indexed by a list of states.

example : SPred [Nat, Bool] = (Nat → Bool → ULift Prop) := rfl
🔗def
Std.Do.SPred.entails.{u} {σs : List (Type u)} (P Q : Std.Do.SPred σs) : Prop
Std.Do.SPred.entails.{u} {σs : List (Type u)} (P Q : Std.Do.SPred σs) : Prop

Entailment in SPred.

syntaxNotation for SPred
term ::= ...
    | Embedding of pure Lean values into `SVal`. An alias for `SPred.pure`. term
term ::= ...
    | Entailment in `SPred`; sugar for `SPred.entails`. term ⊢ₛ term
term ::= ...
    | Bi-entailment in `SPred`; sugar for `SPred.bientails`. term ⊣⊢ₛ term
term ::= ...
    | spred(`And a b`, or `a ∧ b`, is the conjunction of propositions. It can be
constructed and destructed like a pair: if `ha : a` and `hb : b` then
`⟨ha, hb⟩ : a ∧ b`, and if `h : a ∧ b` then `h.left : a` and `h.right : b`.


Conventions for notations in identifiers:

 * The recommended spelling of `∧` in identifiers is `and`.term  term)
term ::= ...
    | spred(`Or a b`, or `a ∨ b`, is the disjunction of propositions. There are two
constructors for `Or`, called `Or.inl : a → a ∨ b` and `Or.inr : b → a ∨ b`,
and you can use `match` or `cases` to destruct an `Or` assumption into the
two cases.


Conventions for notations in identifiers:

 * The recommended spelling of `∨` in identifiers is `or`.term  term)
term ::= ...
    | spred(term  term)
term ::= ...
    | spred(`Not p`, or `¬p`, is the negation of `p`. It is defined to be `p → False`,
so if your goal is `¬p` you can use `intro h` to turn the goal into
`h : p ⊢ False`, and if you have `hn : ¬p` and `h : p` then `hn h : False`
and `(hn h).elim` will prove anything.
For more information: [Propositional Logic](https://lean-lang.org/theorem_proving_in_lean4/propositions_and_proofs.html#propositional-logic)


Conventions for notations in identifiers:

 * The recommended spelling of `¬` in identifiers is `not`.¬term)
term ::= ...
    | spred( ident, term)
term ::= ...
    | spred( `binderIdent` matches an `ident` or a `_`. It is used for identifiers in binding
position, where `_` means that the value should be left unnamed and inaccessible.
ident, term)

A common case for when a VC of the form H ⊢ₛ T is left behind is when the base monad m is polymorphic. In this case, the proof will depend on a WP m ps instance which governs the translation into the Assertion language, but the exact correspondence to σs : List (Type u) is yet unknown. To successfully discharge such a VC, mvcgen comes with an entire proof mode that is inspired by that of the Iris concurrent separation logic. (In fact, the proof mode was adapted in large part from its Lean clone, iris-lean.) The Lean 4 test file tests/lean/run/spredProofMode.lean contains many examples of that proof mode to learn from, and the reference manual contains a list of all proof mode tactics.

18.1.9. Further reading

More examples can be found in Lean's test suite.