A predicate indexed by a list of states.
example : SPred [Nat, Bool] = (Nat → Bool → ULift Prop) := rfl
A predicate transformer semantics is an interpretation of programs as functions from predicates to predicates, rather than values to values. A postcondition is an assertion that holds after running a program, while a precondition is an assertion that must hold prior to running the program in order for the postcondition to be guaranteed to hold.
The predicate transformer semantics used by mvcgen transforms postconditions into the weakest preconditions under which the program will ensure the postcondition.
An assertion P is weaker than P' if, in all states, P' suffices to prove P, but P does not suffice to prove P'.
Logically equivalent assertions are considered to be equal.
The predicates in question are stateful: they can mention the program's current state.
Furthermore, postconditions can relate the return value and any exceptions thrown by the program to the final state.
SPred is a type of predicates that is parameterized over a monadic state, expressed as a list of the types of the fields that make up the state.
The usual logical connectives and quantifiers are defined for SPred.
Each monad that can be used with mvcgen is assigned a state type by an instance of WP, and Assertion is the corresponding type of assertions for that monad, which is used for preconditions.
Assertion is a wrapper around SPred: while SPred is parameterized by a list of states types, Assertion is parameterized by a more informative type that it translates to a list of state types for SPred.
A PostCond pairs an Assertion about a return value with assertions about potential exceptions; the available exceptions are also specified by the monad's WP instance.
The predicate transformer semantics of monadic programs is based on a logic in which propositions may mention the program's state.
Here, “state” refers not only to mutable state, but also to read-only values such as those that are provided via ReaderT.
Different monads have different state types available, but each individual state always has a type.
Given a list of state types, SPred is a type of predicates over these states.
SPred is not inherently tied to the monadic verification framework.
The related Assertion computes a suitable SPred for a monad's state as expressed via its WP instance's PostShape output parameter.
A predicate indexed by a list of states.
example : SPred [Nat, Bool] = (Nat → Bool → ULift Prop) := rfl
Ordinary propositions that do not mention the state can be used as stateful predicates by adding a trivial universal quantification.
This is written with the syntax ⌜P⌝, which is syntactic sugar for SPred.pure.
SPredterm ::= ...
| Embedding of pure Lean values into `SVal`. An alias for `SPred.pure`. ⌜term⌝
Embedding of pure Lean values into SVal. An alias for SPred.pure.
The predicate ItIsSecret expresses that a state of type String is "secret":
def ItIsSecret : SPred [String] := fun s => ⌜s = "secret"⌝
Stateful predicates are related by entailment.
Entailment of stateful predicates is defined as universally-quantified implication: if P and Q are predicates over a state \sigma, then P entails Q (written P \vdash_s Q) when ∀ s : \sigma, P(s) → Q(s).
SPredterm ::= ...
| Entailment in `SPred`; sugar for `SPred.entails`. term ⊢ₛ term
Entailment in SPred; sugar for SPred.entails.
term ::= ...
| Tautology in `SPred`; sugar for `SPred.entails ⌜True⌝`. ⊢ₛ term
Tautology in SPred; sugar for SPred.entails ⌜True⌝.
term ::= ...
| Bi-entailment in `SPred`; sugar for `SPred.bientails`. term ⊣⊢ₛ term
Bi-entailment in SPred; sugar for SPred.bientails.
The logic of stateful predicates includes an implication connective.
The difference between entailment and implication is that entailment is a statement in Lean's logic, while implication is internal to the stateful logic.
Given stateful predicates P and Q for state σ, P ⊢ₛ Q is a Prop while spred(P → Q) is an SPred σ.
The syntax of stateful predicates overlaps with that of ordinary Lean terms.
In particular, stateful predicates use the usual syntax for logical connectives and quantifiers.
The syntax associated with stateful predicates is automatically enabled in contexts such as pre- and postconditions where they are clearly intended; other contexts must explicitly opt in to the syntax using Std.Do.«termSpred(_)» : termspred.
The usual meanings of these operators can be recovered by using the Std.Do.«termTerm(_)» : termterm operator.
Std.Do.«termSpred(_)» : termspred indicates that logical connectives and quantifiers should be understood as those pertaining to stateful predicates, while Std.Do.«termTerm(_)» : termterm indicates that they should have the usual meaning.
term ::= ... | spred(term)
term ::= ... | 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)
Syntactic sugar for SPred.and.
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)
Syntactic sugar for SPred.or.
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)
Syntactic sugar for SPred.not.
term ::= ... | spred(term → term)
Syntactic sugar for SPred.imp.
term ::= ...
| spred(If and only if, or logical bi-implication. `a ↔ b` means that `a` implies `b` and vice versa.
By `propext`, this implies that `a` and `b` are equal and hence any expression involving `a`
is equivalent to the corresponding expression with `b` instead.
Conventions for notations in identifiers:
* The recommended spelling of `↔` in identifiers is `iff`.term ↔ term)
Syntactic sugar for SPred.iff.
term ::= ... | spred(∀ ident, term)
term ::= ... | spred(∀ ident : term, term)
term ::= ... | spred(∀(ident (ident |Explicit binder, like `(x y : A)` or `(x y)`. Default values can be specified using `(x : A := v)` syntax, and tactics using `(x : A := by tac)`.hole)* : term), term)A *hole* (or *placeholder term*), which stands for an unknown term that is expected to be inferred based on context. For example, in `@id _ Nat.zero`, the `_` must be the type of `Nat.zero`, which is `Nat`. The way this works is that holes create fresh metavariables. The elaborator is allowed to assign terms to metavariables while it is checking definitional equalities. This is often known as *unification*. Normally, all holes must be solved for. However, there are a few contexts where this is not necessary: * In `match` patterns, holes are catch-all patterns. * In some tactics, such as `refine'` and `apply`, unsolved-for placeholders become new goals. Related concept: implicit parameters are automatically filled in with holes during the elaboration process. See also `?m` syntax (synthetic holes).
term ::= ...
| spred(∀ A *hole* (or *placeholder term*), which stands for an unknown term that is expected to be inferred based on context.
For example, in `@id _ Nat.zero`, the `_` must be the type of `Nat.zero`, which is `Nat`.
The way this works is that holes create fresh metavariables.
The elaborator is allowed to assign terms to metavariables while it is checking definitional equalities.
This is often known as *unification*.
Normally, all holes must be solved for. However, there are a few contexts where this is not necessary:
* In `match` patterns, holes are catch-all patterns.
* In some tactics, such as `refine'` and `apply`, unsolved-for placeholders become new goals.
Related concept: implicit parameters are automatically filled in with holes during the elaboration process.
See also `?m` syntax (synthetic holes).
_, term)term ::= ...
| spred(∀ A *hole* (or *placeholder term*), which stands for an unknown term that is expected to be inferred based on context.
For example, in `@id _ Nat.zero`, the `_` must be the type of `Nat.zero`, which is `Nat`.
The way this works is that holes create fresh metavariables.
The elaborator is allowed to assign terms to metavariables while it is checking definitional equalities.
This is often known as *unification*.
Normally, all holes must be solved for. However, there are a few contexts where this is not necessary:
* In `match` patterns, holes are catch-all patterns.
* In some tactics, such as `refine'` and `apply`, unsolved-for placeholders become new goals.
Related concept: implicit parameters are automatically filled in with holes during the elaboration process.
See also `?m` syntax (synthetic holes).
_ : term, term)term ::= ... | spred(∀(Explicit binder, like `(x y : A)` or `(x y)`. Default values can be specified using `(x : A := v)` syntax, and tactics using `(x : A := by tac)`._ (ident |A *hole* (or *placeholder term*), which stands for an unknown term that is expected to be inferred based on context. For example, in `@id _ Nat.zero`, the `_` must be the type of `Nat.zero`, which is `Nat`. The way this works is that holes create fresh metavariables. The elaborator is allowed to assign terms to metavariables while it is checking definitional equalities. This is often known as *unification*. Normally, all holes must be solved for. However, there are a few contexts where this is not necessary: * In `match` patterns, holes are catch-all patterns. * In some tactics, such as `refine'` and `apply`, unsolved-for placeholders become new goals. Related concept: implicit parameters are automatically filled in with holes during the elaboration process. See also `?m` syntax (synthetic holes).hole)* : term), term)A *hole* (or *placeholder term*), which stands for an unknown term that is expected to be inferred based on context. For example, in `@id _ Nat.zero`, the `_` must be the type of `Nat.zero`, which is `Nat`. The way this works is that holes create fresh metavariables. The elaborator is allowed to assign terms to metavariables while it is checking definitional equalities. This is often known as *unification*. Normally, all holes must be solved for. However, there are a few contexts where this is not necessary: * In `match` patterns, holes are catch-all patterns. * In some tactics, such as `refine'` and `apply`, unsolved-for placeholders become new goals. Related concept: implicit parameters are automatically filled in with holes during the elaboration process. See also `?m` syntax (synthetic holes).
Each form of universal quantification is syntactic sugar for an invocation of SPred.forall on a function that takes the quantified variable as a parameter.
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)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, term)term ::= ... | spred(∃ (ident`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.binderIdent* : term), term)`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.
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._, term)A *hole* (or *placeholder term*), which stands for an unknown term that is expected to be inferred based on context. For example, in `@id _ Nat.zero`, the `_` must be the type of `Nat.zero`, which is `Nat`. The way this works is that holes create fresh metavariables. The elaborator is allowed to assign terms to metavariables while it is checking definitional equalities. This is often known as *unification*. Normally, all holes must be solved for. However, there are a few contexts where this is not necessary: * In `match` patterns, holes are catch-all patterns. * In some tactics, such as `refine'` and `apply`, unsolved-for placeholders become new goals. Related concept: implicit parameters are automatically filled in with holes during the elaboration process. See also `?m` syntax (synthetic holes).
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._ : term, term)A *hole* (or *placeholder term*), which stands for an unknown term that is expected to be inferred based on context. For example, in `@id _ Nat.zero`, the `_` must be the type of `Nat.zero`, which is `Nat`. The way this works is that holes create fresh metavariables. The elaborator is allowed to assign terms to metavariables while it is checking definitional equalities. This is often known as *unification*. Normally, all holes must be solved for. However, there are a few contexts where this is not necessary: * In `match` patterns, holes are catch-all patterns. * In some tactics, such as `refine'` and `apply`, unsolved-for placeholders become new goals. Related concept: implicit parameters are automatically filled in with holes during the elaboration process. See also `?m` syntax (synthetic holes).
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._A *hole* (or *placeholder term*), which stands for an unknown term that is expected to be inferred based on context. For example, in `@id _ Nat.zero`, the `_` must be the type of `Nat.zero`, which is `Nat`. The way this works is that holes create fresh metavariables. The elaborator is allowed to assign terms to metavariables while it is checking definitional equalities. This is often known as *unification*. Normally, all holes must be solved for. However, there are a few contexts where this is not necessary: * In `match` patterns, holes are catch-all patterns. * In some tactics, such as `refine'` and `apply`, unsolved-for placeholders become new goals. Related concept: implicit parameters are automatically filled in with holes during the elaboration process. See also `?m` syntax (synthetic holes).binderIdent* : term), term)`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.
Each form of existential quantification is syntactic sugar for an invocation of SPred.exists on a function that takes the quantified variable as a parameter.
Just as SPred represents predicate over states, SVal represents a value that is derived from a state.
A value indexed by a curried tuple of states.
example : SVal [Nat, Bool] String = (Nat → Bool → String) := rfl
The language of assertions about monadic programs is parameterized by a postcondition shape, which describes the inputs to and outputs from a computation in a given monad.
Preconditions may mention the initial values of the monad's state, while postconditions may mention the returned value, the final values of the monad's state, and must furthermore account for any exceptions that could have been thrown.
The postcondition shape of a given monad determines the states and exceptions in the monad.
PostShape.pure describes a monad in which assertions may not mention any states, PostShape.arg describes a state value, and PostShape.except describes a possible exception.
Because these constructors can be continually added, the postcondition shape of a monad transformer can be defined in terms of the postcondition shape of the underlying transformed monad.
Behind the scenes, an Assertion is translated into an appropriate SPred by translating the postcondition shape into a list of state types, discarding exceptions.
Std.Do.PostShape.{u} : Type (u + 1)Std.Do.PostShape.{u} : Type (u + 1)
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.
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
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
Syntactic sugar for a nested sequence of product constructors, terminating in (), in which the first element is an assertion about non-exceptional return values and the remaining elements are assertions about the exceptional cases for a postcondition.
Encodes one continuation barrel for each PostShape.except in the given predicate shape.
example : ExceptConds (.pure) = Unit := rfl example : ExceptConds (.except ε .pure) = ((ε → ULift Prop) × Unit) := rfl example : ExceptConds (.arg σ (.except ε .pure)) = ((ε → ULift Prop) × Unit) := rfl example : ExceptConds (.except ε (.arg σ .pure)) = ((ε → σ → ULift Prop) × Unit) := rfl
Postconditions for programs that might throw exceptions come in two varieties. The total correctness interpretation ⦃P⦄ prog ⦃⇓ r => Q' r⦄ asserts that, given P holds, then prog terminates and Q' holds for the result. The partial correctness interpretation ⦃P⦄ prog ⦃⇓? r => Q' r⦄ asserts that, given P holds, and if prog terminates then Q' holds for the result.
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
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.
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 ::= ...
| 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
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.
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.
A predicate transformer is a function from postconditions for some postcondition state into assertions for that state.
The function must be conjunctive, which means it must distribute over PostCond.and.
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.
Std.Do.PredTrans.mk.{u}
apply : PostCond α ps → Assertion ps
Apply the predicate transformer to a postcondition.
conjunctive : PredTrans.Conjunctive self.apply
The predicate transformer is conjunctive: t (Q₁ ∧ₚ Q₂) ⊣⊢ₛ t Q₁ ∧ t Q₂.
So the stronger the postcondition, the stronger the resulting precondition.
Transforming a conjunction of postconditions is the same as the conjunction of transformed postconditions.
The stronger the postcondition, the stronger the transformed precondition.
Predicate transformers form a monad.
The pure operator is the identity transformer; it simply instantiates the postcondition with the its argument.
The bind operator composes predicate transformers.
The helper operators PredTrans.pushArg, PredTrans.pushExcept, and PredTrans.pushOption modify a predicate transformer by adding a standard side effect.
They are used to implement the WP instances for transformers such as StateT, ExceptT, and OptionT; they can also be used to implement monads that can be thought of in terms of one of these.
For example, PredTrans.pushArg is typically used for state monads, but can also be used to implement a reader monad's instance, treating the reader's value as read-only state.
Std.Do.PredTrans.pushArg.{u} {ps : PostShape} {α σ : Type u} (x : StateT σ (PredTrans ps) α) : PredTrans (PostShape.arg σ ps) αStd.Do.PredTrans.pushArg.{u} {ps : PostShape} {α σ : Type u} (x : StateT σ (PredTrans ps) α) : PredTrans (PostShape.arg σ ps) α
Std.Do.PredTrans.pushExcept.{u_1} {ps : PostShape} {α ε : Type u_1} (x : ExceptT ε (PredTrans ps) α) : PredTrans (PostShape.except ε ps) αStd.Do.PredTrans.pushExcept.{u_1} {ps : PostShape} {α ε : Type u_1} (x : ExceptT ε (PredTrans ps) α) : PredTrans (PostShape.except ε ps) α
The weakest precondition semantics of a monad are provided by the WP type class.
Instances of WP determine the monad's postcondition shape and provide the logical rules for interpreting the monad's operations as a predicate transformer in its postcondition shape.
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.
Std.Do.WP.mk.{u, v}
wp : {α : Type u} → m α → PredTrans ps α
Interpret a monadic program x : m α in terms of a predicate transformer PredTrans ps α.
term ::= ...
| `wp⟦x⟧ Q` is defined as `(WP.wp x).apply Q`. wp⟦term (: term)?⟧
wp⟦x⟧ Q is defined as (WP.wp x).apply Q.
Most of the built-in specification lemmas for mvcgen relies on the presence of a WPMonad instance, in addition to the WP instance.
In addition to being lawful, weakest preconditions of the monad's implementations of pure and bind should correspond to the pure and bind operators for the predicate transformer monad.
Without a WPMonad instance, mvcgen typically returns the original proof goal unchanged.
A WP that is also a monad morphism, preserving pure and bind. (They all are.)
Std.Do.WPMonad.mk.{u, v}
map_const : ∀ {α β : Type u}, Functor.mapConst = Functor.map ∘ Function.const β
LawfulMonad mWP m psid_map : ∀ {α : Type u} (x : m α), id <$> x = x
LawfulMonad mWP m pscomp_map : ∀ {α β γ : Type u} (g : α → β) (h : β → γ) (x : m α), (h ∘ g) <$> x = h <$> g <$> x
LawfulMonad mWP m psseqLeft_eq : ∀ {α β : Type u} (x : m α) (y : m β), x <* y = Function.const β <$> x <*> y
LawfulMonad mWP m psseqRight_eq : ∀ {α β : Type u} (x : m α) (y : m β), x *> y = Function.const α id <$> x <*> y
LawfulMonad mWP m pspure_seq : ∀ {α β : Type u} (g : α → β) (x : m α), pure g <*> x = g <$> x
LawfulMonad mWP m psmap_pure : ∀ {α β : Type u} (g : α → β) (x : α), g <$> pure x = pure (g x)
LawfulMonad mWP m psseq_pure : ∀ {α β : Type u} (g : m (α → β)) (x : α), g <*> pure x = (fun h => h x) <$> g
LawfulMonad mWP m psseq_assoc : ∀ {α β γ : Type u} (x : m α) (g : m (α → β)) (h : m (β → γ)), h <*> (g <*> x) = Function.comp <$> h <*> g <*> x
LawfulMonad mWP m psbind_pure_comp : ∀ {α β : Type u} (f : α → β) (x : m α), (do let a ← x pure (f a)) = f <$> x
LawfulMonad mWP m psbind_map : ∀ {α β : Type u} (f : m (α → β)) (x : m α), (do let x_1 ← f x_1 <$> x) = f <*> x
LawfulMonad mWP m pspure_bind : ∀ {α β : Type u} (x : α) (f : α → m β), pure x >>= f = f x
LawfulMonad mWP m psbind_assoc : ∀ {α β γ : Type u} (x : m α) (f : α → m β) (g : β → m γ), x >>= f >>= g = x >>= fun x => f x >>= g
LawfulMonad mWP m pswp : {α : Type u} → m α → PredTrans ps α
LawfulMonad mWP m pswp_pure : ∀ {α : Type u} (a : α), wp (pure a) = pure a
wp_bind : ∀ {α β : Type u} (x : m α) (f : α → m β), (wp do let a ← x f a) = do let a ← wp x wp (f a)
WPMonad Instance
This reimplementation of Id has a WP instance, but no WPMonad instance:
def Identity (α : Type u) : Type u := α
variable {α : Type u}
def Identity.run (act : Identity α) : α := act
instance : Monad Identity where
pure x := x
bind x f := f x
instance : WP Identity .pure where
wp x := PredTrans.pure x
theorem Identity.of_wp_run_eq {x : α} {prog : Identity α}
(h : Identity.run prog = x) (P : α → Prop) :
(⊢ₛ wp⟦prog⟧ (⇓ a => ⟨P a⟩)) → P x := α:Type ux:αprog:Identity αh:prog.run = xP:α → Prop⊢ (⊢ₛ wp⟦prog⟧ (PostCond.noThrow fun a => { down := P a })) → P x
α:Type ux:αprog:Identity αh:prog.run = xP:α → Proph':⊢ₛ wp⟦prog⟧ (PostCond.noThrow fun a => { down := P a })⊢ P x
All goals completed! 🐙
The missing instance prevents mvcgen from using its specifications for pure and bind.
This tends to show up as a verification condition that's equal to the original goal.
This function that reverses a list:
def rev (xs : List α) : Identity (List α) := do
let mut out := []
for x in xs do
out := x :: out
return out
It is correct if it is equal to List.reverse.
However, mvcgen does not make the goal easier to prove:
theorem rev_correct :
(rev xs).run = xs.reverse := α✝:Type u_1xs:List α✝⊢ (rev xs).run = xs.reverse
α✝:Type u_1xs:List α✝x:List α✝h:(rev xs).run = x⊢ x = xs.reverse
α✝:Type u_1xs:List α✝x:List α✝h:(rev xs).run = x⊢ ⊢ₛ wp⟦rev xs⟧ (PostCond.noThrow fun a => { down := a = xs.reverse })
α✝:Type u_1xs:List α✝x:List α✝h:(rev xs).run = xout✝:List α✝ := []⊢ (wp⟦do
let r ←
forIn xs out✝ fun x r => do
pure PUnit.unit
pure (ForInStep.yield (x :: r))
pure r⟧
(PostCond.noThrow fun a => { down := a = xs.reverse })).down
When the verification condition is just the original problem, without even any simplification of bind, the problem is usually a missing WPMonad instance.
The issue can be resolved by adding a suitable instance:
instance : WPMonad Identity .pure where
wp_pure _ := rfl
wp_bind _ _ := rfl
With this instance, and a suitable invariant, mvcgen and grind can prove the theorem.
theorem rev_correct :
(rev xs).run = xs.reverse := α✝:Type u_1xs:List α✝⊢ (rev xs).run = xs.reverse
α✝:Type u_1xs:List α✝x:List α✝h:(rev xs).run = x⊢ x = xs.reverse
α✝:Type u_1xs:List α✝x:List α✝h:(rev xs).run = x⊢ ⊢ₛ wp⟦rev xs⟧ (PostCond.noThrow fun a => { down := a = xs.reverse })
α✝:Type u_1xs:List α✝x:List α✝h:(rev xs).run = x⊢ ⊢ₛ
wp⟦do
let r ←
forIn xs [] fun x r => do
pure PUnit.unit
pure (ForInStep.yield (x :: r))
pure r⟧
(PostCond.noThrow fun a => { down := a = xs.reverse })
mvcgen invariants
· ⇓⟨xs, out⟩ =>
⌜out = xs.prefix.reverse⌝
with All goals completed! 🐙
Monads that can be invoked from pure code typically provide a invocation operator that takes any required input state as a parameter and returns either a value paired with an output state or some kind of exceptional value.
Examples include StateT.run, ExceptT.run, and Id.run.
Adequacy lemmas provide a bridge between statements about invocations of monadic programs and those programs' weakest precondition semantics as given by their WP instances.
They show that a property about the invocation is true if its weakest precondition is true.
Adequacy lemma for EStateM.run.
Useful if you want to prove a property about an expression x defined as EStateM.run prog s and
you want to use mvcgen to reason about prog.
A Hoare triple (Hoare, 1969)C. A. R. Hoare (1969). “An Axiomatic Basis for Computer Programming”. Communications of the ACM.12 10pp. 576–583. consists of a precondition, a program, and a postcondition. Running the program in a state for which the precondition is true results in a state where the postcondition is true.
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 ⦄
⦃P⦄ x ⦃Q⦄ is syntactic sugar for Triple x P Q.
Conjunction for two Hoare triple specifications of a program x.
This theorem is useful for decomposing proofs, because unrelated facts about x can be proven
separately and then combined with this theorem.
Modus ponens for two Hoare triple specifications of a program x.
This theorem is useful for separating proofs. If h₁ : Triple x P₁ Q₁ proves a basic property about
x and h₂ : Triple x P₂ (Q₁ →ₚ Q₂) is an advanced proof for Q₂ that builds on the basic proof
for Q₁, then mp x h₁ h₂ is a proof for Q₂ about x.
Specification lemmas are designated theorems that associate Hoare triples with functions.
When mvcgen encounters a function, it checks whether there are any registered specification lemmas and attempts to use them to discharge intermediate verification conditions.
If there is no applicable specification lemma, then the connection between the statement's pre- and postconditions will become a verification condition.
Specification lemmas allow compositional reasoning about libraries of monadic code.
When applied to a theorem whose statement is a Hoare triple, the spec attribute registers the theorem as a specification lemma.
These lemmas are used in order of priority.
For theorems, the simpPre↓, simpPost↑, and ← specifiers are ignored.
The spec attribute may also be applied to definitions.
On definitions, it indicates that the definition should be unfolded during verification condition generation.
For definitions, spec uses the simpPre↓, simpPost↑, and ← specifiers in the same manner as simp.
attr ::= ... |spec (Theorems tagged with the `spec` attribute are used by the `mspec` and `mvcgen` tactics. * When used on a theorem `foo_spec : Triple (foo a b c) P Q`, then `mspec` and `mvcgen` will use `foo_spec` as a specification for calls to `foo`. * Otherwise, when used on a definition that `@[simp]` would work on, it is added to the internal simp set of `mvcgen` that is used within `wp⟦·⟧` contexts to simplify match discriminants and applications of constants.simpPre? |Use this rewrite rule before entering the subtermssimpPost?) (← ? | <- ?) prio?Use this rewrite rule after entering the subterms
Theorems tagged with the spec attribute are used by the mspec and mvcgen tactics.
When used on a theorem foo_spec : Triple (foo a b c) P Q, then mspec and mvcgen will use
foo_spec as a specification for calls to foo.
Otherwise, when used on a definition that @[simp] would work on, it is added to the internal
simp set of mvcgen that is used within wp⟦·⟧ contexts to simplify match discriminants and
applications of constants.
Universally-quantified variables in specification lemmas can be used to relate input states to output states and return values. These variables are referred to as schematic variables.
The function double doubles the value of a Nat state:
def double : StateM Nat Unit := do
modify (2 * ·)
Its specification should relate the initial and final states, but it cannot know their precise values. The specification uses a schematic variable to stand for the initial state:
theorem double_spec :
⦃ fun s => ⌜s = n⌝ ⦄ double ⦃ ⇓ () s => ⌜s = 2 * n⌝ ⦄ := n:Nat⊢ ⦃fun s => ⌜s = n⌝⦄ double ⦃PostCond.noThrow fun x s => ⌜s = 2 * n⌝⦄
n:Nat⊢ ⦃fun s => ⌜s = n⌝⦄ modify fun x => 2 * x ⦃PostCond.noThrow fun x s => ⌜s = 2 * n⌝⦄
mvcgen with All goals completed! 🐙
The assertion in the precondition is a function because the PostShape of StateM Nat is .arg Nat .pure, and Assertion (.arg Nat .pure) is SPred [Nat].
These types are used in invariants.
The specification lemmas for ForIn.forIn and ForIn'.forIn' take parameters of type Invariant, and mvcgen ensures that invariants are not accidentally generated by other automation.
The type of loop invariants used by the specifications of for ... in ... loops.
A loop invariant is a PostCond that takes as parameters
A List.Cursor xs representing the iteration state of the loop. It is parameterized by the list
of elements xs that the for loop iterates over.
A state tuple of type β, which will be a nesting of MProds representing the elaboration of
let mut variables and early return.
The loop specification lemmas will use this in the following way:
Before entering the loop, the cursor's prefix is empty and the suffix is xs.
After leaving the loop, the cursor's prefix is xs and the suffix is empty.
During the induction step, the invariant holds for a suffix with head element x.
After running the loop body, the invariant then holds after shifting x to the prefix.
Std.Do.Invariant.withEarlyReturn.{u₁, u₂} {β : Type (max u₁ u₂)} {ps : PostShape} {α✝ : Type (max u₁ u₂)} {xs : List α✝} {γ : Type (max u₁ u₂)} (onContinue : xs.Cursor → β → Assertion ps) (onReturn : γ → β → Assertion ps) (onExcept : ExceptConds ps := ExceptConds.false) : Invariant xs (MProd (Option γ) β) psStd.Do.Invariant.withEarlyReturn.{u₁, u₂} {β : Type (max u₁ u₂)} {ps : PostShape} {α✝ : Type (max u₁ u₂)} {xs : List α✝} {γ : Type (max u₁ u₂)} (onContinue : xs.Cursor → β → Assertion ps) (onReturn : γ → β → Assertion ps) (onExcept : ExceptConds ps := ExceptConds.false) : Invariant xs (MProd (Option γ) β) ps
Helper definition for specifying loop invariants for loops with early return.
for ... in ... loops with early return of type γ elaborate to a call like this:
forIn (β := MProd (Option γ) ...) (b := ⟨none, ...⟩) collection loopBody
Note that the first component of the MProd state tuple is the optional early return value.
It is none as long as there was no early return and some r if the loop returned early with r.
This function allows to specify different invariants for the loop body depending on whether the loop
terminated early or not. When there was an early return, the loop has effectively finished, which is
encoded by the additional ⌜xs.suffix = []⌝ assertion in the invariant. This assertion is vital for
successfully proving the induction step, as it contradicts with the assumption that
xs.suffix = x::rest of the inductive hypothesis at the start of the loop body, meaning that users
won't need to prove anything about the bogus case where the loop has returned early yet takes
another iteration of the loop body.
Invariants use lists to model the sequence of values in 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 of 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.
The current position in the loop is tracked with a List.Cursor that represents a position in a list as a combination of the elements to the left of the position and the elements to the right.
This type is not a traditional zipper, in which the prefix is reversed for efficient movement: it is intended for use in specifications and proofs, not in run-time code, so the prefix is in the original order.
The position of the cursor in the list. It's a shortcut for the number of elements in the prefix.