This section is a tutorial that introduces the most important concepts of vcgen top-down.
The vcgen tactic proves properties of any kind of program that has a weakest precondition semantics, from monadic computations to the syntax trees of a small imperative language.
This tutorial focuses on monadic programs written with Lean.Parser.Term.do : termdo-notation, because they are the executable programs that Lean users most often want to prove something about.
Recall that you need to import Std.WP and Std.Tactic.Do and open the namespaces Std.WP and Lean.Order to run these examples:
One style in which program specifications can be written is to provide a preconditionP, which the caller of a program \mathit{prog} is expected to ensure, and a postconditionQ, which the \mathit{prog} is expected to ensure.
The program \mathit{prog} satisfies the specification if running it when the precondition P holds always results in the postcondition Q holding.
In general, many different preconditions might suffice for a program to ensure the postcondition.
After all, new preconditions can be generated by replacing a precondition P_1 with P_1 \wedge P_2.
The weakest precondition\textbf{wp}⟦\mathit{prog}⟧(Q) of a program \mathit{prog} and postcondition Q is a precondition for which \mathit{prog} ensures the postcondition Q and is implied by all other such preconditions.
One way to prove something about the result of a program is to find the weakest precondition that guarantees the desired result, and then to show that this weakest precondition is simply true.
This means that the postcondition holds no matter what.
As a first example of vcgen, the function mySum computes the sum of an array using local mutable state and a Lean.Parser.Term.doFor : doElem`for x in e do s` iterates over `e` assuming `e`'s type has an instance of the `ForIn` typeclass.
`break` and `continue` are supported inside `for` loops.
`for x in e, x2 in e2, ... do s` iterates over the given collections in parallel,
until at least one of them is exhausted.
The types of `e2` etc. must implement the `Std.ToStream` typeclass.
for loop:
If mySum is correct, then it is equal to Array.sum.
The statement mySum l = l.sum mentions no monad, because Lean.Parser.Term.do : termdo is an internal implementation detail of mySum.
The proof below therefore first brings the goal into the form that vcgen works on, a weakest precondition, with the lemma Id.of_run_eq_wp.
This lemma states that facts about the result of running a computation in the Id monad that terminates normally (Id computations never throw exceptions) can be proved by showing that the weakest precondition that ensures the desired result is true.
Next, the proof uses vcgen to replace the formulation in terms of weakest preconditions with a set of verification conditions.
While vcgen is mostly automatic, it does require an invariant for the loop.
A loop invariant is a statement that is both assumed and guaranteed by the body of the loop; if it is true when the loop begins, then it will be true when the loop terminates.
theoremmySum_correct(l:ArrayNat):mySuml=l.sum:=l:ArrayNat⊢ mySuml=l.sum-- Focus on the part of the program with the `do` block (`Id.run ...`)l:ArrayNatx:Nath:mySuml=x⊢ x=l.suml:ArrayNatx:Nath:mySuml=x⊢ wp(haveout:=0;dolet__s←forInloutfuni__s=>haveout:=__s;haveout:=out+i;pure(ForInStep.yieldout)haveout:Nat:=__spureout)(funx=>x=l.sum)()-- Break down into verification conditionsl:ArrayNatx:Nath:mySuml=x⊢ InvariantNatNatPropl:ArrayNatx:Nath:mySuml=x⊢ ?inv1[](ForIn.toListl)0l:ArrayNatx:Nath:mySuml=xa✝¹:Nata✝:?inv1(ForIn.toListl)[]a✝¹⊢ a✝¹=l.suml:ArrayNatx:Nath:mySuml=xpref✝:ListNatcur✝:Natsuff✝:ListNat_h✝:ForIn.toListl=pref✝++cur✝::suff✝b✝:Nata✝:?inv1pref✝(cur✝::suff✝)b✝out:Nat:=b✝+cur✝⊢ ?inv1(pref✝++[cur✝])suff✝out-- Specify the invariant which should hold throughout the loop.-- It is a function of three arguments:-- * `pref` is the list of elements that the loop has already visited-- * `suff` is the list of elements that the loop has yet to visit-- * `out` is the current value of the `let mut` variable-- Our invariant is that `out` holds the sum of the prefix.caseinv1l:ArrayNatx:Nath:mySuml=x⊢ InvariantNatNatPropAll goals completed! 🐙-- Prove that the invariant is true at the startcasevc1l:ArrayNatx:Nath:mySuml=x⊢ [].sum=0All goals completed! 🐙-- Prove that the invariant at the end of the loop implies the-- property we wantedcasevc2l:ArrayNatx:Nath:mySuml=xa✝¹:Nata✝:(ForIn.toListl).sum=a✝¹⊢ a✝¹=l.sumAll goals completed! 🐙-- Prove that our invariant is preserved at each step of the loopcasevc3l:ArrayNatx:Nath:mySuml=xpref✝:ListNatcur✝:Natsuff✝:ListNat_h✝:ForIn.toListl=pref✝++cur✝::suff✝b✝:Nata✝:pref✝.sum=b✝out:Nat:=b✝+cur✝⊢ (pref✝++[cur✝]).sum=outAll goals completed! 🐙
The case labels vc1, vc2, … number the VCs in the order in which they arise during generation.
In this proof:
vc1 proves that the invariant holds when the loop starts.
vc2 proves that the invariant after the final iteration implies the desired property.
vc3 proves that each iteration of the loop body preserves the invariant.
After specifying the loop invariant, the proof can be shortened to just all_goals grind.
The vcgen invariants ...with... form supplies the invariants and then runs the given grind step on every remaining VC; the finish step closes each VC with grind's full automation. 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:
theoremmySum_correct_vanilla(l:ArrayNat):mySuml=l.sum:=l:ArrayNat⊢ mySuml=l.sum-- Turn the array into a listcaseslwithl:ListNat⊢ mySum{toList:=l}={toList:=l}.sum-- Unfold `mySum` and rewrite `forIn` to `foldl`l:ListNat⊢ List.foldl(funba=>b+a)0l=l.sum-- Generalize the inductive hypothesismkl:ListNat⊢ ∀(out:Nat),List.foldl(funx1x2=>x1+x2)outl=out+l.sum-- Grind awayinductionlwithgrindAll goals completed! 🐙
This proof is similarly succinct as the proof in mySum_correct_shorter that uses vcgen.
However, the traditional approach relies on important properties of the program:
The Lean.Parser.Term.doFor : doElem`for x in e do s` iterates over `e` assuming `e`'s type has an instance of the `ForIn` typeclass.
`break` and `continue` are supported inside `for` loops.
`for x in e, x2 in e2, ... do s` iterates over the given collections in parallel,
until at least one of them is exhausted.
The types of `e2` etc. must implement the `Std.ToStream` typeclass.
for loop does not Lean.Parser.Term.doBreak : doElem`break` exits the surrounding `for` loop. break or Lean.Parser.Term.doReturn : doElem`return e` inside of a `do` block makes the surrounding block evaluate to `pure e`,
skipping any further statements.
Note that uses of the `do` keyword in other syntax like in `for _ in _ do`
do not constitute a surrounding block in this sense;
in supported editors, the corresponding `do` keyword of the surrounding block
is highlighted when hovering over `return`.
`return` not followed by a term starting on the same line is equivalent to `return ()`.
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 does not carry out any effects in the underlying monad (that is, the only effects are those introduced by Lean.Parser.Term.do : termdo-notation).
The Id monad has no effects, so all of its comptutations are 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 vcgen and its support library, and also see where traditional proofs become difficult.
This is usually caused by:
Lean.Parser.Term.do : termdo blocks using control flow constructs such as Lean.Parser.Term.doFor : doElem`for x in e do s` iterates over `e` assuming `e`'s type has an instance of the `ForIn` typeclass.
`break` and `continue` are supported inside `for` loops.
`for x in e, x2 in e2, ... do s` iterates over the given collections in parallel,
until at least one of them is exhausted.
The types of `e2` etc. must implement the `Std.ToStream` typeclass.
for loops, Lean.Parser.Term.doBreak : doElem`break` exits the surrounding `for` loop. breaks and early Lean.Parser.Term.doReturn : doElem`return e` inside of a `do` block makes the surrounding block evaluate to `pure e`,
skipping any further statements.
Note that uses of the `do` keyword in other syntax like in `for _ in _ do`
do not constitute a surrounding block in this sense;
in supported editors, the corresponding `do` keyword of the surrounding block
is highlighted when hovering over `return`.
`return` not followed by a term starting on the same line is equivalent to `return ()`.
return.
The use of effects in non-Id monads, which affects the implicit monadic context (state, exceptions) in ways that need to be reflected in loop invariants.
vcgen scales to these challenges with reasonable effort.
Let us consider another example that combines Lean.Parser.Term.doFor : doElem`for x in e do s` iterates over `e` assuming `e`'s type has an instance of the `ForIn` typeclass.
`break` and `continue` are supported inside `for` loops.
`for x in e, x2 in e2, ... do s` iterates over the given collections in parallel,
until at least one of them is exhausted.
The types of `e2` etc. must implement the `Std.ToStream` typeclass.
for loops with an early return.
List.Nodup is a predicate that asserts that a given list does not contain any duplicates.
The function nodup below decides this predicate:
This function is correct if it returns true for every list that satisfies List.Nodup and false for every list that does not.
Just as it was in mySum, the use of Lean.Parser.Term.do : termdo-notation and the Id monad is an internal implementation detail of nodup.
Thus, the proof begins by using Id.of_run_eq_wp to make the proof state amenable to vcgen:
-- A future 4.36 release adds these annotations to the library.attribute[localgrindnorm]Lean.Order.ofProp_prop_eqLean.Order.meet_prop_eq_andLean.Order.join_prop_eq_orLean.Order.iSup_prop_eq_existstheoremnodup_correct(l:ListInt):nodupl↔l.Nodup:=byl:ListInt⊢ nodupl=true↔l.Nodupgeneralizeh:nodupl=rl:ListIntr:Boolh:nodupl=r⊢ r=true↔l.NodupapplyId.of_run_eq_wphl:ListIntr:Boolh:nodupl=r⊢ wp(haveseen:=∅;dolet__s←forInl(none,seen)funx__s=>haveseen:=__s.snd;ifx∈seenthenpure(ForInStep.done(somefalse,seen))elsehaveseen:=seen.insertx;pure(ForInStep.yield(none,seen))have__r:OptionBool:=__s.fstmatch__rwith|somer=>purer|none=>puretrue)(funr=>r=true↔l.Nodup)()vcgeninvariants·Invariant.withEarlyReturnNewDo(onReturn:=funretseen=>ret=false∧¬l.Nodup)(onContinue:=funprefsuffseen=>(∀x,x∈seen↔x∈pref)∧pref.Nodup)withfinishAll goals completed! 🐙
The proof has the same structure as the one for mySum: grind discharges all verification conditions with its existing automation around List.Nodup, and only the loop invariant differs.
Since our loop has an early return, we construct the invariant using the helper function Invariant.withEarlyReturnNewDo, which supports the extensible do-notation elaborator.
This function allows us to specify the invariant in two parts:
onReturnretseen 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.
onContinueprefsuffseen is the invariant that holds after each regular iteration.
As in mySum, the list pref holds the elements that the loop has already visited and suff holds the elements that the loop has yet to visit.
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.
Now consider the following direct (and excessively golfed) proof without vcgen:
The proof is even shorter than the one with vcgen.
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 could pick up again.
The usual way to avoid replicating the control flow of a definition in a proof is to use the fun_cases or fun_induction tactics.
Unfortunately, fun_cases does not help with control flow inside a forIn application.
A vcgen-powered proof, on the other hand, never needs to copy any part of the original program.
The vcgen tactic ships with support for many forIn implementations, and a custom forIn implementation can be supported with a specification lemma that is annotated with @[spec].
Writing such a lemma takes considerable work, because a loop specification takes the invariant as a parameter and must prove that the loop preserves it.
A Std.Internal.PureForIn instance is less work: it states that the loop over a container is the loop over the list ForIn.toList of its elements.
With this instance, the loop specification that ships with vcgen applies to the container.
Compositional Reasoning About Effectful Programs Using Hoare Triples🔗
The previous examples reasoned about functions defined using Id.runLean.Parser.Term.do : termdo <prog> to make use of local mutability and early return in <prog>.
However, real-world programs often use Lean.Parser.Term.do : termdo 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 a monadic return type Mα and compose well with other functions of that return type.
In other words, the monad is part of the function's interface, not merely its implementation.
Here is an example involving a stateful function mkFresh that returns auto-incremented counter values:
mkFreshNn 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: the returned list of numbers should contain no duplicates.
In this proof, StateM.of_run'_eq_wp serves the same role that Id.of_run_eq_wp did in the preceding examples.
theoremmkFreshN_correct(n:Nat):((mkFreshNn).run's).Nodup:=bys:Supplyn:Nat⊢ List.Nodup(StateT.run'(mkFreshNn)s)-- Focus on `(mkFreshN n).run' s`.generalizeh:(mkFreshNn).run's=xs:Supplyn:Natx:Id(ListNat)h:StateT.run'(mkFreshNn)s=x⊢ List.NodupxapplyStateM.of_run'_eq_wphs:Supplyn:Natx:Id(ListNat)h:StateT.run'(mkFreshNn)s=x⊢ wp(mkFreshNn)(funax=>a.Nodup)()s-- Show something about monadic program `mkFresh n`.-- The `mkFreshN` and `mkFresh` arguments make `vcgen` unfold-- these definitions.vcgen[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.·funprefsuffaccstate=>(∀x∈acc,x<state.counter)∧acc.toList.NodupwithfinishAll goals completed! 🐙
This proof unfolds both mkFreshN and mkFresh.
In a larger program, many functions call mkFresh, and every proof about one of them like the above would unfold mkFresh again.
Instead, the behavior of mkFresh can be stated once, proved once, and then used at every call.
Proofs that use the specification of a function and never its code are called modular.
Hoare triples are the standard form of specifications for effectful programs, and they make such modular reasoning possible.
A Hoare triple consists of a precondition, a statement, and a postcondition; it asserts that if the precondition holds, then the postcondition holds after running the statement.
In Lean syntax, this is written ⦃P⦄prog⦃Q⦄, where P is the precondition, prog:mα is the statement, and Q is the postcondition.
P and Q are written in an assertion language that is determined by the specific monad m.Marginal noteIn particular, the monad's instance of the type class WP specifies the ways in which assertions may refer to the monad's state or the exceptions it may throw.
An explicit exception postcondition can be supplied after a semicolon, as in ⦃ P ⦄ prog ⦃ Q; E ⦄.
When the exception postcondition is omitted, it defaults to the bottom assertion ⊥, which asserts that the program throws no exception.
The section “Exceptions” below shows postconditions for programs that throw.
One suitable specification for mkFreshN as a Hoare triple is this translation of mkFreshN_correct:
⦃fun_=>True⦄mkFreshNn⦃funr_=>r.Nodup⦄
For StateM programs, an assertion is a predicate on the state, and a postcondition additionally takes the result value as its first argument.
The precondition fun(_:Supply)=>True imposes no requirements on the state in which the program is called.
The postcondition states that the result value is a list with no duplicate elements, regardless of the final state.
A specification for the single-step mkFresh describes its effects on the monad's state:
The universally quantified c is a logical variable: it relates the initial state to the result and to the final state.
This Hoare triple states:
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.
A triple ⦃P⦄prog⦃Q⦄ means P⊑wpprogQ⊥: the precondition entails the weakest precondition from the beginning of this tutorial.
The weakest precondition semantics wp interprets programs as predicate transformers that map postconditions to preconditions.
The Hoare triple syntax is notation for Std.WP.Triple, which is defined as follows, with some binders elided:
structure Triple ... (x : Prog) [WP Prog Value Pred EPred]
(pre : Pred) (post : Value → Pred) (epost : EPred) : Prop where
intro ::
le_wp : pre ⊑ wp x post epost
The WP type class interprets a program type Prog with result values of type Value as a predicate transformer over an assertion type Pred and an exception assertion type EPred.
Many of the standard monads and 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.
The entailment ⊑ in the definition is the order of the assertion lattice; for StateM it is pointwise implication.
For StateM programs, the following type is equivalent to Std.WP.Triple:
Specifications as Hoare triples are compositional because they allow statements to be sequenced.
Given ⦃P⦄stmt1⦃fun_=>R⦄ and ⦃R⦄stmt2⦃Q⦄, it follows that ⦃P⦄(dostmt1;stmt2)⦃Q⦄.
Just as proofs about ordinary functions can rely on lemmas about the functions that they call, proofs about monadic programs can use lemmas that are specified in terms of Hoare triples.
The specifications from the previous section become specification lemmas.
A specification lemma is a Hoare triple that is automatically used during verification condition generation to obtain the pre- and postconditions of each statement in a Lean.Parser.Term.do : termdo-block.
When the system cannot automatically prove that the postcondition of one statement implies the precondition of the next, then this missing reasoning step becomes a verification condition.
Specification lemmas can either be passed as arguments to vcgen or registered in a global (or scoped, or local) database of specifications using the @[spec] attribute:
@[spec]theoremmkFresh_spec(c:Nat):⦃funstate=>state.counter=c⦄mkFresh⦃funrstate=>r=c∧c<state.counter⦄:=byc:Nat⊢ ⦃funstate=>state.counter=c⦄mkFresh⦃funrstate=>r=c∧c<state.counter⦄-- Unfold `mkFresh` and blast away:vcgen[mkFresh]withfinishAll goals completed! 🐙@[spec]theoremmkFreshN_spec(n:Nat):⦃fun_=>True⦄mkFreshNn⦃funr_=>r.Nodup⦄:=byn:Nat⊢ ⦃funx=>True⦄mkFreshNn⦃funrx=>r.Nodup⦄-- `vcgen [mkFreshN, mkFresh_spec]` if `mkFresh_spec` were not-- registered with `@[spec]`vcgen[mkFreshN]invariants-- As before:·funprefsuffaccstate=>(∀x∈acc,x<state.counter)∧acc.toList.NodupwithfinishAll goals completed! 🐙
The original correctness theorem can now be proved using vcgen alone:
Real-world programs often use monads that are built from multiple monad transformers, with operations being frequently lifted from one monad to another.
Verification of these programs requires taking this into account.
We can tweak the previous example to demonstrate this.
Now, there is an application with two separate monads, both built using transformers:
The specification lemmas change only in two places: their pure parts are wrapped in corner brackets, and the invariant takes an extra argument for the Bool state of AppM:
The corner brackets ⌜p⌝ embed a proposition p into the assertion language as a pure assertion, which holds independently of the state.
In CounterM, an assertion takes the Supply state and then the String environment of the reader.
The precondition of mkFresh_spec binds only the state, and ⌜state.counter = c⌝ is a pure assertion about the environment: it holds for every environment if state.counter = c holds.
These proofs might not look much more exciting than when only a single monad was involved.
However, under the radar of the user the proofs build on a cascade of specifications for MonadLift instances.
They also build on the WPMonad instances of StateT and ReaderT, which make the specifications of pure and bind available in every layer.
A WPMonad instance requires only that the weakest precondition of pure and bind is at least as weak as the one that the predicate transformer monad computes.
In other words, wp is a lax morphism of ordered monads.
If let mut is the Lean.Parser.Term.do : termdo-equivalent of StateT, then early Lean.Parser.Term.doReturn : doElem`return e` inside of a `do` block makes the surrounding block evaluate to `pure e`,
skipping any further statements.
Note that uses of the `do` keyword in other syntax like in `for _ in _ do`
do not constitute a surrounding block in this sense;
in supported editors, the corresponding `do` keyword of the surrounding block
is highlighted when hovering over `return`.
`return` not followed by a term starting on the same line is equivalent to `return ()`.
return is the equivalent of ExceptT.
We have seen how vcgen copes with StateT; here we will look at the program logic's support for ExceptT.
Exceptions are the reason why a triple carries an exception postcondition next to the success postcondition.
To see why, suppose there were only the success postcondition, and suppose that program prog throws an exception in a prestate satisfying P.
Should we be able to prove ⦃P⦄prog⦃Q'⦄?
There is no result value, so it is unclear what this proof means for Q'!
So there are two reasonable options, inspired by non-termination in traditional program logics:
⦃P⦄prog⦃Q';⊤⦄ asserts that, given P holds, and ifprog terminates normally thenQ' holds for the result.
A triple without an explicit exception postcondition carries the bottom assertion (⊥:EPred) and thus has the total interpretation: an exception would have to establish ⊥, so no exception can be thrown.
The top assertion (⊤:EPred) permits every exception and yields the partial interpretation.
In the running example, ⦃P⦄prog⦃Q'⦄ is unprovable, but ⦃P⦄prog⦃Q';⊤⦄ is trivially provable.
Between (⊥:EPred) and (⊤:EPred), the exception postcondition expresses a spectrum of correctness properties.
For example, suppose that our Supply of fresh numbers is bounded and we want to throw an exception if the supply is exhausted.
Then mkFresh should throw an exception only if the supply is indeed exhausted, as in this implementation:
The exception postcondition after the semicolon applies when mkFresh throws.
The monad's WP instance determines the types of both postconditions: the exception postcondition takes the thrown exception in place of the result value, and each state gives an extra parameter.
In this new monad, mkFreshN's implementation is unchanged, except for the type signature:
A StateTσ layer turns the assertion type Pred into σ → Pred and leaves the exception postcondition type unchanged.
An ExceptTε layer leaves the assertion type unchanged and turns the exception postcondition type EPred into (ε→Pred)×EPred, adding one exception branch of type ε → Pred.
The WP instance for EStateMεσ uses assertions of type σ → Prop and exception postconditions of type ε → σ → Prop, mirroring the single exception branch of the equivalent transformer stack ExceptTε(StateMσ).
The vcgen framework is designed to be extensible.
None of the monads presented so far have in any way been hard-coded into vcgen.
Rather, vcgen relies on instances of the WP and WPMonad type classes and user-provided specifications to generate verification conditions.
Every program type needs a WP instance, and a monad additionally needs a WPMonad instance.
This section develops both for a custom monad.
The WP instance defines the weakest precondition interpretation of a monad m into predicate transformers PredTransPredEPredα,
and the matching WPMonad instance asserts that this translation is sound for pure and bind.
Suppose one wants to use vcgen to generate verification conditions for programs generated by Aeneas.
Aeneas translates Rust programs into Lean programs in the following Result monad:
inductiveErrorwhere|integerOverflow:Error-- ... more error kinds ...inductiveResult(α:Typeu)where|ok(v:α):Resultα|fail(e:Error):Resultα|div
There are both MonadResult and LawfulMonadResult instances.
Supporting this monad in vcgen is a matter of:
Adding WP and WPMonad instances for Result
Registering specification lemmas for the translation of basic Rust primitives such as addition etc.
The WP instance for Result specifies the assertion type Prop because there are no state-like effects, and the exception postcondition type Error→Prop because there is a single exception of type Error.
The WP instance translates programs in Resultα to predicate transformers in PredTransProp(Error→Prop)α.
That is, a function mapping a postcondition and an exception postcondition to the weakest precondition.
The implementation of WP.wp is similar to that of ExceptError.
Each case of Result determines the precondition directly:
Result.okv yields the success postcondition at v.
Result.faile yields the exception postcondition at e.
Result.div yields False, meaning that all specifications assert that the program never diverges.
The WPMonad instance proves that this interpretation is sound for pure and bind, in its fields WPMonad.pure_le_wp_pure and WPMonad.bind_le_wp_bind.
For bind, both the definition of wp and the definition of bind need to be unfolded to expose the nested match structure that a case split and simp make short work of.
The definition of the WP interpretation determines what properties can be derived from proved specifications via Result.of_eq_wp.
This lemma connects the weakest precondition to the result of running the program.
The second step registers specification lemmas for the Rust primitives.
As an example, here is a definition of UInt32 addition in Result that models integer overflow:
It is a priority of vcgen to break down programs into verification conditions that are straightforward to understand.
The VCs are ordinary Lean goals, so any tactic can discharge them.
When the assertion language is concrete, as for Id or StateM programs, the goals speak directly about result values and states, and grind applies.
During VC generation, vcgen internalizes the goal context into grind's E-graph.
The with clause runs a single grind-mode step on every remaining VC inside that shared context, which avoids re-internalizing the context for every VC.
The step finish closes a VC with grind's full automation.