If a monad is implemented in terms of monad transformers that are provided by the Lean standard library, such as ExceptT and StateT, then it should not require additional instances to work with vcgen.
Other monads will require instances of WP, LawfulMonad, and WPMonad.
Once the basic instances are provided, the next step is to prove a soundness lemma.
This lemma should show that the weakest precondition for running the monadic computation and asserting a desired predicate is in fact sufficient to prove the predicate.
In addition to the definition of the monad, typical libraries provide a set of primitive operators.
Each of these should be provided with a specification lemma.
It may additionally be useful to make the internals of the state private, and export a carefully-designed set of assertion operators.
The specification lemmas for the library's primitive operators should preserve every fact about the state that they do not deliberately hide.
While it's often easier to think in terms of how the operator transforms an input state into an output state, verification condition generation works more reliably when the postcondition is universally quantified, so that automation can instantiate it with the precondition of the next statement instead of proving an entailment.
Auto-Framing Specifications
The function double doubles a natural number state:
Thinking chronologically, a reasonable specification is that the value of the output state is twice that of the input state.
This is expressed using a logical variable that stands for the initial state:
Now, the precondition merely states that the postcondition should hold for double the initial state.
Any property that Q states about state that double does not change, such as a second state layer in a monad transformer stack, holds after the call.
At a call of double, vcgen instantiates Q with the postcondition that the rest of the program requires, so no entailment between postconditions remains as a verification condition.
The precondition of better_double_spec is exactly the weakest precondition of double, so callers depend on the complete behavior of double.
An auto-framing specification can also leave details open.
For example, the precondition funs=>∀s',s≤s'→Q()s' only promises that double does not decrease the state.
Rather than writing it from scratch, the WP instance uses PredTrans.pushArg.
This operator was designed to model state monads, but LogM can be seen as a state monad that can only append to the state.
This appending is visible in the body of the instance, where the initial state and the log that resulted from the action are appended:
The soundness lemma has one important detail: the weakest precondition is applied to the empty array.
This is necessary because the logging computation has been modeled as an append-only state, so there must be some initial state.
Semantically, the empty array is the correct choice so as to not place items in a log that don't come from the program; technically, it must also be a value that commutes with the append operator on arrays.
Next, each operator in the library should be provided with a specification lemma.
There is only one: log.
For new monads, these proofs must often break the abstraction boundaries of Hoare triples and weakest preconditions; the specifications that they provide can then be used abstractly by clients of the library.