feat: Hoare logic for monadic programs and verification condition generation (#8995)

This PR introduces a Hoare logic for monadic programs in
`Std.Do.Triple`, and assorted tactics:

*  `mspec` for applying Hoare triple specifications
* `mvcgen` to turn a Hoare triple proof obligation `⦃P⦄ prog ⦃Q⦄` into
pure verification conditoins (i.e., without any traces of Hoare triples
or weakest preconditions reminiscent of `prog`). The resulting
verification conditions in the stateful logic of `Std.Do.SPred` can be
discharged manually with the tactics coming with its custom proof mode
or with automation such as `simp` and `grind`.

This is pre-release of a planned feature and not yet intended for
production use. We are grateful for feedback of early adopters, though.

Co-authored-by: Sebastian Graf <sg@lean-fro.org>
This commit is contained in:
Sebastian Graf 2025-06-26 17:49:56 +02:00 committed by GitHub
parent 83e226204d
commit f87d05ad4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 2884 additions and 1 deletions

View file

@ -2100,6 +2100,68 @@ introductions in sequence.
macro (name := mintroMacro) (priority:=low) "mintro" : tactic =>
Macro.throwError "to use `mintro`, please include `import Std.Tactic.Do`"
/--
`mspec` is an `apply`-like tactic that applies a Hoare triple specification to the target of the
stateful goal.
Given a stateful goal `H ⊢ₛ wp⟦prog⟧.apply Q'`, `mspec foo_spec` will instantiate
`foo_spec : ... → ⦃P⦄ foo ⦃Q⦄`, match `foo` against `prog` and produce subgoals for
the verification conditions `?pre : H ⊢ₛ P` and `?post : Q ⊢ₚ Q'`.
* If `prog = x >>= f`, then `mspec Specs.bind` is tried first so that `foo` is matched against `x`
instead. Tactic `mspec_no_bind` does not attempt to do this decomposition.
* If `?pre` or `?post` follow by `.rfl`, then they are discharged automatically.
* `?post` is automatically simplified into constituent `⊢ₛ` entailments on
success and failure continuations.
* `?pre` and `?post.*` goals introduce their stateful hypothesis as `h`.
* Any uninstantiated MVar arising from instantiation of `foo_spec` becomes a new subgoal.
* If the goal looks like `fun s => _ ⊢ₛ _` then `mspec` will first `mintro ∀s`.
* If `P` has schematic variables that can be instantiated by doing `mintro ∀s`, for example
`foo_spec : ∀(n:Nat), ⦃⌜n = Natₛ⌝⦄ foo ⦃Q⦄`, then `mspec` will do `mintro ∀s` first to
instantiate `n = s`.
* Right before applying the spec, the `mframe` tactic is used, which has the following effect:
Any hypothesis `Hᵢ` in the goal `h₁:H₁, h₂:H₂, ..., hₙ:Hₙ ⊢ₛ T` that is
pure (i.e., equivalent to some `⌜φᵢ⌝`) will be moved into the pure context as `hᵢ:φᵢ`.
Additionally, `mspec` can be used without arguments or with a term argument:
* `mspec` without argument will try and look up a spec for `x` registered with `@[spec]`.
* `mspec (foo_spec blah ?bleh)` will elaborate its argument as a term with expected type
`⦃?P⦄ x ⦃?Q⦄` and introduce `?bleh` as a subgoal.
This is useful to pass an invariant to e.g., `Specs.forIn_list` and leave the inductive step
as a hole.
-/
macro (name := mspecMacro) (priority:=low) "mspec" : tactic =>
Macro.throwError "to use `mspec`, please include `import Std.Tactic.Do`"
/--
`mvcgen` will break down a Hoare triple proof goal like `⦃P⦄ prog ⦃Q⦄` into verification conditions,
provided that all functions used in `prog` have specifications registered with `@[spec]`.
A verification condition is an entailment in the stateful logic of `Std.Do.SPred`
in which the original program `prog` no longer occurs.
Verification conditions are introduced by the `mspec` tactic; see the `mspec` tactic for what they
look like.
When there's no applicable `mspec` spec, `mvcgen` will try and rewrite an application
`prog = f a b c` with the simp set registered via `@[spec]`.
When used like `mvcgen +noLetElim [foo_spec, bar_def, instBEqFloat]`, `mvcgen` will additionally
* add a Hoare triple specification `foo_spec : ... → ⦃P⦄ foo ... ⦃Q⦄` to `spec` set for a
function `foo` occurring in `prog`,
* unfold a definition `def bar_def ... := ...` in `prog`,
* unfold any method of the `instBEqFloat : BEq Float` instance in `prog`.
* it will no longer substitute away `let`-expressions that occur at most once in `P`, `Q` or `prog`.
Furthermore, `mvcgen` tries to close trivial verification conditions by `SPred.entails.rfl` or
the tactic sequence `try (mpure_intro; trivial)`. The variant `mvcgen_no_trivial` does not do this.
For debugging purposes there is also `mvcgen_step 42` which will do at most 42 VC generation
steps. This is useful for bisecting issues with the generated VCs.
-/
macro (name := mvcgenMacro) (priority:=low) "mvcgen" : tactic =>
Macro.throwError "to use `mvcgen`, please include `import Std.Tactic.Do`"
end Tactic
namespace Attr

View file

@ -5,3 +5,8 @@ Authors: Sebastian Graf
-/
prelude
import Lean.Elab.Tactic.Do.ProofMode
import Lean.Elab.Tactic.Do.Syntax
import Lean.Elab.Tactic.Do.Attr
import Lean.Elab.Tactic.Do.LetElim
import Lean.Elab.Tactic.Do.Spec
import Lean.Elab.Tactic.Do.VCGen

View file

@ -0,0 +1,235 @@
/-
Copyright (c) 2025 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sebastian Graf
-/
prelude
import Lean.Meta
import Std.Do.Triple.Basic
import Std.Tactic.Do.Syntax
namespace Lean.Elab.Tactic.Do.SpecAttr
open Lean Meta Std.Do
builtin_initialize registerTraceClass `Elab.Tactic.Do.specAttr
/--
This attribute should not be used directly.
It is an implementation detail of the `mvcgen` tactic.
-/
builtin_initialize mvcgenSimpExt : Meta.SimpExtension ←
Meta.registerSimpAttr `mvcgen_simp "simp theorems internally used by `mvcgen`"
/--
The simp set accumulated by the `@[spec]` attribute.
(This does not include Hoare triple specs.)
It is an implementation detail of the `mvcgen` tactic.
-/
def getSpecSimpTheorems : CoreM SimpTheorems :=
mvcgenSimpExt.getTheorems
inductive SpecProof where
| global (declName : Name)
| local (fvarId : FVarId)
| stx (id : Name) (ref : Syntax) (proof : Expr)
deriving Inhabited, BEq
/-- A unique identifier corresponding to the origin. -/
def SpecProof.key : SpecProof → Name
| .global declName => declName
| .local fvarId => fvarId.name
| .stx id _ _ => id
instance : Hashable SpecProof where
hash sp := hash sp.key
def SpecProof.instantiate (proof : SpecProof) : MetaM (Array Expr × Array BinderInfo × Expr × Expr) := do
let prf ← match proof with
| .global declName => mkConstWithFreshMVarLevels declName
| .local fvarId => pure <| mkFVar fvarId
| .stx _ _ proof => pure proof -- TODO: Think about simp-like generalization
-- We instantiate here deeply specifically for local hypotheses, the type of which
-- may contain MVars at multiple levels.
-- An example is `ih` in `serialize_bytesize` from the serialization schema test
let type ← instantiateMVars (← inferType prf)
let (xs, bs, type) ← forallMetaTelescope type
return (xs, bs, prf.beta xs, type)
instance : ToMessageData SpecProof where
toMessageData := fun
| .global declName => m!"SpecProof.global {declName}"
| .local fvarId => m!"SpecProof.local {mkFVar fvarId}"
| .stx _ ref proof => m!"SpecProof.stx _ {ref} {proof}"
structure SpecTheorem where
keys : Array DiscrTree.Key
/--
Expr key tested for matching, in ∀-quantified form.
`keys = (← mkPath (← forallMetaTelescope prog).2.2)`.
-/
prog : Expr
/-- The proof for the theorem. -/
proof : SpecProof
/--
If `etaPotential` is non-zero, then the precondition contains meta variables that can be
instantiated after applying `mintro ∀s` `etaPotential` many times.
-/
etaPotential : Nat := 0
priority : Nat := eval_prio default
deriving Inhabited, BEq
abbrev SpecEntry := SpecTheorem
structure SpecTheorems where
specs : DiscrTree SpecTheorem := DiscrTree.empty
erased : PHashSet SpecProof := {}
deriving Inhabited
def SpecTheorems.isErased (d : SpecTheorems) (thmId : SpecProof) : Bool :=
d.erased.contains thmId
def SpecTheorems.eraseCore (d : SpecTheorems) (thmId : SpecProof) : SpecTheorems :=
{ d with erased := d.erased.insert thmId }
abbrev SpecExtension := SimpleScopedEnvExtension SpecEntry SpecTheorems
private partial def countBVarDependentMVars (xs : Array Expr) (e : Expr) : MetaM Nat :=
go e
where
go (e : Expr) : MetaM Nat := do
if !e.hasExprMVar then return 0
match e with
| .app f a =>
if let some (_, lhs, rhs) := e.eq? then
let l := lhs.getAppFn'
let r := rhs.getAppFn'
if l.isMVar && rhs.hasLooseBVars && xs.contains l then return 1
if r.isMVar && lhs.hasLooseBVars && xs.contains r then return 1
return ← (· + ·) <$> go lhs <*> go rhs
return ← (· + ·) <$> go f <*> go a
| .mdata _ e => go e
| .lam _ ty b _ => (· + ·) <$> go ty <*> go b
| .letE _ ty v b _ => (· + · + ·) <$> go ty <*> go v <*> go b
| .forallE _ ty b _ => (· + ·) <$> go ty <*> go b
| .proj _ _ e => go e
| .mvar .. => return 0
| .lit .. | .fvar .. | .bvar .. | .sort .. | .const .. => return 0
def simpSPredConfig : ConfigWithKey :=
{ simpGlobalConfig.config with
iota := true,
beta := true,
zeta := true,
zetaDelta := true,
proj := .yesWithDelta }.toConfigWithKey
/-- If `σs : List Type`, then `e : SPred σs`.
Return the number of times `e` needs to be applied
in order to assign closed solutions to meta variables. -/
partial def computeMVarBetaPotentialForSPred (xs : Array Expr) (σs : Expr) (e : Expr) : MetaM Nat :=
withNewMCtxDepth do
withConfigWithKey simpSPredConfig do
if xs.isEmpty then return 0
let ctx ← Simp.Context.mkDefault
let mut σs ← whnfR σs
let mut e := e
let mut n : Nat := 0
let mut lastSuccess := 0
let mut boundAssignments ← countBVarDependentMVars xs e
while σs.isAppOfArity ``List.cons 3 do
n := n+1
let σ := σs.getArg! 1
let s ← mkFreshExprMVar σ
e := e.beta #[s]
let (r, _) ← simp e ctx
-- In practice we only need to reduce `fun s => ...`, `SVal.curry` and functions that operate
-- on the state tuple bound by `SVal.curry`.
-- We could write a custom function should `simp` become a bottleneck.
e := r.expr
let count ← countBVarDependentMVars xs e
if count < boundAssignments || e.getAppFn'.isMVar then
lastSuccess := n
boundAssignments := count
σs ← whnfR (σs.getArg! 2)
return lastSuccess
private def mkSpecTheorem (type : Expr) (proof : SpecProof) (prio : Nat) : MetaM SpecTheorem := do
-- cf. mkSimpTheoremCore
let type ← instantiateMVars type
unless (← isProp type) do
throwError "invalid 'spec', proposition expected{indentExpr type}"
withNewMCtxDepth do
let (xs, _, type) ← withSimpGlobalConfig (forallMetaTelescopeReducing type)
let type ← whnfR type
let_expr Triple _m ps _inst _α prog P _Q := type
| throwError "unexpected kind of spec theorem; not a triple{indentExpr type}"
let keys ← DiscrTree.mkPath prog (noIndexAtArgs := false)
-- beta potential of `P` describes how many times we want to `mintro ∀s`, that is,
-- *eta*-expand the goal.
let σs := mkApp (mkConst ``PostShape.args) ps
let etaPotential ← computeMVarBetaPotentialForSPred xs σs P
-- logInfo m!"Beta potential {etaPotential} for {P}"
-- logInfo m!"mkSpecTheorem: {keys}, proof: {proof}"
return { keys, prog := (← mkForallFVars xs prog), proof, etaPotential, priority := prio }
def mkSpecTheoremFromConst (declName : Name) (prio : Nat := eval_prio default) : MetaM SpecTheorem := do
-- cf. mkSimpTheoremsFromConst
let cinfo ← getConstInfo declName
let us := cinfo.levelParams.map mkLevelParam
let val := mkConst declName us
-- withSimpGlobalConfig do -- This sets iota := false, which we do not want (for computeMVarBetaPotentialForSPred)
let type ← inferType val
mkSpecTheorem type (.global declName) prio
def mkSpecTheoremFromLocal (fvar : FVarId) (prio : Nat := eval_prio default) : MetaM SpecTheorem := do
let some decl ← fvar.findDecl? | throwError "invalid 'spec', local constant not found"
mkSpecTheorem decl.type (.local fvar) prio
def mkSpecTheoremFromStx (ref : Syntax) (proof : Expr) (prio : Nat := eval_prio default) : MetaM SpecTheorem := do
let type ← inferType proof
mkSpecTheorem type (.stx (← mkFreshId) ref proof) prio
def addSpecTheoremEntry (d : SpecTheorems) (e : SpecTheorem) : SpecTheorems :=
{ d with specs := d.specs.insertCore e.keys e }
def addSpecTheorem (ext : SpecExtension) (declName : Name) (prio : Nat) (attrKind : AttributeKind) : MetaM Unit := do
let thm ← mkSpecTheoremFromConst declName prio
ext.add thm attrKind
def mkSpecExt : SimpleScopedEnvExtension.Descr SpecEntry SpecTheorems where
name := `specMap
initial := {}
addEntry := fun d e => addSpecTheoremEntry d e
builtin_initialize specAttr : SpecExtension ← registerSimpleScopedEnvExtension mkSpecExt
def mkSpecAttr (ext : SpecExtension) : AttributeImpl where
name := `spec
descr := "Marks Hoare triple specifications and simp theorems to use with the `mspec` and `mvcgen` tactics"
-- .afterCompilation seems unnecessarily conservative, but the simp attribute impl needs it.
-- The reason is that we cannot annotate definitions with `@[spec]` otherwise; the error is
-- > trying to realize id.eq_1 but `enableRealizationsForConst` must be called for 'id' first
applicationTime := AttributeApplicationTime.afterCompilation
add := fun declName stx attrKind => do
let go : MetaM Unit := do
let info ← getConstInfo declName
let prio ← Attribute.Builtin.getPrio stx
try
addSpecTheorem ext declName prio attrKind
catch _ =>
let impl ← getBuiltinAttributeImpl `mvcgen_simp
try
impl.add declName stx attrKind
catch e =>
trace[Elab.Tactic.Do.specAttr] "Reason for failure to apply spec attribute: {e.toMessageData}"
throwError "Invalid 'spec': target was neither a Hoare triple specification nor a 'simp' lemma"
discard <| go.run {} {}
builtin_initialize registerBuiltinAttribute (mkSpecAttr specAttr)
def SpecExtension.getTheorems (ext : SpecExtension) : CoreM SpecTheorems :=
return ext.getState (← getEnv)
def getSpecTheorems : CoreM SpecTheorems :=
specAttr.getTheorems

View file

@ -0,0 +1,177 @@
/-
Copyright (c) 2025 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sebastian Graf
-/
prelude
import Lean.Meta
namespace Lean.Elab.Tactic.Do
open Lean Meta Tactic
inductive Uses where
| zero
| one
| many
deriving BEq, Ord, Inhabited
def Uses.add : Uses → Uses → Uses
| .zero, b => b
| a, .zero => a
| _, _ => .many
def Uses.toNat : Uses → Nat
| .zero => 0
| .one => 1
| .many => 2
def Uses.fromNat : Nat → Uses
| 0 => .zero
| 1 => .one
| _ => .many
instance : Add Uses where
add := Uses.add
abbrev FVarUses := Std.HashMap FVarId Uses
def FVarUses.add (a b : FVarUses) : FVarUses :=
a.fold (init := b) fun acc k v => acc.alter k fun
| none => some v
| some v' => some (v + v')
instance : Add FVarUses where
add := FVarUses.add
inductive BVarUses (n : Nat) where
| none
| some (uses : Vector Uses n) -- indexed by BVars in reverse order
def BVarUses.single {numBVars : Nat} (n : Nat) (_ : n < numBVars := by get_elem_tactic) : BVarUses numBVars :=
-- NB: BVarUses is indexed by BVars in reverse order
.some <| .ofFn fun (i : Fin numBVars) => if i.val = numBVars - 1 - n then .one else .zero
def BVarUses.pop {numBVars : Nat} : BVarUses (numBVars + 1) → (Uses × BVarUses numBVars)
| .none => (.zero, .none)
| .some uses => (uses.back, .some uses.pop)
def BVarUses.add {numBVars : Nat} (a b : BVarUses numBVars) : BVarUses numBVars := match a, b with
| .none, b => b
| a, .none => a
| .some a, .some b => .some (a.zipWith (fun a b => a + b) b)
instance : Add (BVarUses numBVars) where
add := BVarUses.add
def over1Of2 (f : α₁ → α₂) (x : α₁ × β) : α₂ × β := (f x.1, x.2)
def addMData (d : MData) (e : Expr) : Expr := match e with
| .mdata d' e => .mdata (d.mergeBy (fun _ new _ => new) d') e
| _ => .mdata d e
private def okToDup (e : Expr) : Bool := match e with
| .bvar .. => true
| .fvar .. => false -- viable, but would invalidate use information (which we could work around)
| .lit .. | .const .. | .sort .. | .mvar .. => true
| .mdata _ e => okToDup e
| .proj _ _ e => okToDup e -- Not sure about this one. Do we want to duplicate projs?
| .app .. => Simp.isOfNatNatLit e || Simp.isOfScientificLit e || Simp.isCharLit e
| .lam .. | .forallE .. | .letE .. => false
mutual
partial def countUsesDecl (fvarId : FVarId) (ty : Expr) (val? : Option Expr) (bodyUses : FVarUses) (subst : Array FVarId := #[]) : MetaM (Expr × Option Expr × FVarUses) := do
let (ty, tyUses) ← countUses ty subst
let (val?, valUses) ← match val? with
| none => pure (none, {})
| some val => over1Of2 some <$> countUses val subst
let uses := bodyUses.getD fvarId .zero
let fvs := if uses == .zero then bodyUses else bodyUses + tyUses + valUses
let fvs := fvs.erase fvarId
let ty := addMData (MData.empty.setNat `uses uses.toNat) ty
return (ty, val?, fvs)
partial def countUses (e : Expr) (subst : Array FVarId := #[]) : MetaM (Expr × FVarUses) := match e with
| .bvar n =>
if _ : n < subst.size then
return (e, {(subst[n], .one)})
else
throwError "BVar index out of bounds: {n} >= {subst.size}"
| .fvar fvarId => return (e, {(fvarId, .one)})
| .letE x ty val body b => do
let fv ← mkFreshFVarId
let (body, fvs) ← countUses body (subst.push fv)
let (ty, .some val, fvs) ← countUsesDecl fv ty (some val) fvs subst | failure
return (.letE x ty val body b, fvs)
| .lam x ty body bi => do
let fv ← mkFreshFVarId
let (ty, fvs₁) ← countUses ty subst
let (body, fvs₂) ← countUses body (subst.push fv)
let fvs := (fvs₁ + fvs₂).erase fv
return (.lam x ty body bi, fvs) -- NB: We do not track uses of lam-bound x
| .forallE x ty body bi => do -- (almost) identical to lam
let fv ← mkFreshFVarId
let (ty, fvs₁) ← countUses ty subst
let (body, fvs₂) ← countUses body (subst.push fv)
let fvs := (fvs₁ + fvs₂).erase fv
return (.forallE x ty body bi, fvs) -- NB: We do not track uses of forall-bound x
| .proj s i e => over1Of2 (Expr.proj s i) <$> countUses e subst
| .mdata d e => over1Of2 (Expr.mdata d) <$> countUses e subst
| .app f a => do
let (f, fvarUses₁) ← countUses f subst
let (a, fvarUses₂) ← countUses a subst
return (.app f a, fvarUses₁ + fvarUses₂)
| .lit .. | .const .. | .sort .. | .mvar .. => return (e, {})
end
def countUsesLCtx (ctx : LocalContext) (targetUses : FVarUses) : MetaM LocalContext := do
let decls : Array LocalDecl := Array.mkEmpty ctx.decls.size
let decls ← Prod.fst <$> ctx.foldrM (init := (decls, targetUses)) fun decl (decls, targetUses) => do
let (ty, val?, fvs) ← countUsesDecl decl.fvarId decl.type decl.value? targetUses
let decl := match val? with
| none => decl.setType ty
| some val => decl.setType ty |>.setValue val
return (decls.push decl, fvs)
-- NB: decls are in reverse order; root comes first
let decls ← StateRefT'.run' (ω:=IO.RealWorld) (s := decls) <| ctx.decls.mapM fun decl => do
if decl.isNone then return decl
modifyGet fun (decls : Array LocalDecl) => (some decls.back!, decls.pop)
return { ctx with decls }
def doNotDup (u : Uses) (rhs : Expr) (elimTrivial : Bool) : Bool :=
u == .many && not (elimTrivial && okToDup rhs)
def elimLetsCore (e : Expr) (elimTrivial := true) : MetaM Expr := StateRefT'.run' (s := Std.HashSet.emptyWithCapacity 10) do
-- Figure out if we can make this work with Core.transform.
-- I think that would entail keeping track of BVar shifts in `pre` and `post`.
let pre (e : Expr) : StateRefT (Std.HashSet FVarId) MetaM TransformStep := do
match e with
| .letE _ ty val body _ => do
let .mdata d _ := ty | return .continue
let uses := Uses.fromNat (d.getNat `uses 2) -- 2 goes to .many
if doNotDup uses val elimTrivial then return .continue
return .visit (body.instantiate1 val) -- urgh O(n^2). See comment above
| _ => return .continue
Meta.transform e (pre := pre)
def elimLets (mvar : MVarId) (elimTrivial := true): MetaM MVarId := mvar.withContext do
let ctx ← getLCtx
let (ty, fvarUses) ← countUses (← mvar.getType)
let ctx ← countUsesLCtx ctx fvarUses
let mut fvs := #[]
let mut vals := #[]
for decl in ctx do
let .some val := decl.value? | continue
let .mdata d _ := decl.type | continue
let uses := Uses.fromNat (d.getNat `uses 2) -- 2 goes to .many
if doNotDup uses val elimTrivial then continue
fvs := fvs.push (mkFVar decl.fvarId)
vals := vals.push val
let ty := ty.replaceFVars fvs vals
let ty ← elimLetsCore ty elimTrivial
let newMVar ← mkFreshExprSyntheticOpaqueMVar ty (← mvar.getTag)
mvar.assign newMVar
let mut mvar := newMVar.mvarId!
for fvarId in fvs do
mvar ← mvar.tryClear fvarId.fvarId!
return mvar

View file

@ -0,0 +1,246 @@
/-
Copyright (c) 2022 Lars König. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Lars König, Mario Carneiro, Sebastian Graf
-/
prelude
import Lean.Elab.Tactic.Do.ProofMode.Basic
import Lean.Elab.Tactic.Do.ProofMode.Intro
import Lean.Elab.Tactic.Do.ProofMode.Pure
import Lean.Elab.Tactic.Do.ProofMode.Frame
import Lean.Elab.Tactic.Do.ProofMode.Assumption
import Lean.Elab.Tactic.Do.Attr
import Std.Do.Triple
namespace Lean.Elab.Tactic.Do
open Lean Elab Tactic Meta
open Std.Do Do.SpecAttr Do.ProofMode
builtin_initialize registerTraceClass `Elab.Tactic.Do.spec
def findSpec (database : SpecTheorems) (wp : Expr) : MetaM SpecTheorem := do
let_expr WP.wp _m _ps _instWP _α prog := wp | throwError "target not a wp application {wp}"
let prog ← instantiateMVarsIfMVarApp prog
let prog := prog.headBeta
let candidates ← database.specs.getMatch prog
let candidates := candidates.filter fun spec => !database.erased.contains spec.proof
let candidates := candidates.insertionSort fun s₁ s₂ => s₁.priority < s₂.priority
trace[Elab.Tactic.Do.spec] "Candidates for {prog}: {candidates.map (·.proof)}"
let specs ← candidates.filterM fun spec => do
let (_, _, _, type) ← spec.proof.instantiate
trace[Elab.Tactic.Do.spec] "{spec.proof} instantiates to {type}"
let_expr Triple m ps instWP α specProg _P _Q := type | throwError "Not a triple: {type}"
isDefEq wp (mkApp5 (← mkConstWithFreshMVarLevels ``WP.wp) m ps instWP α specProg)
trace[Elab.Tactic.Do.spec] "Specs for {prog}: {specs.map (·.proof)}"
if specs.isEmpty then throwError m!"No specs found for {indentExpr prog}\nCandidates: {candidates.map (·.proof)}"
return specs[0]!
def elabTermIntoSpecTheorem (stx : TSyntax `term) (expectedTy : Expr) : TacticM (SpecTheorem × List MVarId) := do
if stx.raw.isIdent then
match ← Term.resolveId? stx.raw (withInfo := true) with
| some (.const declName _) => return (← mkSpecTheoremFromConst declName, [])
| some (.fvar fvarId) => return (← mkSpecTheoremFromLocal fvarId, [])
| _ => pure ()
try
let (prf, mvars) ← --Term.withSynthesize <|
elabTermWithHoles stx expectedTy `mspec (allowNaturalHoles := true)
let specThm ← mkSpecTheoremFromStx stx.raw prf
return (specThm, mvars)
catch e : Exception =>
trace[Elab.Tactic.Do.spec] "Internal error. This happens for example when the head symbol of the spec is wrong. Message:\n {e.toMessageData}"
throw e
def elabSpec (stx? : Option (TSyntax `term)) (wp : Expr) : TacticM (SpecTheorem × List MVarId) := do
let_expr f@WP.wp m ps instWP α prog := wp | throwError "target not a wp application {wp}"
let P ← mkFreshExprMVar (mkApp (mkConst ``Assertion) ps) (userName := `P)
let Q ← mkFreshExprMVar (mkApp2 (mkConst ``PostCond) α ps) (userName := `Q)
let expectedTy := mkApp7 (mkConst ``Triple f.constLevels!) m ps instWP α prog P Q
trace[Elab.Tactic.Do.spec] "spec syntax: {stx?}"
trace[Elab.Tactic.Do.spec] "expected type: {← instantiateMVars expectedTy}"
match stx? with
| none => pure (← findSpec (← getSpecTheorems) wp, [])
| some stx => elabTermIntoSpecTheorem stx expectedTy
variable {n} [Monad n] [MonadControlT MetaM n] [MonadLiftT MetaM n]
private def mkProj' (n : Name) (i : Nat) (Q : Expr) : MetaM Expr := do
return (← projectCore? Q i).getD (mkProj n i Q)
mutual
partial def dischargePostEntails (α : Expr) (ps : Expr) (Q : Expr) (Q' : Expr) (goalTag : Name) (discharge : Expr → Name → n Expr) : n Expr := do
-- Often, Q' is fully instantiated while Q contains metavariables. Try refl
if (← isDefEq Q Q') then
return mkApp3 (mkConst ``PostCond.entails.refl) α ps Q'
let Q ← whnfR Q
let Q' ← whnfR Q'
-- If Q (postcond of the spec) is just an fvar, we do not decompose further
if let some _fvarId := Q.fvarId? then
return ← discharge (mkApp4 (mkConst ``PostCond.entails) α ps Q Q') (goalTag ++ `post)
-- Otherwise decompose the conjunction
let prf₁ ← withLocalDeclD Name.anonymous α fun a => do
let Q1a := (← mkProj' ``Prod 0 Q).betaRev #[a]
let Q'1a := (← mkProj' ``Prod 0 Q').betaRev #[a]
let σs := mkApp (mkConst ``PostShape.args) ps
let uniq ← liftMetaM mkFreshId
let goal := MGoal.mk σs (Hyp.mk `h uniq Q1a).toExpr Q'1a
mkLambdaFVars #[a] (← discharge goal.toExpr (goalTag ++ `success))
let prf₂ ← dischargeFailEntails ps (← mkProj' ``Prod 1 Q) (← mkProj' ``Prod 1 Q') (goalTag ++ `except) discharge
mkAppM ``And.intro #[prf₁, prf₂] -- This is just a bit too painful to construct by hand
partial def dischargeFailEntails (ps : Expr) (Q : Expr) (Q' : Expr) (goalTag : Name) (discharge : Expr → Name → n Expr) : n Expr := do
if ps.isAppOf ``PostShape.pure then
return mkConst ``True.intro
if ← isDefEq Q Q' then
return mkApp2 (mkConst ``FailConds.entails.refl) ps Q
if ← isDefEq Q (mkApp (mkConst ``FailConds.false) ps) then
return mkApp2 (mkConst ``FailConds.entails_false) ps Q'
if ← isDefEq Q' (mkApp (mkConst ``FailConds.true) ps) then
return mkApp2 (mkConst ``FailConds.entails_true) ps Q
-- the remaining cases are recursive.
if let some (_σ, ps) := ps.app2? ``PostShape.arg then
return ← dischargeFailEntails ps Q Q' goalTag discharge
if let some (ε, ps) := ps.app2? ``PostShape.except then
let Q ← whnfR Q
let Q' ← whnfR Q'
let prf₁ ← withLocalDeclD Name.anonymous ε fun e => do
let Q1e := (← mkProj' ``Prod 0 Q).betaRev #[e]
let Q'1e := (← mkProj' ``Prod 0 Q').betaRev #[e]
let σs := mkApp (mkConst ``PostShape.args) ps
let uniq ← liftMetaM mkFreshId
let goal := MGoal.mk σs (Hyp.mk `h uniq Q1e).toExpr Q'1e
mkLambdaFVars #[e] (← discharge goal.toExpr (goalTag ++ `handle))
let prf₂ ← dischargeFailEntails ps (← mkProj' ``Prod 1 Q) (← mkProj' ``Prod 1 Q') (goalTag ++ `except) discharge
return ← mkAppM ``And.intro #[prf₁, prf₂] -- This is just a bit too painful to construct by hand
-- This case happens when decomposing with unknown `ps : PostShape`
discharge (mkApp3 (mkConst ``FailConds.entails) ps Q Q') goalTag
end
def dischargeMGoal (goal : MGoal) (goalTag : Name) (discharge : Expr → Name → n Expr) : n Expr := do
-- controlAt MetaM (fun map => do trace[Elab.Tactic.Do.spec] "dischargeMGoal: {(← reduceProj? goal.target).getD goal.target}"; map (pure ()))
-- simply try one of the assumptions for now. Later on we might want to decompose conjunctions etc; full xsimpl
let some prf ← liftMetaM goal.assumption | discharge goal.toExpr goalTag
return prf
def mkPreTag (goalTag : Name) : Name := Id.run do
let dflt := goalTag ++ `pre1
let .str p s := goalTag | return dflt
unless "pre".isPrefixOf s do return dflt
let some n := (s.toSubstring.drop 3).toString.toNat? | return dflt
return .str p ("pre" ++ toString (n + 1))
/--
Returns the proof and the list of new unassigned MVars.
-/
def mSpec (goal : MGoal) (elabSpecAtWP : Expr → n (SpecTheorem × List MVarId)) (discharge : Expr → Name → n Expr) (goalTag : Name) (mkPreTag := mkPreTag) : n (Expr × List MVarId) := do
-- First instantiate `fun s => ...` in the target via repeated `mintro ∀s`.
let (holes, prf) ← mIntroForallN goal goal.target.consumeMData.getNumHeadLambdas fun goal => do
-- Elaborate the spec for the wp⟦e⟧ app in the target
let T := goal.target.consumeMData
unless T.getAppFn.constName! == ``PredTrans.apply do
liftMetaM (throwError "target not a PredTrans.apply application {indentExpr T}")
let wp := T.getArg! 2
liftMetaM <| do trace[Elab.Tactic.Do.spec] "before elabSpecAtWP {wp}"
let (specThm, elabMVars) ← elabSpecAtWP wp
liftMetaM <| do trace[Elab.Tactic.Do.spec] "after elabSpecAtWP {specThm.proof} {wp}"
-- The precondition of `specThm` might look like `⌜?n = Natₛ ∧ ?m = Boolₛ⌝`, which expands to
-- `SVal.curry (fun tuple => ?n = SVal.uncurry (getThe Nat tuple) ∧ ?m = SVal.uncurry (getThe Bool tuple))`.
-- Note that the assignments for `?n` and `?m` depend on the bound variable `tuple`.
-- Here, we further eta expand and simplify according to `etaPotential` so that the solutions for
-- `?n` and `?m` do not depend on `tuple`.
let residualEta := specThm.etaPotential - (T.getAppNumArgs - 4) -- 4 arguments expected for PredTrans.apply
mIntroForallN goal residualEta fun goal => do
-- Compute a frame of `P` that we duplicate into the pure context using `Spec.frame`
-- For now, frame = `P` or nothing at all
mTryFrame goal fun goal => do
-- Fully instantiate the specThm without instantiating its MVars to `wp` yet
let (schematicMVars, _, spec, specTy) ← specThm.proof.instantiate
-- Apply the spec to the excess arguments of the `wp⟦e⟧ Q` application
let T := goal.target.consumeMData
let args := T.getAppArgs
let Q' := args[3]!
let excessArgs := (args.extract 4 args.size).reverse
liftMetaM <| do trace[Elab.Tactic.Do.spec] "before WTF {wp} {spec} {specTy}"
-- Actually instantiate the specThm using the expected type computed from `wp`.
let_expr f@Triple m ps instWP α prog P Q := specTy | do liftMetaM (throwError "target not a Triple application {specTy}")
let wp' := mkApp5 (mkConst ``WP.wp f.constLevels!) m ps instWP α prog
unless (← withAssignableSyntheticOpaque <| isDefEq wp wp') do
Term.throwTypeMismatchError none wp wp' spec
liftMetaM <| do trace[Elab.Tactic.Do.spec] "WTF {wp} {wp'} {spec} {specTy}"
let P := P.betaRev excessArgs
let spec := spec.betaRev excessArgs
-- often P or Q are schematic (i.e. an MVar app). Try to solve by rfl.
let P ← instantiateMVarsIfMVarApp P
let Q ← instantiateMVarsIfMVarApp Q
let HPRfl ← withDefault <| withAssignableSyntheticOpaque <| isDefEqGuarded P goal.hyps
let QQ'Rfl ← withDefault <| withAssignableSyntheticOpaque <| isDefEqGuarded Q Q'
-- Discharge the validity proof for the spec if not rfl
let mut prePrf : Expr → Expr := id
if !HPRfl then
-- let P := (← reduceProjBeta? P).getD P
-- Try to avoid creating a longer name if the postcondition does not need to create a goal
let tag := if !QQ'Rfl then mkPreTag goalTag else goalTag
let HPPrf ← dischargeMGoal { goal with target := P } tag discharge
prePrf := mkApp6 (mkConst ``SPred.entails.trans) goal.σs goal.hyps P goal.target HPPrf
-- Discharge the entailment on postconditions if not rfl
let mut postPrf : Expr → Expr := id
if !QQ'Rfl then
-- Try to avoid creating a longer name if the precondition does not need to create a goal
let tag := if !HPRfl then goalTag ++ `post else goalTag
let wpApplyQ := mkApp4 (mkConst ``PredTrans.apply) ps α wp Q -- wp⟦x⟧.apply Q; that is, T without excess args
let wpApplyQ' := mkApp4 (mkConst ``PredTrans.apply) ps α wp Q' -- wp⟦x⟧.apply Q'
let QQ' ← dischargePostEntails α ps Q Q' tag discharge
let QQ'mono := mkApp6 (mkConst ``PredTrans.mono) ps α wp Q Q' QQ'
postPrf := fun h =>
mkApp6 (mkConst ``SPred.entails.trans) goal.σs P (wpApplyQ.betaRev excessArgs) (wpApplyQ'.betaRev excessArgs)
h (QQ'mono.betaRev excessArgs)
-- finally build the proof; HPPrf.trans (spec.trans QQ'mono)
let prf := prePrf (postPrf spec)
let holes := elabMVars ++ schematicMVars.toList.map (·.mvarId!)
let holes ← liftMetaM <| holes.filterM fun mv => not <$> mv.isAssignedOrDelayedAssigned
return (holes, prf)
-- (This is after closing the `mForallIntro` and `mTryFrame` blocks.)
-- Functions like `mkForallFVars` etc. might have instantiated some of the MVar holes and in
-- doing so have introduced new MVars in turn.
-- Thus we try and instantiate all MVars and collect the MVars of the instantiated expressions.
let holes ← liftMetaM <| holes.flatMapM fun mv => do
let e ← instantiateMVars (mkMVar mv)
let mvs ← getMVars e
let mvs ← mvs.filterM fun mv => not <$> mv.isAssignedOrDelayedAssigned
return mvs.toList
return (prf, holes)
private def addMVar (mvars : IO.Ref (List MVarId)) (goal : Expr) (name : Name) : MetaM Expr := do
let m ← mkFreshExprSyntheticOpaqueMVar goal (tag := name)
mvars.modify (m.mvarId! :: ·)
return m
@[builtin_tactic Lean.Parser.Tactic.mspecNoBind]
def evalMSpecNoBind : Tactic
| `(tactic| mspec_no_bind $[$spec]?) => do
let (mvar, goal) ← mStartMVar (← getMainGoal)
mvar.withContext do
let goals ← IO.mkRef []
let (prf, specHoles) ← mSpec goal (elabSpec spec) (fun goal name => liftMetaM (addMVar goals goal name)) (← getMainTag)
mvar.assign prf
let goals ← goals.get
if let [mvar'] := goals then mvar'.setTag (← mvar.getTag)
replaceMainGoal (goals ++ specHoles)
| _ => throwUnsupportedSyntax
-- TODO: Define the simp set as a list here and build `simpArgs` syntax from it
macro_rules
| `(tactic| mspec_no_simp $[$spec]?) =>
`(tactic| ((try with_reducible mspec_no_bind $(mkIdent ``Std.Do.Spec.bind)); mspec_no_bind $[$spec]?))
| `(tactic| mspec $[$spec]?) =>
`(tactic| mspec_no_simp $[$spec]?; open Std.Do in all_goals ((try simp only [SPred.true_intro_simp, SPred.true_intro_simp_nil, SVal.curry_cons, SVal.uncurry_cons, SVal.getThe_here, SVal.getThe_there]); (try mpure_intro; trivial)))

View file

@ -0,0 +1,62 @@
/-
Copyright (c) 2022 Lars König. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Lars König, Mario Carneiro, Sebastian Graf
-/
prelude
import Init.NotationExtra
import Lean.Elab.BuiltinNotation
import Std.Do.PostCond
import Std.Do.Triple.Basic
namespace Std.Do
open Lean Parser Meta Elab Term
def post_syntax := leading_parser
"post⟨" >> withoutPosition (withoutForbidden (sepBy termParser ", " (allowTrailingSep := true))) >> "⟩"
scoped syntax:max "post⟨" term,+ "⟩" : term
macro_rules | `(post⟨$handlers,*⟩) => `(by exact ⟨$handlers,*, ()⟩)
-- NB: Postponement through by exact is the entire point of this macro
-- until https://github.com/leanprover/lean4/pull/8074 lands
example : PostCond Nat .pure := post⟨fun s => True⟩
example : PostCond (Nat × Nat) (PostShape.except Nat (PostShape.arg Nat PostShape.pure)) :=
post⟨fun (r, xs) s => r ≤ 4 ∧ s = 4 ∧ r + xs > 4, fun e s => e = 42 ∧ s = 4⟩
open Lean Parser Term in
def funArrow : Parser := unicodeSymbol " ↦ " " => "
@[inherit_doc PostCond.total]
scoped macro "⇓" xs:Lean.Parser.Term.funBinder+ funArrow e:term : term =>
`(PostCond.total (by exact (fun $xs* => spred($e)))) -- NB: Postponement through by exact
@[app_unexpander PostCond.total]
private def unexpandPostCondTotal : PrettyPrinter.Unexpander
| `($_ fun $xs* => $e) => do `(⇓ $xs* => $(← SPred.Notation.unpack e))
| _ => throw ()
elab_rules : term
| `(⦃$P⦄ $x ⦃$Q⦄) => do
-- In a simple world, this would just be a macro expanding to
-- `Triple $x spred($P) spred($Q)`.
-- However, currently we need to help type inference for P and Q.
-- Specifically, if `x : StateT σ m α`, `[wp : WP m ps]` and `P : σ → Assertion ps`,
-- then `Triple x P _` will not elaborate because `σ → Assertion ps =?= Assertion ?ps` fails.
-- We must first instantiate `?ps` to `.arg σ ps` through the `outParam` of `WP`, hence this elaborator.
-- This is tracked in #8766, and #8074 might be a fix.
let x ← elabTerm x none
let ty ← inferType x
tryPostponeIfMVar ty
let ty ← instantiateMVars ty
let .app m α := ty.consumeMData | throwError "Not a type application {ty}"
let some u ← Level.dec <$> getLevel ty | throwError "Wrong level 0 {ty}"
let ps ← mkFreshExprMVar (mkConst ``PostShape)
let inst ← synthInstance (mkApp2 (mkConst ``WP [u]) m ps)
let P ← elabTerm (← `(spred($P))) (mkApp (mkConst ``Assertion) ps)
let Q ← elabTerm (← `(spred($Q))) (mkApp2 (mkConst ``PostCond) α ps)
return mkApp7 (mkConst ``Triple [u]) m ps inst α x P Q
@[app_unexpander Triple]
private def unexpandTriple : PrettyPrinter.Unexpander
| `($_ $x $P $Q) => do
`(⦃$(← SPred.Notation.unpack P)⦄ $x ⦃$Q⦄)
| _ => throw ()

View file

@ -0,0 +1,392 @@
/-
Copyright (c) 2025 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sebastian Graf
-/
prelude
import Std.Do.WP
import Std.Do.Triple
import Lean.Elab.Tactic.Simp
import Lean.Elab.Tactic.Meta
import Lean.Elab.Tactic.Do.ProofMode.Basic
import Lean.Elab.Tactic.Do.ProofMode.Intro
import Lean.Elab.Tactic.Do.ProofMode.Cases
import Lean.Elab.Tactic.Do.ProofMode.Specialize
import Lean.Elab.Tactic.Do.LetElim
import Lean.Elab.Tactic.Do.Spec
import Lean.Elab.Tactic.Do.Attr
import Lean.Elab.Tactic.Do.Syntax
namespace Lean.Elab.Tactic.Do
open Lean Parser Elab Tactic Meta Do ProofMode SpecAttr
open Std.Do
builtin_initialize registerTraceClass `Elab.Tactic.Do.vcgen
register_builtin_option mvcgen.warning : Bool := {
defValue := true
group := "debug"
descr := "disable `mvcgen` usage warning"
}
inductive Fuel where
| limited (n : Nat)
| unlimited
deriving DecidableEq
structure Config where
/--
If true, do not substitute away let-declarations that are used at most once before starting
VC generation.
-/
noLetElim : Bool := false
declare_config_elab elabConfig Config
structure Context where
config : Config
specThms : SpecTheorems
simpCtx : Simp.Context
simprocs : Simp.SimprocsArray
structure State where
fuel : Fuel := .unlimited
simpState : Simp.State := {}
/--
The verification conditions that have been generated so far.
Includes `Type`-valued goals arising from instantiation of specifications.
-/
vcs : Array MVarId := #[]
abbrev VCGenM := ReaderT Context (StateRefT State MetaM)
def burnOne : VCGenM PUnit := do
let s ← get
match s.fuel with
| Fuel.limited 0 => return ()
| Fuel.limited (n + 1) => set { s with fuel := .limited n }
| Fuel.unlimited => return ()
def ifOutOfFuel (x : VCGenM α) (k : VCGenM α) : VCGenM α := do
let s ← get
match s.fuel with
| Fuel.limited 0 => x
| _ => k
def emitVC (subGoal : Expr) (name : Name) : VCGenM Expr := do
let m ← liftM <| mkFreshExprSyntheticOpaqueMVar subGoal (tag := name)
modify fun s => { s with vcs := s.vcs.push m.mvarId! }
return m
def addSubGoalAsVC (goal : MVarId) : VCGenM PUnit := do
modify fun s => { s with vcs := s.vcs.push goal }
def liftSimpM (x : SimpM α) : VCGenM α := do
let ctx ← read
let s ← get
let mref := (Simp.mkDefaultMethodsCore ctx.simprocs).toMethodsRef
let (a, simpState) ← x mref ctx.simpCtx |>.run s.simpState
set { s with simpState }
return a
instance : MonadLift SimpM VCGenM where
monadLift x := liftSimpM x
syntax (name := mvcgen_step) "mvcgen_step" optConfig
(num)? (" [" withoutPosition((simpStar <|> simpErase <|> simpLemma),*,?) "]")? : tactic
syntax (name := mvcgen_no_trivial) "mvcgen_no_trivial" optConfig
(" [" withoutPosition((simpStar <|> simpErase <|> simpLemma),*,?) "]")? : tactic
syntax (name := mvcgen) "mvcgen" optConfig
(" [" withoutPosition((simpStar <|> simpErase <|> simpLemma),*,?) "]")? : tactic
private def mkSpecContext (optConfig : Syntax) (lemmas : Syntax) (ignoreStarArg := false) : TacticM Context := do
let config ← elabConfig optConfig
let mut specThms ← getSpecTheorems
let mut simpStuff := #[]
let mut starArg := false
for arg in lemmas[1].getSepArgs do
if arg.getKind == ``simpErase then
try
-- Try and build SpecTheorems for the lemma to erase to see if it's
-- meant to be interpreted by SpecTheorems. Otherwise fall back to SimpTheorems.
let specThm ←
if let some fvar ← Term.isLocalIdent? arg[1] then
mkSpecTheoremFromLocal fvar.fvarId!
else
let id := arg[1]
if let .ok declName ← observing (realizeGlobalConstNoOverloadWithInfo id) then
mkSpecTheoremFromConst declName
else
withRef id <| throwUnknownConstant id.getId.eraseMacroScopes
specThms := specThms.eraseCore specThm.proof
catch _ =>
simpStuff := simpStuff.push ⟨arg⟩ -- simp tracks its own erase stuff
else if arg.getKind == ``simpLemma then
unless arg[0].isNone && arg[1].isNone do
-- When there is ←, →, ↑ or ↓ then this is for simp
simpStuff := simpStuff.push ⟨arg⟩
continue
let term := arg[2]
match ← Term.resolveId? term (withInfo := true) <|> Term.elabCDotFunctionAlias? ⟨term⟩ with
| some (.const declName _) =>
let info ← getConstInfo declName
try
let thm ← mkSpecTheoremFromConst declName
specThms := addSpecTheoremEntry specThms thm
catch _ =>
simpStuff := simpStuff.push ⟨arg⟩
| some (.fvar fvar) =>
let decl ← getFVarLocalDecl (.fvar fvar)
try
let thm ← mkSpecTheoremFromLocal fvar
specThms := addSpecTheoremEntry specThms thm
catch _ =>
simpStuff := simpStuff.push ⟨arg⟩
| _ => withRef term <| throwError "Could not resolve {repr term}"
else if arg.getKind == ``simpStar then
starArg := true
simpStuff := simpStuff.push ⟨arg⟩
else
throwUnsupportedSyntax
-- Build a mock simp call to build a simp context that corresponds to `simp [simpStuff]`
let stx ← `(tactic| simp +unfoldPartialApp [$(Syntax.TSepArray.ofElems simpStuff),*])
-- logInfo s!"{stx}"
let res ← mkSimpContext stx.raw
(eraseLocal := false)
(simpTheorems := getSpecSimpTheorems)
(ignoreStarArg := ignoreStarArg)
-- logInfo m!"{res.ctx.simpTheorems.map (·.toUnfold.toList)}"
if starArg && !ignoreStarArg then
let fvars ← getPropHyps
for fvar in fvars do
unless specThms.isErased (.local fvar) do
try
let thm ← mkSpecTheoremFromLocal fvar
specThms := addSpecTheoremEntry specThms thm
catch _ => continue
return { config, specThms, simpCtx := res.ctx, simprocs := res.simprocs }
def isDuplicable (e : Expr) : Bool := match e with
| .bvar .. => true
| .mvar .. => true
| .fvar .. => true
| .const .. => true
| .lit .. => true
| .sort .. => true
| .mdata _ e => isDuplicable e
| .proj _ _ e => isDuplicable e
| .lam .. => false
| .forallE .. => false
| .letE .. => false
| .app .. => e.isAppOf ``OfNat.ofNat
def withSharing (name : Name) (type : Expr) (val : Expr) (k : Expr → (Expr → VCGenM Expr) → VCGenM α) (kind : LocalDeclKind := .default) : VCGenM α :=
if isDuplicable val then
k val pure
else
withLetDecl name type val (kind := kind) fun fv => do
k fv (liftM <| mkForallFVars #[fv] ·)
/-- Reduces (1) Prod projection functions and (2) Projs in application heads,
and (3) beta reduces. -/
private partial def reduceProjBeta? (e : Expr) : MetaM (Option Expr) :=
go none e.getAppFn e.getAppRevArgs
where
go lastReduction f rargs := do
match f with
| .mdata _ f => go lastReduction f rargs
| .app f a => go lastReduction f (rargs.push a)
| .lam .. =>
if rargs.size = 0 then return lastReduction
let e' := f.betaRev rargs
go (some e') e'.getAppFn e'.getAppRevArgs
| .const name .. =>
let env ← getEnv
match env.getProjectionStructureName? name with
| some ``Prod => -- only reduce fst and snd for now
match ← Meta.unfoldDefinition? (mkAppRev f rargs) with
| some e' => go lastReduction e'.getAppFn e'.getAppRevArgs
| none => pure lastReduction
| _ => pure lastReduction
| .proj .. => match ← reduceProj? f with
| some f' =>
let e' := mkAppRev f' rargs
go (some e') e'.getAppFn e'.getAppRevArgs
| none => pure lastReduction
| _ => pure lastReduction
partial def step (ctx : Context) (fuel : Fuel) (goal : MGoal) (name : Name) : MetaM (Expr × Array MVarId) := do
withReducible do
let (res, state) ← StateRefT'.run (ReaderT.run (onGoal goal name) ctx) { fuel }
return (res, state.vcs)
where
onFail (goal : MGoal) (name : Name) : VCGenM Expr := do
-- logInfo m!"fail {goal.toExpr}"
emitVC goal.toExpr name
tryGoal (goal : Expr) (name : Name) : VCGenM Expr := do
forallTelescope goal fun xs body => do
let res ← try mStart body catch _ =>
return ← mkLambdaFVars xs (← emitVC goal name)
let mut prf ← onGoal res.goal name
-- logInfo m!"tryGoal: {res.goal.toExpr}"
-- res.goal.checkProof prf
if let some proof := res.proof? then
prf := mkApp proof prf
mkLambdaFVars xs prf
assignMVars (mvars : List MVarId) : VCGenM PUnit := do
for mvar in mvars do
-- trace[Elab.Tactics.Do.vcgen] "assignMVars {← mvar.getTag}, assigned: {← mvar.isAssigned}"
if ← mvar.isAssigned then continue
-- I used to filter for `isProp` here and add any non-Props directly as subgoals,
-- but then we would get spurious instantiations of non-synthetic goals such as loop
-- invariants.
mvar.assign (← mvar.withContext <| tryGoal (← mvar.getType) (← mvar.getTag))
onGoal goal name : VCGenM Expr := do
let T := goal.target
let T := (← reduceProjBeta? T).getD T -- very slight simplification
-- logInfo m!"target: {T}"
let goal := { goal with target := T }
let f := T.getAppFn
if f.isLambda then
return ← onLambda goal name
if f.isConstOf ``SPred.imp then
return ← onImp goal name
else if f.isConstOf ``PredTrans.apply then
return ← onWPApp goal name
onFail { goal with target := T } name
onImp goal name : VCGenM Expr := ifOutOfFuel (onFail goal name) do
burnOne
(·.2) <$> mIntro goal (← `(binderIdent| _)) (fun g =>
do return ((), ← onGoal g name))
onLambda goal name : VCGenM Expr := ifOutOfFuel (onFail goal name) do
burnOne
(·.2) <$> mIntroForall goal (← `(binderIdent| _)) (fun g =>
do return ((), ← onGoal g name))
onWPApp goal name : VCGenM Expr := ifOutOfFuel (onFail goal name) do
let args := goal.target.getAppArgs
let trans := args[2]!
-- logInfo m!"trans: {trans}"
let Q := args[3]!
let wp ← instantiateMVarsIfMVarApp trans
match_expr wp with
| c@WP.wp m ps instWP α e =>
let e ← instantiateMVarsIfMVarApp e
let e := e.headBeta
trace[Elab.Tactics.Do.vcgen] "Target: {e}"
let goalWithNewProg e' :=
let wp' := mkApp5 c m ps instWP α e'
let args' := args.set! 2 wp'
{ goal with target := mkAppN (mkConst ``PredTrans.apply) args' }
-- lambda-expressions
if e.getAppFn'.isLambda && false then
-- We are likely in the implementation of a StateT function; do `mintro ∀s`
return ← onLambda goal name
-- let-expressions
if let .letE x ty val body _nonDep := e.getAppFn' then
burnOne
return ← withSharing x ty val fun fv leave => do
let e' := ((body.instantiate1 fv).betaRev e.getAppRevArgs)
leave (← onWPApp (goalWithNewProg e') name)
-- match-expressions
if let .some info := isMatcherAppCore? (← getEnv) e then
-- Bring into simp NF
let res? ← Simp.simpMatchDiscrs? info e
let e ← -- returns/continues only if old e is defeq to new e
if let .some res := res? then
burnOne
if let .some heq := res.proof? then
let prf ← onWPApp (goalWithNewProg res.expr) name
let prf := mkApp10 (mkConst ``Triple.rewrite_program c.constLevels!) m ps α goal.hyps Q instWP e res.expr heq prf
return prf
else
pure res.expr
else
pure e
-- Try reduce the matcher
let e ← match (← reduceMatcher? e) with
| .reduced e' =>
burnOne
return ← onWPApp (goalWithNewProg e') name
| .stuck _ => pure e
| _ => pure e
-- Last resort: Split match
-- logInfo m!"split match {e}"
burnOne
let mvar ← mkFreshExprSyntheticOpaqueMVar goal.toExpr (tag := name)
let mvars ← Split.splitMatch mvar.mvarId! e
assignMVars mvars
return mvar
-- Unfold local bindings (TODO don't do this unconditionally)
let f := e.getAppFn'
if let some (some val) ← f.fvarId?.mapM (·.getValue?) then
burnOne
let e' := val.betaRev e.getAppRevArgs
-- logInfo m!"unfold local var {f}, new WP: {wpe}"
return ← onWPApp (goalWithNewProg e') name
-- Unfold definitions according to reducibility and spec attributes,
-- apply specifications
if f.isConst then
burnOne
try
let specThm ← findSpec ctx.specThms wp
trace[Elab.Tactics.Do.vcgen] "Candidate spec for {f.constName!}: {specThm.proof}"
let (prf, specHoles) ← mSpec goal (fun _wp => return (specThm, [])) tryGoal name
assignMVars specHoles
return prf
catch ex =>
trace[Elab.Tactics.Do.vcgen] "Failed to find spec. Trying simp. Reason: {ex.toMessageData}"
let res ← Simp.simp e
unless res.expr != e do return ← onFail goal name
burnOne
if let .some heq := res.proof? then
trace[Elab.Tactics.Do.vcgen] "Simplified"
let prf ← onWPApp (goalWithNewProg res.expr) name
let prf := mkApp10 (mkConst ``Triple.rewrite_program c.constLevels!) m ps α goal.hyps Q instWP e res.expr heq prf
return prf
else
return ← onWPApp (goalWithNewProg res.expr) name
return ← onFail goal name
| _ => return ← onFail goal name
def genVCs (goal : MVarId) (ctx : Context) (fuel : Fuel) : TacticM (Array MVarId) := do
let goal ← if ctx.config.noLetElim then pure goal else elimLets goal
let (mvar, goal) ← mStartMVar goal
mvar.withContext do
let (prf, vcs) ← step ctx (fuel := fuel) goal (← mvar.getTag)
mvar.assign prf
replaceMainGoal vcs.toList
return vcs
@[builtin_tactic Lean.Parser.Tactic.mvcgenStep]
def evalMVCGenStep : Tactic := fun stx => withMainContext do
let ctx ← mkSpecContext stx[1] stx[3]
let n := if stx[2].isNone then 1 else stx[2][0].toNat
discard <| genVCs (← getMainGoal) ctx (fuel := .limited n)
@[builtin_tactic Lean.Parser.Tactic.mvcgenNoTrivial]
def evalMVCGenNoTrivial : Tactic := fun stx => withMainContext do
let ctx ← mkSpecContext stx[0] stx[1]
discard <| genVCs (← getMainGoal) ctx (fuel := .unlimited)
@[builtin_tactic Lean.Parser.Tactic.mvcgen]
def evalMVCGen : Tactic := fun stx => withMainContext do
if mvcgen.warning.get (← getOptions) then
logWarningAt stx "The `mvcgen` tactic is experimental and still under development. Avoid using it in production projects."
-- I would like to define this simply as a macro
-- `(tactic| mvcgen_no_trivial $c $lemmas <;> try (guard_target =~ (⌜True⌝ ⊢ₛ _); mpure_intro; trivial))
-- but optConfig is not a leading_parser, and neither is the syntax for `lemmas`
let ctx ← mkSpecContext stx[1] stx[2]
let vcs ← genVCs (← getMainGoal) ctx (fuel := .unlimited)
let tac ← `(tactic| try (mpure_intro; trivial))
for vc in vcs do discard <| runTactic vc tac

View file

@ -5,3 +5,7 @@ Authors: Sebastian Graf
-/
prelude
import Std.Do.SPred
import Std.Do.WP
import Std.Do.Triple
import Std.Do.PredTrans
import Std.Do.PostCond

226
src/Std/Do/PostCond.lean Normal file
View file

@ -0,0 +1,226 @@
/-
Copyright (c) 2025 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sebastian Graf
-/
prelude
import Std.Do.SPred
/-!
# Pre and postconditions
This module defines `Assertion` and `PostCond`, the types which constitute
the pre and postconditions of a Hoare triple in the program logic.
## Predicate shapes
Since `WP` supports arbitrary monads, `PostCond` must be general enough to
cope with state and exceptions.
For this reason, `PostCond` is indexed by a `PostShape` which is an abstraction
of the stack of effects supported by the monad, corresponding directly to
`StateT` and `ExceptT` layers in a transformer stack.
For every `StateT σ` effect, we get one `PostShape.arg σ` layer, whereas for every
`ExceptT ε` effect, we get one `PostShape.except ε` layer.
Currently, the only supported base layer is `PostShape.pure`.
## Pre and postconditions
The type of preconditions `Assertion ps` is distinct from the type of postconditions `PostCond α ps`.
This is because postconditions not only specify what happens upon successful termination of the
program, but also need to specify a property that holds upon failure.
We get one "barrel" for the success case, plus one barrel per `PostShape.except` layer.
It does not make much sense to talk about failure barrels in the context of preconditions.
Hence, `Assertion ps` is defined such that all `PostShape.except` layers are discarded from `ps`,
via `PostShape.args`.
-/
namespace Std.Do
inductive PostShape : Type 1 where
| pure : PostShape
| arg : (σ : Type) → PostShape → PostShape
| except : (ε : Type) → PostShape → PostShape
abbrev PostShape.args : PostShape → List Type
| .pure => []
| .arg σ s => σ :: PostShape.args s
| .except _ s => PostShape.args s
/--
An assertion on the `.arg`s in the given predicate shape.
```
example : Assertion (.arg ρ .pure) = (ρ → Prop) := rfl
example : Assertion (.except ε .pure) = Prop := rfl
example : Assertion (.arg σ (.except ε .pure)) = (σ → Prop) := rfl
example : Assertion (.except ε (.arg σ .pure)) = (σ → Prop) := rfl
```
This is an abbreviation for `SPred` under the hood, so all theorems about `SPred` apply.
-/
abbrev Assertion (ps : PostShape) : Type :=
SPred (PostShape.args ps)
/--
Encodes one continuation barrel for each `PostShape.except` in the given predicate shape.
```
example : FailConds (.pure) = Unit := rfl
example : FailConds (.except ε .pure) = ((ε → Prop) × Unit) := rfl
example : FailConds (.arg σ (.except ε .pure)) = ((ε → Prop) × Unit) := rfl
example : FailConds (.except ε (.arg σ .pure)) = ((ε → σ → Prop) × Unit) := rfl
```
-/
def FailConds : PostShape → Type
| .pure => Unit
| .arg _ ps => FailConds ps
| .except ε ps => (ε → Assertion ps) × FailConds ps
@[simp]
def FailConds.const {ps : PostShape} (p : Prop) : FailConds ps := match ps with
| .pure => ()
| .arg _ ps => @FailConds.const ps p
| .except _ ps => (fun _ε => spred(⌜p⌝), @FailConds.const ps p)
@[simp]
def FailConds.true : FailConds ps := FailConds.const True
@[simp]
def FailConds.false : FailConds ps := FailConds.const False
instance : Inhabited (FailConds ps) where
default := FailConds.true
def FailConds.entails {ps : PostShape} (x y : FailConds ps) : Prop :=
match ps with
| .pure => True
| .arg _ ps => @entails ps x y
| .except _ ps => (∀ e, x.1 e ⊢ₛ y.1 e) ∧ @entails ps x.2 y.2
infixr:25 " ⊢ₑ " => FailConds.entails
@[simp, refl]
theorem FailConds.entails.refl {ps : PostShape} (x : FailConds ps) : x ⊢ₑ x := by
induction ps <;> simp [entails, *]
theorem FailConds.entails.rfl {ps : PostShape} {x : FailConds ps} : x ⊢ₑ x := refl x
theorem FailConds.entails.trans {ps : PostShape} {x y z : FailConds ps} : (x ⊢ₑ y) → (y ⊢ₑ z) → x ⊢ₑ z := by
induction ps
case pure => intro _ _; trivial
case arg σ s ih => exact ih
case except ε s ih => intro h₁ h₂; exact ⟨fun e => (h₁.1 e).trans (h₂.1 e), ih h₁.2 h₂.2⟩
@[simp]
theorem FailConds.entails_false {x : FailConds ps} : FailConds.false ⊢ₑ x := by
induction ps <;> simp_all [false, const, entails, SPred.false_elim]
@[simp]
theorem FailConds.entails_true {x : FailConds ps} : x ⊢ₑ FailConds.true := by
induction ps <;> simp_all [true, const, entails]
@[simp]
def FailConds.and {ps : PostShape} (x : FailConds ps) (y : FailConds ps) : FailConds ps :=
match ps with
| .pure => ()
| .arg _ ps => @FailConds.and ps x y
| .except _ _ => (fun e => SPred.and (x.1 e) (y.1 e), FailConds.and x.2 y.2)
infixr:35 " ∧ₑ " => FailConds.and
theorem FailConds.and_true {x : FailConds ps} : x ∧ₑ FailConds.true ⊢ₑ x := by
induction ps
case pure => trivial
case arg ih => exact ih
case except ε ps ih =>
simp_all only [true, and, const]
constructor <;> simp only [SPred.and_true.mp, implies_true, ih]
theorem FailConds.true_and {x : FailConds ps} : FailConds.true ∧ₑ x ⊢ₑ x := by
induction ps
case pure => trivial
case arg ih => exact ih
case except ε ps ih =>
simp_all only [true, and, const]
constructor <;> simp only [SPred.true_and.mp, implies_true, ih]
theorem FailConds.and_false {x : FailConds ps} : x ∧ₑ FailConds.false ⊢ₑ FailConds.false := by
induction ps
case pure => trivial
case arg ih => exact ih
case except ε ps ih =>
simp_all only [false, and, const]
constructor <;> simp only [SPred.and_false.mp, implies_true, ih]
theorem FailConds.false_and {x : FailConds ps} : FailConds.false ∧ₑ x ⊢ₑ FailConds.false := by
induction ps
case pure => trivial
case arg ih => exact ih
case except ε ps ih =>
simp_all only [and, false, const]
constructor <;> simp only [SPred.false_and.mp, implies_true, ih]
theorem FailConds.and_eq_left {ps : PostShape} {p q : FailConds ps} (h : p ⊢ₑ q) :
p = (p ∧ₑ q) := by
induction ps
case pure => trivial
case arg ih => exact ih h
case except ε ps ih =>
simp_all only [and]
apply Prod.ext
· ext a; exact (SPred.and_eq_left.mp (h.1 a)).to_eq
· exact ih h.2
/--
A multi-barreled postcondition for the given predicate shape.
```
example : PostCond α (.arg ρ .pure) = ((αρ → Prop) × Unit) := rfl
example : PostCond α (.except ε .pure) = ((α → Prop) × (ε → Prop) × Unit) := rfl
example : PostCond α (.arg σ (.except ε .pure)) = ((ασ → Prop) × (ε → Prop) × Unit) := rfl
example : PostCond α (.except ε (.arg σ .pure)) = ((ασ → Prop) × (ε → σ → Prop) × Unit) := rfl
```
-/
abbrev PostCond (α : Type) (s : PostShape) : Type :=
(α → Assertion s) × FailConds s
/-- A postcondition expressing total correctness. -/
abbrev PostCond.total (p : α → Assertion ps) : PostCond α ps :=
(p, FailConds.false)
/-- A postcondition expressing partial correctness. -/
abbrev PostCond.partial (p : α → Assertion ps) : PostCond α ps :=
(p, FailConds.true)
instance : Inhabited (PostCond α ps) where
default := PostCond.total fun _ => default
@[simp]
def PostCond.entails (p q : PostCond α ps) : Prop :=
(∀ a, SPred.entails (p.1 a) (q.1 a)) ∧ FailConds.entails p.2 q.2
infixr:25 " ⊢ₚ " => PostCond.entails
@[refl,simp]
theorem PostCond.entails.refl (Q : PostCond α ps) : Q ⊢ₚ Q := ⟨fun a => SPred.entails.refl (Q.1 a), FailConds.entails.refl Q.2⟩
theorem PostCond.entails.rfl {Q : PostCond α ps} : Q ⊢ₚ Q := refl Q
theorem PostCond.entails.trans {P Q R : PostCond α ps} (h₁ : P ⊢ₚ Q) (h₂ : Q ⊢ₚ R) : P ⊢ₚ R :=
⟨fun a => (h₁.1 a).trans (h₂.1 a), h₁.2.trans h₂.2⟩
@[simp]
theorem PostCond.entails_total (p : α → Assertion ps) (q : PostCond α ps) : PostCond.total p ⊢ₚ q ↔ ∀ a, p a ⊢ₛ q.1 a := by
simp only [entails, FailConds.entails_false, and_true]
@[simp]
theorem PostCond.entails_partial (p : PostCond α ps) (q : α → Assertion ps) : p ⊢ₚ PostCond.partial q ↔ ∀ a, p.1 a ⊢ₛ q a := by
simp only [entails, FailConds.entails_true, and_true]
abbrev PostCond.and (p : PostCond α ps) (q : PostCond α ps) : PostCond α ps :=
(fun a => SPred.and (p.1 a) (q.1 a), FailConds.and p.2 q.2)
infixr:35 " ∧ₚ " => PostCond.and
theorem PostCond.and_eq_left {p q : PostCond α ps} (h : p ⊢ₚ q) :
p = (p ∧ₚ q) := by
ext
· exact (SPred.and_eq_left.mp (h.1 _)).to_eq
· exact FailConds.and_eq_left h.2

239
src/Std/Do/PredTrans.lean Normal file
View file

@ -0,0 +1,239 @@
/-
Copyright (c) 2025 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sebastian Graf
-/
prelude
import Init.Control.Lawful
import Std.Do.PostCond
/-!
# Predicate transformers for arbitrary postcondition shapes
This module defines the type `PredTrans ps` of *predicate transformers* for a given `ps :
PostShape`.
`PredTrans` forms the semantic domain of the weakest precondition interpretation
`WP` in which we interpret monadic programs.
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₂`.
Since `PredTrans` itself forms a monad, we can interpret monadic programs by writing
a monad morphism into `PredTrans`; this is exactly what `WP` encodes.
This module defines interpretations of common monadic operations, such as `get`, `throw`,
`liftM`, etc.
-/
namespace Std.Do
/-- The stronger the postcondition, the stronger the transformed precondition. -/
def PredTrans.Monotonic {ps : PostShape} {α : Type} (t : PostCond α ps → Assertion ps) : Prop :=
∀ Q₁ Q₂, (Q₁ ⊢ₚ Q₂) → (t Q₁) ⊢ₛ (t Q₂)
/-- Transforming a conjunction of postconditions is the same as the conjunction of transformed
postconditions. -/
def PredTrans.Conjunctive {ps : PostShape} {α : Type} (t : PostCond α ps → Assertion ps) : Prop :=
∀ Q₁ Q₂, t (Q₁ ∧ₚ Q₂) ⊣⊢ₛ t Q₁ ∧ t Q₂
/-- Any predicate transformer that is conjunctive is also monotonic. -/
def PredTrans.Conjunctive.mono {ps : PostShape} {α : Type}
(t : PostCond α ps → Assertion ps) (h : PredTrans.Conjunctive t) : PredTrans.Monotonic t := by
intro Q₁ Q₂ hq
replace hq : Q₁ = (Q₁ ∧ₚ Q₂) := PostCond.and_eq_left hq
rw [hq, (h Q₁ Q₂).to_eq]
exact SPred.and_elim_r
/--
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₂`.
-/
@[ext]
structure PredTrans (ps : PostShape) (α : Type) : Type where
apply : PostCond α ps → Assertion ps
conjunctive : PredTrans.Conjunctive apply
namespace PredTrans
theorem mono {ps : PostShape} {α : Type} (t : PredTrans ps α) : Monotonic t.apply :=
Conjunctive.mono t.apply t.conjunctive
/--
Given a fixed postcondition, the *stronger* predicate transformer will yield a
*weaker* precondition.
-/
def le {ps : PostShape} {α : Type} (x y : PredTrans ps α) : Prop :=
∀ Q, y.apply Q ⊢ₛ x.apply Q -- the weaker the precondition, the smaller the PredTrans
instance : LE (PredTrans ps α) := ⟨le⟩
def pure {ps : PostShape} {α : Type} (a : α) : PredTrans ps α :=
{ apply := fun Q => Q.1 a, conjunctive := by intro _ _; simp }
def bind {ps : PostShape} {α β : Type} (x : PredTrans ps α) (f : α → PredTrans ps β) : PredTrans ps β :=
{ apply := fun Q => x.apply (fun a => (f a).apply Q, Q.2),
conjunctive := by
intro Q₁ Q₂
apply SPred.bientails.of_eq
dsimp
conv => rhs; rw [← (x.conjunctive _ _).to_eq]
conv in (f _).apply _ => rw [((f _).conjunctive _ _).to_eq]
}
def const {ps : PostShape} {α : Type} (p : Assertion ps) : PredTrans ps α :=
{ apply := fun Q => p, conjunctive := by intro _ _; simp [SPred.and_self.to_eq] }
instance : Monad (PredTrans ps) where
pure := pure
bind := bind
@[simp]
theorem pure_apply {ps : PostShape} {α : Type} (a : α) (Q : PostCond α ps) :
(PredTrans.pure a : PredTrans ps α).apply Q = Q.1 a := by rfl
@[simp]
theorem Pure_pure_apply {ps : PostShape} {α : Type} (a : α) (Q : PostCond α ps) :
(Pure.pure a : PredTrans ps α).apply Q = Q.1 a := by rfl
@[simp]
theorem map_apply {ps : PostShape} {α β : Type} (f : α → β) (x : PredTrans ps α) (Q : PostCond β ps) :
(f <$> x).apply Q = x.apply (fun a => Q.1 (f a), Q.2) := by rfl
@[simp]
theorem bind_apply {ps : PostShape} {α β : Type} (x : PredTrans ps α) (f : α → PredTrans ps β) (Q : PostCond β ps) :
(x >>= f).apply Q = x.apply (fun a => (f a).apply Q, Q.2) := by rfl
@[simp]
theorem seq_apply {ps : PostShape} {α β : Type} (f : PredTrans ps (α → β)) (x : PredTrans ps α) (Q : PostCond β ps) :
(f <*> x).apply Q = f.apply (fun g => x.apply (fun a => Q.1 (g a), Q.2), Q.2) := by rfl
theorem bind_mono {ps : PostShape} {α β : Type} {x y : PredTrans ps α} {f : α → PredTrans ps β}
(h : x ≤ y) : x >>= f ≤ y >>= f := by intro Q; exact (h (_, Q.2))
instance instLawfulMonad : LawfulMonad (PredTrans ps) :=
LawfulMonad.mk' (PredTrans ps)
(id_map := by simp +unfoldPartialApp [Functor.map, bind, pure])
(pure_bind := by simp +unfoldPartialApp [Bind.bind, bind, Pure.pure, pure])
(bind_assoc := by simp +unfoldPartialApp [Bind.bind, bind])
-- The interpretation of `StateT σ (PredTrans ps) α` into `PredTrans (.arg σ ps) α`.
-- Think: modifyGetM
def pushArg {ps : PostShape} {σ : Type} {α : Type} (x : StateT σ (PredTrans ps) α) : PredTrans (.arg σ ps) α :=
{ apply := fun Q s => (x s).apply (fun (a, s) => Q.1 a s, Q.2),
conjunctive := by
intro Q₁ Q₂
apply SPred.bientails.of_eq
ext s
dsimp
rw [← ((x s).conjunctive _ _).to_eq]
}
def popArg {ps : PostShape} {α} (x : PredTrans (.arg σ ps) α) : StateT σ (PredTrans ps) α := fun s =>
{ apply Q := x.apply (fun r s' => Q.1 (r, s'), Q.2) s,
conjunctive := by
intro Q₁ Q₂
apply SPred.bientails.of_eq
dsimp
have {Q₁ Q₂}: spred(x.apply Q₁ ∧ x.apply Q₂) s = spred(x.apply (Q₁ ∧ₚ Q₂) s) := by
rw[congrFun (x.conjunctive _ _).to_eq _]
simp only [SPred.and] at this
simp only [this]
rfl
}
-- The interpretation of `ExceptT ε (PredTrans ps) α` into `PredTrans (.except ε ps) α`
def pushExcept {ps : PostShape} {α ε} (x : ExceptT ε (PredTrans ps) α) : PredTrans (.except ε ps) α :=
{ apply Q := x.apply (fun | .ok a => Q.1 a | .error e => Q.2.1 e, Q.2.2),
conjunctive := by
intro Q₁ Q₂
apply SPred.bientails.of_eq
dsimp
rw[← (x.conjunctive _ _).to_eq]
congr
ext x
cases x <;> simp
}
def popExcept {ps : PostShape} {α} (x : PredTrans (.except ε ps) α) : ExceptT ε (PredTrans ps) α :=
{ apply Q := x.apply (fun a => Q.1 (.ok a), fun e => Q.1 (.error e), Q.2),
conjunctive := by
intro Q₁ Q₂
apply SPred.bientails.of_eq
dsimp
rw[← (x.conjunctive _ _).to_eq]
rfl
}
instance instMonadLiftArg : MonadLift (PredTrans m) (PredTrans (.arg σ m)) where
monadLift x := pushArg (StateT.lift x)
instance instMonadLiftExcept : MonadLift (PredTrans m) (PredTrans (.except ε m)) where
monadLift x := pushExcept (ExceptT.lift x)
instance instMonadFunctorArg : MonadFunctor (PredTrans m) (PredTrans (.arg σ m)) where
monadMap f x := pushArg (fun s => f (popArg x s))
instance instMonadFunctorExcept : MonadFunctor (PredTrans m) (PredTrans (.except ε m)) where
monadMap f x := pushExcept (f x.popExcept)
@[simp]
def pushArg_apply {ps} {α : Type} {σ : Type} {Q : PostCond α (.arg σ ps)} (f : σ → PredTrans ps (α × σ)) :
(pushArg f).apply Q = fun s => (f s).apply (fun ⟨a, s⟩ => Q.1 a s, Q.2) := rfl
@[simp]
def pushExcept_apply {ps} {α ε} {Q : PostCond α (.except ε ps)} (x : PredTrans ps (Except ε α)) :
(pushExcept x).apply Q = x.apply (fun | .ok a => Q.1 a | .error e => Q.2.1 e, Q.2.2) := rfl
def dite_apply {ps} {Q : PostCond α ps} (c : Prop) [Decidable c] (t : c → PredTrans ps α) (e : ¬ c → PredTrans ps α) :
(if h : c then t h else e h).apply Q = if h : c then (t h).apply Q else (e h).apply Q := by split <;> rfl
def ite_apply {ps} {Q : PostCond α ps} (c : Prop) [Decidable c] (t : PredTrans ps α) (e : PredTrans ps α) :
(if c then t else e).apply Q = if c then t.apply Q else e.apply Q := by split <;> rfl
@[simp]
def monadLiftArg_apply {ps} {Q : PostCond α (.arg σ ps)} (t : PredTrans ps α) :
(MonadLift.monadLift t : PredTrans (.arg σ ps) α).apply Q = fun s => t.apply (fun a => Q.1 a s, Q.2) := rfl
@[simp]
def monadLiftExcept_apply {ps} {Q : PostCond α (.except ε ps)} (t : PredTrans ps α) :
(MonadLift.monadLift t : PredTrans (.except ε ps) α).apply Q = t.apply (fun a => Q.1 a, Q.2.2) := rfl
@[simp]
def monadMapArg_apply {ps} {Q : PostCond α (.arg σ ps)} (f : ∀{β}, PredTrans ps β → PredTrans ps β) (t : PredTrans (.arg σ ps) α) :
(MonadFunctor.monadMap (m:=PredTrans ps) f t).apply Q = fun s => (f (t.popArg s)).apply (fun (a, s) => Q.1 a s, Q.2) := rfl
@[simp]
def monadMapExcept_apply {ps} {Q : PostCond α (.except ε ps)} (f : ∀{β}, PredTrans ps β → PredTrans ps β) (t : PredTrans (.except ε ps) α) :
(MonadFunctor.monadMap (m:=PredTrans ps) f t).apply Q = (f t.popExcept).apply (fun | .ok a => Q.1 a | .error e => Q.2.1 e, Q.2.2) := rfl
@[simp]
def popArg_apply {ps} {Q : PostCond (α × σ) ps} (t : PredTrans (.arg σ ps) α) :
(t.popArg s).apply Q = t.apply (fun a s => Q.1 (a, s), Q.2) s := rfl
@[simp]
def popExcept_apply {ps} {Q : PostCond (Except ε α) ps} (t : PredTrans (.except ε ps) α) :
(t.popExcept).apply Q = t.apply (fun a => Q.1 (.ok a), fun e => Q.1 (.error e), Q.2) := rfl
@[simp]
theorem pushArg_popArg : pushArg (popArg x) = x := rfl
@[simp]
theorem popArg_pushArg : popArg (pushArg f) = f := rfl
-- Just a reminder for me that the following would not hold for a suitable defn of pushReader and popReader:
--theorem pushReader_popReader : pushReader (popReader x) = x := sorry
-- goal: x.apply (fun a x => Q.1 a x✝, Q.2) x✝ = x.apply Q x✝
@[simp]
theorem pushExcept_popExcept : pushExcept (popExcept x) = x := rfl
@[simp]
theorem popExcept_pushExcept : popExcept (pushExcept x) = x := by
ext Q
simp only [ExceptT.run, popExcept, pushExcept]
congr
ext x
cases x <;> simp

8
src/Std/Do/Triple.lean Normal file
View file

@ -0,0 +1,8 @@
/-
Copyright (c) 2025 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sebastian Graf
-/
prelude
import Std.Do.Triple.Basic
import Std.Do.Triple.SpecLemmas

View file

@ -0,0 +1,63 @@
/-
Copyright (c) 2025 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sebastian Graf
-/
prelude
import Std.Do.WP
import Std.Do.SPred
/-!
# Hoare triples
Hoare triples form the basis for compositional functional correctness proofs about monadic programs.
As usual, `Triple x P Q` holds iff the precondition `P` entails the weakest precondition
`wp⟦x⟧.apply Q` of `x : m α` for the postcondition `Q`.
It is thus defined in terms of an instance `WP m ps`.
-/
namespace Std.Do
universe u
variable {m : Type → Type u} {ps : PostShape}
/--
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`.
-/
def Triple [WP m ps] {α} (x : m α) (P : Assertion ps) (Q : PostCond α ps) : Prop :=
P ⊢ₛ wp⟦x⟧ Q
@[inherit_doc Std.Do.Triple]
scoped syntax:lead (name := triple) "⦃" term "⦄ " term:lead " ⦃" term "⦄" : term
namespace Triple
instance [WP m ps] (x : m α) : SPred.Tactic.PropAsSPredTautology (Triple x P Q) spred(P → wp⟦x⟧ Q) where
iff := (SPred.entails_true_intro P (wp⟦x⟧ Q)).symm
theorem pure [Monad m] [WPMonad m ps] {α} {Q : PostCond α ps} (a : α) (himp : P ⊢ₛ Q.1 a) :
Triple (pure (f:=m) a) P Q := himp.trans (by simp)
theorem bind [Monad m] [WPMonad m ps] {α β} {P : Assertion ps} {Q : α → Assertion ps} {R : PostCond β ps} (x : m α) (f : α → m β)
(hx : Triple x P (Q, R.2))
(hf : ∀ b, Triple (f b) (Q b) R) :
Triple (x >>= f) P R := by
apply SPred.entails.trans hx
simp only [WP.bind]
apply (wp x).mono _ _
simp only [PostCond.entails, Assertion, FailConds.entails.refl, and_true]
exact hf
theorem and [WP m ps] (x : m α) (h₁ : Triple x P₁ Q₁) (h₂ : Triple x P₂ Q₂) : Triple x spred(P₁ ∧ P₂) (Q₁ ∧ₚ Q₂) :=
(SPred.and_mono h₁ h₂).trans ((wp x).conjunctive Q₁ Q₂).mpr
theorem rewrite_program [WP m ps] {prog₁ prog₂ : m α}
(heq : prog₁ = prog₂) (hprf : Triple prog₂ P Q) :
Triple prog₁ P Q := heq ▸ hprf
end Triple

View file

@ -0,0 +1,481 @@
/-
Copyright (c) 2025 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sebastian Graf
-/
prelude
import Std.Do.Triple.Basic
import Std.Tactic.Do.Syntax
import Std.Do.WP
/-!
# Hoare triple specifications for select functions
This module contains Hoare triple specifications for some functions in Core.
-/
namespace Std.Range
abbrev toList (r : Std.Range) : List Nat :=
List.range' r.start ((r.stop - r.start + r.step - 1) / r.step) r.step
theorem toList_range' (r : Std.Range) (h : r.step = 1) :
toList r = List.range' r.start (r.stop - r.start) := by
simp [toList, h]
end Std.Range
namespace Std.List
@[ext]
structure Zipper {α : Type u} (l : List α) : Type u where
rpref : List α
suff : List α
property : rpref.reverse ++ suff = l
abbrev Zipper.pref {α} {l : List α} (s : List.Zipper l) : List α := s.rpref.reverse
abbrev Zipper.begin (l : List α) : Zipper l := ⟨[],l,rfl⟩
abbrev Zipper.end (l : List α) : Zipper l := ⟨l.reverse,[],by simp⟩
abbrev Zipper.tail (s : Zipper l) (h : s.suff = hd::tl) : Zipper l :=
{ rpref := hd::s.rpref, suff := tl, property := by simp [s.property, ←h] }
end Std.List
namespace Std.Do
-- We override the `Triple` notation in `Std.Do.Triple.Basic` just in this module.
-- The reason is that the actual `Triple` notation is implemented as an elaborator in
-- `Lean.Elab.Tactic.Do.Syntax` for reasons such as #8766. Perhaps #8074 will help.
@[inherit_doc Std.Do.triple]
local notation:lead "⦃" P "⦄ " x:lead " ⦃" Q "⦄" => Triple x (spred(P)) spred(Q)
/-! # If/Then/Else -/
-- @[spec]
theorem Spec.ite {α m ps} {P : Assertion ps} {Q : PostCond α ps} (c : Prop) [Decidable c] [WP m ps] (t : m α) (e : m α)
(ifTrue : c → ⦃P⦄ t ⦃Q⦄) (ifFalse : ¬c → ⦃P⦄ e ⦃Q⦄) :
⦃P⦄ if c then t else e ⦃Q⦄ := by
split <;> apply_rules
-- @[spec]
theorem Spec.dite {α m ps} {P : Assertion ps} {Q : PostCond α ps} (c : Prop) [Decidable c] [WP m ps] (t : c → m α) (e : ¬ c → m α)
(ifTrue : (h : c) → ⦃P⦄ t h ⦃Q⦄) (ifFalse : (h : ¬ c) → ⦃P⦄ e h ⦃Q⦄) :
⦃P⦄ if h : c then t h else e h ⦃Q⦄ := by
split <;> apply_rules
/-! # `Monad` -/
universe u
variable {m : Type → Type u} {ps : PostShape}
theorem Spec.pure' [Monad m] [WPMonad m ps] {P : Assertion ps} {Q : PostCond α ps}
(h : P ⊢ₛ Q.1 a) :
⦃P⦄ Pure.pure (f:=m) a ⦃Q⦄ := Triple.pure a h
-- @[spec]
theorem Spec.pure {m : Type → Type u} {ps : PostShape} [Monad m] [WPMonad m ps] {α} {a : α} {Q : PostCond α ps} :
⦃Q.1 a⦄ Pure.pure (f:=m) a ⦃Q⦄ := Spec.pure' .rfl
theorem Spec.bind' [Monad m] [WPMonad m ps] {x : m α} {f : α → m β} {P : Assertion ps} {Q : PostCond β ps}
(h : ⦃P⦄ x ⦃(fun a => wp⟦f a⟧ Q, Q.2)⦄) :
⦃P⦄ (x >>= f) ⦃Q⦄ := Triple.bind x f h (fun _ => .rfl)
-- @[spec]
theorem Spec.bind {m : Type → Type u} {ps : PostShape} [Monad m] [WPMonad m ps] {α β} {x : m α} {f : α → m β} {Q : PostCond β ps} :
⦃wp⟦x⟧ (fun a => wp⟦f a⟧ Q, Q.2)⦄ (x >>= f) ⦃Q⦄ := Spec.bind' .rfl
-- @[spec]
theorem Spec.map {m : Type → Type u} {ps : PostShape} [Monad m] [WPMonad m ps] {α β} {x : m α} {f : α → β} {Q : PostCond β ps} :
⦃wp⟦x⟧ (fun a => Q.1 (f a), Q.2)⦄ (f <$> x) ⦃Q⦄ := by simp [Triple, SPred.entails.refl]
-- @[spec]
theorem Spec.seq {m : Type → Type u} {ps : PostShape} [Monad m] [WPMonad m ps] {α β} {x : m (α → β)} {y : m α} {Q : PostCond β ps} :
⦃wp⟦x⟧ (fun f => wp⟦y⟧ (fun a => Q.1 (f a), Q.2), Q.2)⦄ (x <*> y) ⦃Q⦄ := by simp [Triple, SPred.entails.refl]
/-! # `MonadLift` -/
-- @[spec]
theorem Spec.monadLift_StateT [Monad m] [WPMonad m ps] (x : m α) (Q : PostCond α (.arg σ ps)) :
⦃fun s => wp⟦x⟧ (fun a => Q.1 a s, Q.2)⦄ (MonadLift.monadLift x : StateT σ m α) ⦃Q⦄ := by simp [Triple, SPred.entails.refl]
-- @[spec]
theorem Spec.monadLift_ReaderT [Monad m] [WPMonad m ps] (x : m α) (Q : PostCond α (.arg ρ ps)) :
⦃fun s => wp⟦x⟧ (fun a => Q.1 a s, Q.2)⦄ (MonadLift.monadLift x : ReaderT ρ m α) ⦃Q⦄ := by simp [Triple, SPred.entails.refl]
-- @[spec]
theorem Spec.monadLift_ExceptT [Monad m] [WPMonad m ps] (x : m α) (Q : PostCond α (.except ε ps)) :
Triple (ps:=.except ε ps)
(MonadLift.monadLift x : ExceptT ε m α)
(wp⟦x⟧ (fun a => Q.1 a, Q.2.2))
Q := by simp [Triple, SPred.entails.refl]
/-! # `MonadLiftT` -/
-- attribute [spec] liftM instMonadLiftTOfMonadLift instMonadLiftT
/-! # `MonadFunctor` -/
-- @[spec]
theorem Spec.monadMap_StateT [Monad m] [WP m ps]
(f : ∀{β}, m β → m β) {α} (x : StateT σ m α) (Q : PostCond α (.arg σ ps)) :
⦃fun s => wp⟦f (x.run s)⟧ (fun (a, s) => Q.1 a s, Q.2)⦄ (MonadFunctor.monadMap (m:=m) f x) ⦃Q⦄ := .rfl
-- @[spec]
theorem Spec.monadMap_ReaderT [Monad m] [WP m ps]
(f : ∀{β}, m β → m β) {α} (x : ReaderT ρ m α) (Q : PostCond α (.arg ρ ps)) :
⦃fun s => wp⟦f (x.run s)⟧ (fun a => Q.1 a s, Q.2)⦄ (MonadFunctor.monadMap (m:=m) f x) ⦃Q⦄ := .rfl
-- @[spec]
theorem Spec.monadMap_ExceptT [Monad m] [WP m ps]
(f : ∀{β}, m β → m β) {α} (x : ExceptT ε m α) (Q : PostCond α (.except ε ps)) :
Triple (ps:=.except ε ps)
(MonadFunctor.monadMap (m:=m) f x)
(wp⟦f x.run⟧ (fun | .ok a => Q.1 a | .error e => Q.2.1 e, Q.2.2))
Q := by simp only [Triple, WP.monadMap_ExceptT]; rfl
/-! # `MonadFunctorT` -/
-- @[spec]
theorem Spec.monadMap_trans [WP o ps] [MonadFunctor n o] [MonadFunctorT m n] :
Triple (ps:=ps)
(MonadFunctorT.monadMap f x : o α)
(wp⟦MonadFunctor.monadMap (m:=n) (MonadFunctorT.monadMap (m:=m) f) x : o α⟧ Q)
Q := by simp [Triple]
-- @[spec]
theorem Spec.monadMap_refl [WP m ps] :
⦃wp⟦f x : m α⟧ Q⦄
(MonadFunctorT.monadMap f x : m α)
⦃Q⦄ := by simp [Triple]
/-! # `ReaderT` -/
-- attribute [spec] ReaderT.run
-- @[spec]
theorem Spec.read_ReaderT [Monad m] [WPMonad m psm] :
⦃fun r => Q.1 r r⦄ (MonadReaderOf.read : ReaderT ρ m ρ) ⦃Q⦄ := by simp [Triple]
-- @[spec]
theorem Spec.withReader_ReaderT [Monad m] [WPMonad m psm] :
⦃fun r => wp⟦x⟧ (fun a _ => Q.1 a r, Q.2) (f r)⦄ (MonadWithReaderOf.withReader f x : ReaderT ρ m α) ⦃Q⦄ := by simp [Triple]
/-! # `StateT` -/
-- attribute [spec] StateT.run
-- @[spec]
theorem Spec.get_StateT [Monad m] [WPMonad m psm] :
⦃fun s => Q.1 s s⦄ (MonadStateOf.get : StateT σ m σ) ⦃Q⦄ := by simp [Triple]
-- @[spec]
theorem Spec.set_StateT [Monad m] [WPMonad m psm] :
⦃fun _ => Q.1 () s⦄ (MonadStateOf.set s : StateT σ m PUnit) ⦃Q⦄ := by simp [Triple]
-- @[spec]
theorem Spec.modifyGet_StateT [Monad m] [WPMonad m ps] :
⦃fun s => Q.1 (f s).1 (f s).2⦄ (MonadStateOf.modifyGet f : StateT σ m α) ⦃Q⦄ := by
simp [Triple]
/-! # `ExceptT` -/
-- @[spec]
theorem Spec.run_ExceptT [WP m ps] (x : ExceptT ε m α) :
Triple (ps:=ps)
(x.run : m (Except ε α))
(wp⟦x⟧ (fun a => Q.1 (.ok a), fun e => Q.1 (.error e), Q.2))
Q := by simp [Triple]
-- @[spec]
theorem Spec.throw_ExceptT [Monad m] [WPMonad m ps] :
⦃Q.2.1 e⦄ (MonadExceptOf.throw e : ExceptT ε m α) ⦃Q⦄ := by
simp [Triple]
-- @[spec]
theorem Spec.tryCatch_ExceptT [Monad m] [WPMonad m ps] (Q : PostCond α (.except ε ps)) :
⦃wp⟦x⟧ (Q.1, fun e => wp⟦h e⟧ Q, Q.2.2)⦄ (MonadExceptOf.tryCatch x h : ExceptT ε m α) ⦃Q⦄ := by
simp [Triple]
/-! # `Except` -/
-- @[spec]
theorem Spec.throw_Except [Monad m] [WPMonad m ps] :
⦃Q.2.1 e⦄ (MonadExceptOf.throw e : Except ε α) ⦃Q⦄ := SPred.entails.rfl
-- @[spec]
theorem Spec.tryCatch_Except (Q : PostCond α (.except ε .pure)) :
⦃wp⟦x⟧ (Q.1, fun e => wp⟦h e⟧ Q, Q.2.2)⦄ (MonadExceptOf.tryCatch x h : Except ε α) ⦃Q⦄ := by
simp [Triple]
/-! # `EStateM` -/
-- @[spec]
theorem Spec.get_EStateM :
⦃fun s => Q.1 s s⦄ (MonadStateOf.get : EStateM ε σ σ) ⦃Q⦄ := SPred.entails.rfl
-- @[spec]
theorem Spec.set_EStateM :
⦃fun _ => Q.1 () s⦄ (MonadStateOf.set s : EStateM ε σ PUnit) ⦃Q⦄ := SPred.entails.rfl
-- @[spec]
theorem Spec.modifyGet_EStateM :
⦃fun s => Q.1 (f s).1 (f s).2⦄ (MonadStateOf.modifyGet f : EStateM ε σ α) ⦃Q⦄ := SPred.entails.rfl
-- @[spec]
theorem Spec.throw_EStateM :
⦃Q.2.1 e⦄ (MonadExceptOf.throw e : EStateM ε σ α) ⦃Q⦄ := SPred.entails.rfl
open EStateM.Backtrackable in
-- @[spec]
theorem Spec.tryCatch_EStateM (Q : PostCond α (.except ε (.arg σ .pure))) :
⦃fun s => wp⟦x⟧ (Q.1, fun e s' => wp⟦h e⟧ Q (restore s' (save s)), Q.2.2) s⦄ (MonadExceptOf.tryCatch x h : EStateM ε σ α) ⦃Q⦄ := by
simp [Triple]
/-! # Lifting `MonadStateOf` -/
-- attribute [spec] modify modifyThe getThe
-- instMonadStateOfMonadStateOf instMonadStateOfOfMonadLift
/-! # Lifting `MonadReaderOf` -/
-- attribute [spec] readThe withTheReader
-- instMonadReaderOfMonadReaderOf instMonadReaderOfOfMonadLift
-- instMonadWithReaderOfMonadWithReaderOf instMonadWithReaderOfOfMonadFunctor
/-! # Lifting `MonadExceptOf` -/
-- attribute [spec] throwThe tryCatchThe
-- @[spec]
theorem Spec.throw_MonadExcept [MonadExceptOf ε m] [WP m _]:
⦃wp⟦MonadExceptOf.throw e : m α⟧ Q⦄ (throw e : m α) ⦃Q⦄ := SPred.entails.rfl
-- @[spec]
theorem Spec.tryCatch_MonadExcept [MonadExceptOf ε m] [WP m ps] (Q : PostCond α ps) :
⦃wp⟦MonadExceptOf.tryCatch x h : m α⟧ Q⦄ (tryCatch x h : m α) ⦃Q⦄ := SPred.entails.rfl
-- @[spec]
theorem Spec.throw_ReaderT [WP m sh] [Monad m] [MonadExceptOf ε m] :
⦃wp⟦MonadLift.monadLift (MonadExceptOf.throw (ε:=ε) e : m α) : ReaderT ρ m α⟧ Q⦄ (MonadExceptOf.throw e : ReaderT ρ m α) ⦃Q⦄ := SPred.entails.rfl
-- @[spec]
theorem Spec.throw_StateT [WP m ps] [Monad m] [MonadExceptOf ε m] (Q : PostCond α (.arg σ ps)) :
⦃wp⟦MonadLift.monadLift (MonadExceptOf.throw (ε:=ε) e : m α) : StateT σ m α⟧ Q⦄ (MonadExceptOf.throw e : StateT σ m α) ⦃Q⦄ := SPred.entails.rfl
-- @[spec]
theorem Spec.throw_ExceptT_lift [WP m ps] [Monad m] [MonadExceptOf ε m] (Q : PostCond α (.except ε' ps)) :
Triple (ps:=.except ε' ps)
(MonadExceptOf.throw e : ExceptT ε' m α)
(wp⟦MonadExceptOf.throw (ε:=ε) e : m (Except ε' α)⟧ (fun | .ok a => Q.1 a | .error e => Q.2.1 e, Q.2.2))
Q := by
simp [Triple]
apply (wp _).mono
simp
intro x
split <;> rfl
-- @[spec]
theorem Spec.tryCatch_ReaderT [WP m ps] [Monad m] [MonadExceptOf ε m] (Q : PostCond α (.arg ρ ps)) :
⦃fun r => wp⟦MonadExceptOf.tryCatch (ε:=ε) (x.run r) (fun e => (h e).run r) : m α⟧ (fun a => Q.1 a r, Q.2)⦄
(MonadExceptOf.tryCatch x h : ReaderT ρ m α)
⦃Q⦄ := SPred.entails.rfl
-- @[spec]
theorem Spec.tryCatch_StateT [WP m ps] [Monad m] [MonadExceptOf ε m] (Q : PostCond α (.arg σ ps)) :
⦃fun s => wp⟦MonadExceptOf.tryCatch (ε:=ε) (x.run s) (fun e => (h e).run s) : m (α × σ)⟧ (fun xs => Q.1 xs.1 xs.2, Q.2)⦄
(MonadExceptOf.tryCatch x h : StateT σ m α)
⦃Q⦄ := SPred.entails.rfl
-- @[spec]
theorem Spec.tryCatch_ExceptT_lift [WP m ps] [Monad m] [MonadExceptOf ε m] (Q : PostCond α (.except ε' ps)) :
Triple
(ps:=.except ε' ps)
(MonadExceptOf.tryCatch x h : ExceptT ε' m α)
(wp⟦MonadExceptOf.tryCatch (ε:=ε) x h : m (Except ε' α)⟧ (fun | .ok a => Q.1 a | .error e => Q.2.1 e, Q.2.2))
Q := by
simp only [Triple, WP.tryCatch_lift_ExceptT]
apply (wp _).mono
simp
intro x
split <;> rfl
/-! # `ForIn` -/
-- @[spec]
theorem Spec.forIn'_list {α : Type} {β : Type} {m : Type → Type v} {ps : PostShape}
[Monad m] [WPMonad m ps]
{xs : List α} {init : β} {f : (a : α) → a ∈ xs → β → m (ForInStep β)}
(inv : PostCond (β × List.Zipper xs) ps)
(step : ∀ b rpref x (hx : x ∈ xs) suff (h : xs = rpref.reverse ++ x :: suff),
⦃inv.1 (b, ⟨rpref, x::suff, by simp [h]⟩)⦄
f x hx b
⦃(fun r => match r with
| .yield b' => inv.1 (b', ⟨x::rpref, suff, by simp [h]⟩)
| .done b' => inv.1 (b', ⟨xs.reverse, [], by simp⟩), inv.2)⦄) :
⦃inv.1 (init, ⟨[], xs, by simp⟩)⦄ forIn' xs init f ⦃(fun b => inv.1 (b, ⟨xs.reverse, [], by simp⟩), inv.2)⦄ := by
suffices h : ∀ rpref suff (h : xs = rpref.reverse ++ suff),
⦃inv.1 (init, ⟨rpref, suff, by simp [h]⟩)⦄
forIn' (m:=m) suff init (fun a ha => f a (by simp[h,ha]))
⦃(fun b => inv.1 (b, ⟨xs.reverse, [], by simp [h]⟩), inv.2)⦄
from h [] xs rfl
intro rpref suff h
induction suff generalizing rpref init
case nil => apply Triple.pure; simp [h]
case cons x suff ih =>
simp only [List.forIn'_cons]
apply Triple.bind
case hx => exact step init rpref x (by simp[h]) suff h
case hf =>
intro r
split
next => apply Triple.pure; simp [h]
next b =>
simp
have := @ih b (x::rpref) (by simp [h])
simp at this
exact this
-- using the postcondition as a constant invariant:
theorem Spec.forIn'_list_const_inv {α : Type} {β : Type} {m : Type → Type v} {ps : PostShape}
[Monad m] [WPMonad m ps]
{xs : List α} {init : β} {f : (a : α) → a ∈ xs → β → m (ForInStep β)}
{inv : PostCond β ps}
(step : ∀ x (hx : x ∈ xs) b,
⦃inv.1 b⦄
f x hx b
⦃(fun r => match r with | .yield b' => inv.1 b' | .done b' => inv.1 b', inv.2)⦄) :
⦃inv.1 init⦄ forIn' xs init f ⦃inv⦄ :=
Spec.forIn'_list (fun p => inv.1 p.1, inv.2) (fun b _ x hx _ _ => step x hx b)
-- @[spec]
theorem Spec.forIn_list {α : Type} {β : Type} {m : Type → Type v} {ps : PostShape}
[Monad m] [WPMonad m ps]
{xs : List α} {init : β} {f : α → β → m (ForInStep β)}
(inv : PostCond (β × List.Zipper xs) ps)
(step : ∀ b rpref x suff (h : xs = rpref.reverse ++ x :: suff),
⦃inv.1 (b, ⟨rpref, x::suff, by simp [h]⟩)⦄
f x b
⦃(fun r => match r with
| .yield b' => inv.1 (b', ⟨x::rpref, suff, by simp [h]⟩)
| .done b' => inv.1 (b', ⟨xs.reverse, [], by simp⟩), inv.2)⦄) :
⦃inv.1 (init, ⟨[], xs, by simp⟩)⦄ forIn xs init f ⦃(fun b => inv.1 (b, ⟨xs.reverse, [], by simp⟩), inv.2)⦄ := by
simp only [← forIn'_eq_forIn]
exact Spec.forIn'_list inv (fun b rpref x _ suff h => step b rpref x suff h)
-- using the postcondition as a constant invariant:
theorem Spec.forIn_list_const_inv {α : Type} {β : Type} {m : Type → Type v} {ps : PostShape}
[Monad m] [WPMonad m ps]
{xs : List α} {init : β} {f : α → β → m (ForInStep β)}
{inv : PostCond β ps}
(step : ∀ hd b,
⦃inv.1 b⦄
f hd b
⦃(fun r => match r with | .yield b' => inv.1 b' | .done b' => inv.1 b', inv.2)⦄) :
⦃inv.1 init⦄ forIn xs init f ⦃inv⦄ :=
Spec.forIn_list (fun p => inv.1 p.1, inv.2) (fun b _ hd _ _ => step hd b)
-- @[spec]
theorem Spec.foldlM_list {α : Type} {β : Type} {m : Type → Type v} {ps : PostShape}
[Monad m] [WPMonad m ps]
{xs : List α} {init : β} {f : β → α → m β}
(inv : PostCond (β × List.Zipper xs) ps)
(step : ∀ b rpref x suff (h : xs = rpref.reverse ++ x :: suff),
⦃inv.1 (b, ⟨rpref, x::suff, by simp [h]⟩)⦄
f b x
⦃(fun b' => inv.1 (b', ⟨x::rpref, suff, by simp [h]⟩), inv.2)⦄) :
⦃inv.1 (init, ⟨[], xs, by simp⟩)⦄ List.foldlM f init xs ⦃(fun b => inv.1 (b, ⟨xs.reverse, [], by simp⟩), inv.2)⦄ := by
have : xs.foldlM f init = forIn xs init (fun a b => .yield <$> f b a) := by
simp only [List.forIn_yield_eq_foldlM, id_map']
rw[this]
apply Spec.forIn_list inv
simp only [Triple, WPMonad.wp_map, PredTrans.map_apply]
exact step
-- using the postcondition as a constant invariant:
theorem Spec.foldlM_list_const_inv {α : Type} {β : Type} {m : Type → Type v} {ps : PostShape}
[Monad m] [WPMonad m ps]
{xs : List α} {init : β} {f : β → α → m β}
{inv : PostCond β ps}
(step : ∀ hd b,
⦃inv.1 b⦄
f b hd
⦃(fun b' => inv.1 b', inv.2)⦄) :
⦃inv.1 init⦄ List.foldlM f init xs ⦃inv⦄ :=
Spec.foldlM_list (fun p => inv.1 p.1, inv.2) (fun b _ hd _ _ => step hd b)
-- @[spec]
theorem Spec.forIn'_range {β : Type} {m : Type → Type v} {ps : PostShape}
[Monad m] [WPMonad m ps]
{xs : Std.Range} {init : β} {f : (a : Nat) → a ∈ xs → β → m (ForInStep β)}
(inv : PostCond (β × List.Zipper xs.toList) ps)
(step : ∀ b rpref x (hx : x ∈ xs) suff (h : xs.toList = rpref.reverse ++ x :: suff),
⦃inv.1 (b, ⟨rpref, x::suff, by simp [h]⟩)⦄
f x hx b
⦃(fun r => match r with
| .yield b' => inv.1 (b', ⟨x::rpref, suff, by simp [h]⟩)
| .done b' => inv.1 (b', ⟨xs.toList.reverse, [], by simp⟩), inv.2)⦄) :
⦃inv.1 (init, ⟨[], xs.toList, by simp⟩)⦄ forIn' xs init f ⦃(fun b => inv.1 (b, ⟨xs.toList.reverse, [], by simp⟩), inv.2)⦄ := by
simp only [Std.Range.forIn'_eq_forIn'_range', Std.Range.size, Std.Range.size.eq_1]
apply Spec.forIn'_list inv (fun b rpref x hx suff h => step b rpref x (Std.Range.mem_of_mem_range' hx) suff h)
-- @[spec]
theorem Spec.forIn_range {β : Type} {m : Type → Type v} {ps : PostShape}
[Monad m] [WPMonad m ps]
{xs : Std.Range} {init : β} {f : Nat → β → m (ForInStep β)}
(inv : PostCond (β × List.Zipper xs.toList) ps)
(step : ∀ b rpref x suff (h : xs.toList = rpref.reverse ++ x :: suff),
⦃inv.1 (b, ⟨rpref, x::suff, by simp [h]⟩)⦄
f x b
⦃(fun r => match r with
| .yield b' => inv.1 (b', ⟨x::rpref, suff, by simp [h]⟩)
| .done b' => inv.1 (b', ⟨xs.toList.reverse, [], by simp⟩), inv.2)⦄) :
⦃inv.1 (init, ⟨[], xs.toList, by simp⟩)⦄ forIn xs init f ⦃(fun b => inv.1 (b, ⟨xs.toList.reverse, [], by simp⟩), inv.2)⦄ := by
simp only [Std.Range.forIn_eq_forIn_range', Std.Range.size]
apply Spec.forIn_list inv step
-- @[spec]
theorem Spec.forIn'_array {α : Type} {β : Type} {m : Type → Type v} {ps : PostShape}
[Monad m] [WPMonad m ps]
{xs : Array α} {init : β} {f : (a : α) → a ∈ xs → β → m (ForInStep β)}
(inv : PostCond (β × List.Zipper xs.toList) ps)
(step : ∀ b rpref x (hx : x ∈ xs) suff (h : xs.toList = rpref.reverse ++ x :: suff),
⦃inv.1 (b, ⟨rpref, x::suff, by simp [h]⟩)⦄
f x hx b
⦃(fun r => match r with
| .yield b' => inv.1 (b', ⟨x::rpref, suff, by simp [h]⟩)
| .done b' => inv.1 (b', ⟨xs.toList.reverse, [], by simp⟩), inv.2)⦄) :
⦃inv.1 (init, ⟨[], xs.toList, by simp⟩)⦄ forIn' xs init f ⦃(fun b => inv.1 (b, ⟨xs.toList.reverse, [], by simp⟩), inv.2)⦄ := by
cases xs
simp
apply Spec.forIn'_list inv (fun b rpref x hx suff h => step b rpref x (by simp[hx]) suff h)
-- @[spec]
theorem Spec.forIn_array {α : Type} {β : Type} {m : Type → Type v} {ps : PostShape}
[Monad m] [WPMonad m ps]
{xs : Array α} {init : β} {f : α → β → m (ForInStep β)}
(inv : PostCond (β × List.Zipper xs.toList) ps)
(step : ∀ b rpref x suff (h : xs.toList = rpref.reverse ++ x :: suff),
⦃inv.1 (b, ⟨rpref, x::suff, by simp [h]⟩)⦄
f x b
⦃(fun r => match r with
| .yield b' => inv.1 (b', ⟨x::rpref, suff, by simp [h]⟩)
| .done b' => inv.1 (b', ⟨xs.toList.reverse, [], by simp⟩), inv.2)⦄) :
⦃inv.1 (init, ⟨[], xs.toList, by simp⟩)⦄ forIn xs init f ⦃(fun b => inv.1 (b, ⟨xs.toList.reverse, [], by simp⟩), inv.2)⦄ := by
cases xs
simp
apply Spec.forIn_list inv step
-- @[spec]
theorem Spec.foldlM_array {α : Type} {β : Type} {m : Type → Type v} {ps : PostShape}
[Monad m] [WPMonad m ps]
{xs : Array α} {init : β} {f : β → α → m β}
(inv : PostCond (β × List.Zipper xs.toList) ps)
(step : ∀ b rpref x suff (h : xs.toList = rpref.reverse ++ x :: suff),
⦃inv.1 (b, ⟨rpref, x::suff, by simp [h]⟩)⦄
f b x
⦃(fun b' => inv.1 (b', ⟨x::rpref, suff, by simp [h]⟩), inv.2)⦄) :
⦃inv.1 (init, ⟨[], xs.toList, by simp⟩)⦄ Array.foldlM f init xs ⦃(fun b => inv.1 (b, ⟨xs.toList.reverse, [], by simp⟩), inv.2)⦄ := by
cases xs
simp
apply Spec.foldlM_list inv step

10
src/Std/Do/WP.lean Normal file
View file

@ -0,0 +1,10 @@
/-
Copyright (c) 2025 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sebastian Graf
-/
prelude
import Std.Do.WP.Basic
import Std.Do.WP.Monad
import Std.Do.WP.IO
import Std.Do.WP.SimpLemmas

111
src/Std/Do/WP/Basic.lean Normal file
View file

@ -0,0 +1,111 @@
/-
Copyright (c) 2025 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sebastian Graf
-/
prelude
import Std.Do.PredTrans
/-!
# Weakest precondition interpretation
This module defines the weakest precondition interpretation `WP` of monadic programs
in terms of predicate transformers `PredTrans`.
This interpretation forms the basis of our notion of Hoare triples.
It is the main mechanism of this library for reasoning about monadic programs.
An instance `WP m ps` determines an interpretation `wp⟦x⟧` of a program `x : m α` in terms of a
predicate transformer `PredTrans ps α`; The monad `m` determines `ps : PostShape` and hence
the particular shape of the predicate transformer.
This library comes with pre-defined instances for common monads and transformers such as
* `WP Id .pure`, interpreting pure computations `x : Id α` in terms of a function (isomorphic to)
`(α → Prop) → Prop`.
* `WP (StateT σ m) (.arg σ ps)` given an instance `WP m ps`, interpreting `StateM σ α` in terms of
a function `(ασ → Prop) → σ → Prop`.
* `WP (ExceptT ε m) (.except ε ps)` given an instance `WP m ps`, interpreting `Except ε α` in terms
of `(α → Prop) → (ε → Prop) → Prop`.
* `WP (EStateM ε σ) (.except ε (.arg σ .pure))` interprets `EStateM ε σ α` in terms of
a function `(ασ → Prop) → (ε → σ → Prop) → σ → Prop`.
These instances are all monad morphisms, a fact which is properly encoded and exploited
by the subclass `WPMonad`.
-/
namespace Std.Do
universe u
variable {m : Type → Type u}
/--
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.
-/
class WP (m : Type → Type u) (ps : outParam PostShape) where
wp {α} (x : m α) : PredTrans ps α
export WP (wp)
scoped syntax:max "wp⟦" term:min (":" term)? "⟧" : term
macro_rules
| `(wp⟦$x:term⟧) => `((WP.wp $x).apply)
| `(wp⟦$x:term : $ty⟧) => `((WP.wp ($x : $ty)).apply)
@[app_unexpander PredTrans.apply]
protected def unexpandWP : Lean.PrettyPrinter.Unexpander
| `($_ $e) => match e with
| `(wp ($x : $ty)) => `(wp⟦$x : $ty⟧)
| `(wp $e) => `(wp⟦$e⟧)
| _ => throw ()
| _ => throw ()
instance Id.instWP : WP Id .pure where
wp x := PredTrans.pure x.run
instance StateT.instWP [WP m ps] : WP (StateT σ m) (.arg σ ps) where
wp x := PredTrans.pushArg (fun s => wp (x s))
instance ReaderT.instWP [WP m ps] : WP (ReaderT ρ m) (.arg ρ ps) where
wp x := PredTrans.pushArg (fun s => (·, s) <$> wp (x s))
instance ExceptT.instWP [WP m ps] : WP (ExceptT ε m) (.except ε ps) where
wp x := PredTrans.pushExcept (wp x)
instance EStateM.instWP : WP (EStateM ε σ) (.except ε (.arg σ .pure)) where
wp x := -- Could define as PredTrans.mkExcept (PredTrans.modifyGetM (fun s => pure (EStateM.Result.toExceptState (x s))))
{ apply := fun Q s => match x s with
| .ok a s' => Q.1 a s'
| .error e s' => Q.2.1 e s'
conjunctive := by
intro _ _
apply SPred.bientails.of_eq
ext s
dsimp
cases (x s) <;> simp
}
instance State.instWP : WP (StateM σ) (.arg σ .pure) :=
inferInstanceAs (WP (StateT σ Id) (.arg σ .pure))
instance Reader.instWP : WP (ReaderM ρ) (.arg ρ .pure) :=
inferInstanceAs (WP (ReaderT ρ Id) (.arg ρ .pure))
instance Except.instWP : WP (Except ε) (.except ε .pure) :=
inferInstanceAs (WP (ExceptT ε Id) (.except ε .pure))
theorem Id.by_wp {α} {x : α} {prog : Id α} (h : Id.run prog = x) (P : α → Prop) :
wp⟦prog⟧ (PostCond.total P) → P x := h ▸ id
theorem StateM.by_wp {α} {x : α × σ} {prog : StateM σ α} (h : StateT.run prog s = x) (P : α × σ → Prop) :
wp⟦prog⟧ (PostCond.total (fun a s' => P (a, s'))) s → P x := by
intro hspec
simp only [wp, PredTrans.pure, PredTrans.pushArg_apply] at hspec
exact h ▸ hspec
theorem EStateM.by_wp {α} {x : EStateM.Result ε σ α} {prog : EStateM ε σ α} (h : EStateM.run prog s = x) (P : EStateM.Result ε σ α → Prop) :
wp⟦prog⟧ (PostCond.total (fun a s' => P (EStateM.Result.ok a s'))) s → P x := by
intro hspec
simp only [wp, FailConds.false, FailConds.const, SVal.curry_cons, SVal.curry_nil] at hspec
split at hspec
case h_1 a s' heq => rw[← heq] at hspec; exact h ▸ hspec
case h_2 => contradiction

46
src/Std/Do/WP/IO.lean Normal file
View file

@ -0,0 +1,46 @@
/-
Copyright (c) 2025 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sebastian Graf
-/
prelude
import Init.System.IO
import Std.Do.WP.Monad
/-!
# Barebones `WP` instance for `IO`
This module defines a `WP` instance for `IO` without any synthetic model of the `IO.RealWorld` whatsoever.
This is useful for reasoning about `IO` programs when the precise result of a side-effect is irrelevant;
for example it can be used to reason about random number generation.
It is however inadequate for reasoning about, e.g., `IO.Ref`s.
-/
namespace Std.Do.IO.Bare
open Std.Do
/--
This is pretty much the instance for `EStateM` specialized to `σ = IO.RealWorld`.
However, `IO.RealWorld` is ommitted in the `PredShape`.
-/
scoped instance instWP : WP (EIO ε) (.except ε .pure) where
wp x := -- Could define as PredTrans.mkExcept (PredTrans.modifyGetM (fun s => pure (EStateM.Result.toExceptState (x s))))
{ apply := fun Q => match x () with
| .ok a _rw => Q.1 a
| .error e _rw => Q.2.1 e
conjunctive := by
intro _ _
apply SPred.bientails.of_eq
dsimp
cases (x ()) <;> rfl
}
instance instLawfulMonad : LawfulMonad (EIO ε) := inferInstanceAs (LawfulMonad (EStateM ε IO.RealWorld))
scoped instance instWPMonad : WPMonad (EIO ε) (.except ε .pure) where
wp_pure a := by simp only [wp, pure, EStateM.pure, PredTrans.pure]
wp_bind x f := by
ext Q : 2
simp only [wp, bind, EStateM.bind, PredTrans.bind]
cases (x ()) <;> rfl

81
src/Std/Do/WP/Monad.lean Normal file
View file

@ -0,0 +1,81 @@
/-
Copyright (c) 2025 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sebastian Graf
-/
prelude
import Std.Do.WP.Basic
/-!
# Monad morphisms and weakest precondition interpretations
A `WP m ps` is a `WPMonad m ps` if the interpretation `WP.wp` is also a monad morphism, that is,
it preserves `pure` and `bind`.
-/
namespace Std.Do
universe u
variable {m : Type → Type u} {ps : PostShape}
/--
A `WP` that is also a monad morphism, preserving `pure` and `bind`. (They all are.)
-/
class WPMonad (m : Type → Type u) (ps : outParam PostShape) [Monad m]
extends LawfulMonad m, WP m ps where
wp_pure : ∀ {α} (a : α), wp (pure a) = pure a
wp_bind : ∀ {α β} (x : m α) (f : α → m β), wp (do let a ← x; f a) = do let a ← wp x; wp (f a)
theorem WPMonad.wp_map [Monad m] [WPMonad m ps] (f : α → β) (x : m α) :
wp (f <$> x) = f <$> wp x := by simp [← bind_pure_comp, wp_pure, wp_bind]
theorem WPMonad.wp_seq [Monad m] [WPMonad m ps] (f : m (α → β)) (x : m α) :
wp (f <*> x) = wp f <*> wp x := by simp [← bind_map, wp_map, wp_bind]
open WPMonad
instance Id.instWPMonad : WPMonad Id .pure where
wp_pure a := by simp only [wp, PredTrans.pure, Pure.pure, Id.run]
wp_bind x f := by simp only [wp, PredTrans.pure, Bind.bind, Id.run, PredTrans.bind]
instance StateT.instWPMonad [Monad m] [WPMonad m ps] : WPMonad (StateT σ m) (.arg σ ps) where
wp_pure a := by ext; simp only [wp, pure, StateT.pure, WPMonad.wp_pure, PredTrans.pure,
PredTrans.pushArg_apply]
wp_bind x f := by ext; simp only [wp, bind, StateT.bind, WPMonad.wp_bind, PredTrans.bind,
PredTrans.pushArg_apply]
instance ReaderT.instWPMonad [Monad m] [WPMonad m ps] : WPMonad (ReaderT ρ m) (.arg ρ ps) where
wp_pure a := by ext; simp only [wp, pure, ReaderT.pure, WPMonad.wp_pure, PredTrans.pure,
PredTrans.pushArg_apply, PredTrans.map_apply]
wp_bind x f := by ext; simp only [wp, bind, ReaderT.bind, WPMonad.wp_bind, PredTrans.bind,
PredTrans.pushArg_apply, PredTrans.map_apply]
instance ExceptT.instWPMonad [Monad m] [WPMonad m ps] : WPMonad (ExceptT ε m) (.except ε ps) where
wp_pure a := by ext; simp only [wp, pure, ExceptT.pure, ExceptT.mk, WPMonad.wp_pure,
PredTrans.pure, PredTrans.pushExcept_apply]
wp_bind x f := by
ext Q
simp only [wp, bind, ExceptT.bind, ExceptT.mk, WPMonad.wp_bind, PredTrans.bind,
ExceptT.bindCont, PredTrans.pushExcept_apply]
congr
ext b
cases b
case error a => simp [wp_pure]
case ok a => rfl
instance EStateM.instWPMonad : WPMonad (EStateM ε σ) (.except ε (.arg σ .pure)) where
wp_pure a := by simp only [wp, pure, EStateM.pure, PredTrans.pure]
wp_bind x f := by
ext Q : 2
simp only [wp, bind, EStateM.bind, PredTrans.bind]
ext s : 1
cases (x s) <;> rfl
instance Except.instWPMonad : WPMonad (Except ε) (.except ε .pure) where
wp_pure a := rfl
wp_bind x f := by cases x <;> rfl
instance State.instWPMonad : WPMonad (StateM σ) (.arg σ .pure) :=
inferInstanceAs (WPMonad (StateT σ Id) (.arg σ .pure))
instance Reader.instWPMonad : WPMonad (ReaderM ρ) (.arg ρ .pure) :=
inferInstanceAs (WPMonad (ReaderT ρ Id) (.arg ρ .pure))

View file

@ -0,0 +1,374 @@
/-
Copyright (c) 2025 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sebastian Graf
-/
prelude
import Std.Do.WP.Basic
import Std.Do.WP.Monad
/-!
# Simp lemmas for working with weakest preconditions
Many weakest preconditions are so simple that we can compute them directly or
express them in terms of "simpler" weakest preconditions.
This module provides simp lemmas targeting expressions of the form `wp⟦x⟧ Q`
and rewrites them to expressions of simpler weakest preconditions.
-/
namespace Std.Do.WP
open WPMonad
universe u
variable {m : Type → Type u} {ps : PostShape}
/-! ## `WP` -/
@[simp]
theorem ReaderT_run [WP m ps] (x : ReaderT ρ m α) :
wp⟦x.run r⟧ Q = wp⟦x⟧ (fun a _ => Q.1 a, Q.2) r := rfl
@[simp]
theorem StateT_run [WP m ps] (x : StateT σ m α) :
wp⟦x.run s⟧ Q = wp⟦x⟧ (fun a s => Q.1 (a, s), Q.2) s := rfl
@[simp]
theorem ExceptT_run [WP m ps] (x : ExceptT ε m α) :
wp⟦x.run⟧ Q = wp⟦x⟧ (fun a => Q.1 (.ok a), fun e => Q.1 (.error e), Q.2) := by
simp [wp, ExceptT.run]
congr
(ext x; cases x) <;> rfl
/-! ## `WPMonad` -/
@[simp]
theorem pure [Monad m] [WPMonad m ps] (a : α) (Q : PostCond α ps) :
wp⟦pure (f:=m) a⟧ Q = Q.1 a := by simp [WPMonad.wp_pure]
@[simp]
theorem bind [Monad m] [WPMonad m ps] (x : m α) (f : α → m β) (Q : PostCond β ps) :
wp⟦x >>= f⟧ Q = wp⟦x⟧ (fun a => wp⟦f a⟧ Q, Q.2) := by simp [WPMonad.wp_bind]
@[simp]
theorem map [Monad m] [WPMonad m ps] (f : α → β) (x : m α) (Q : PostCond β ps) :
wp⟦f <$> x⟧ Q = wp⟦x⟧ (fun a => Q.1 (f a), Q.2) := by simp [WPMonad.wp_map]
@[simp]
theorem seq [Monad m] [WPMonad m ps] (f : m (α → β)) (x : m α) (Q : PostCond β ps) :
wp⟦f <*> x⟧ Q = wp⟦f⟧ (fun f => wp⟦x⟧ (fun a => Q.1 (f a), Q.2), Q.2) := by simp [WPMonad.wp_seq]
/-! ## `MonadLift`
The definitions that follow interpret `liftM` and thus instances of, e.g., `MonadStateOf`.
-/
section MonadLift
@[simp]
theorem monadLift_StateT [Monad m] [WPMonad m ps] (x : m α) (Q : PostCond α (.arg σ ps)) :
wp⟦MonadLift.monadLift x : StateT σ m α⟧ Q = fun s => wp⟦x⟧ (fun a => Q.1 a s, Q.2) := by simp [wp, MonadLift.monadLift, StateT.lift]
@[simp]
theorem monadLift_ReaderT [Monad m] [WPMonad m ps] (x : m α) (Q : PostCond α (.arg ρ ps)) :
wp⟦MonadLift.monadLift x : ReaderT ρ m α⟧ Q = fun s => wp⟦x⟧ (fun a => Q.1 a s, Q.2) := rfl
@[simp]
theorem monadLift_ExceptT [Monad m] [WPMonad m ps] (x : m α) (Q : PostCond α (.except ε ps)) :
wp⟦MonadLift.monadLift x : ExceptT ε m α⟧ Q = wp⟦x⟧ (fun a => Q.1 a, Q.2.2) := by
simp [wp, MonadLift.monadLift, ExceptT.lift, ExceptT.mk]
@[simp]
theorem monadLift_trans [WP o ps] [MonadLift n o] [MonadLiftT m n] :
wp⟦MonadLiftT.monadLift x : o α⟧ Q = wp⟦MonadLift.monadLift (m:=n) (MonadLiftT.monadLift (m:=m) x) : o α⟧ Q := rfl
@[simp]
theorem monadLift_refl [WP m ps] :
wp⟦MonadLiftT.monadLift x : m α⟧ Q = wp⟦x : m α⟧ Q := rfl
-- instances
@[simp]
theorem lift_StateT [WP m ps] [Monad m] (x : m α) :
wp⟦StateT.lift x : StateT σ m α⟧ Q = wp⟦MonadLift.monadLift x : StateT σ m α⟧ Q := rfl
@[simp]
theorem lift_ExceptT [WP m ps] [Monad m] (x : m α) :
wp⟦ExceptT.lift x : ExceptT ε m α⟧ Q = wp⟦MonadLift.monadLift x : ExceptT ε m α⟧ Q := rfl
-- MonadReader
@[simp]
theorem read_MonadReaderOf [MonadReaderOf ρ m] [MonadLift m n] [WP n _] :
wp⟦MonadReaderOf.read : n ρ⟧ Q = wp⟦MonadLift.monadLift (MonadReader.read : m ρ) : n ρ⟧ Q := rfl
@[simp]
theorem readThe [MonadReaderOf ρ m] [WP m ps] :
wp⟦readThe ρ : m ρ⟧ Q = wp⟦MonadReaderOf.read : m ρ⟧ Q := rfl
@[simp]
theorem read_MonadReader [MonadReaderOf ρ m] [WP m ps] :
wp⟦MonadReader.read : m ρ⟧ Q = wp⟦MonadReaderOf.read : m ρ⟧ Q := rfl
-- MonadState
@[simp]
theorem get_MonadStateOf [MonadStateOf σ m] [MonadLift m n] [WP n _] :
wp⟦MonadStateOf.get : n σ⟧ Q = wp⟦MonadLift.monadLift (MonadStateOf.get : m σ) : n σ⟧ Q := rfl
@[simp]
theorem get_MonadState [WP m ps] [MonadStateOf σ m] :
wp⟦MonadState.get : m σ⟧ Q = wp⟦MonadStateOf.get : m σ⟧ Q := rfl
@[simp]
theorem getThe_MonadStateOf [WP m ps] [MonadStateOf σ m] :
wp⟦getThe σ : m σ⟧ Q = wp⟦MonadStateOf.get : m σ⟧ Q := rfl
@[simp]
theorem set_MonadStateOf [MonadStateOf σ m] [MonadLift m n] [WP n _] :
wp⟦MonadStateOf.set x : n PUnit⟧ Q = wp⟦MonadLift.monadLift (MonadStateOf.set (σ:=σ) x : m PUnit) : n PUnit⟧ Q := rfl
@[simp]
theorem set_MonadState [WP m ps] [MonadStateOf σ m] :
wp⟦MonadState.set x : m PUnit⟧ Q = wp⟦MonadStateOf.set x : m PUnit⟧ Q := rfl
@[simp]
theorem modifyGet_MonadStateOf [MonadStateOf σ m] [MonadLift m n] [WP n _]
(f : σα × σ) :
wp⟦MonadStateOf.modifyGet f : n α⟧ Q = wp⟦MonadLift.monadLift (MonadState.modifyGet f : m α) : n α⟧ Q := rfl
@[simp]
theorem modifyGet_MonadState [WP m ps] [MonadStateOf σ m] (f : σα × σ) :
wp⟦MonadState.modifyGet f : m α⟧ Q = wp⟦MonadStateOf.modifyGet f : m α⟧ Q := rfl
@[simp]
theorem modifyGetThe_MonadStateOf [WP m ps] [MonadStateOf σ m] (f : σα × σ) :
wp⟦modifyGetThe σ f : m α⟧ Q = wp⟦MonadStateOf.modifyGet f : m α⟧ Q := rfl
@[simp]
theorem modify_MonadStateOf [WP m ps] [MonadStateOf σ m] (f : σσ) :
wp⟦modify f : m PUnit⟧ Q = wp⟦MonadStateOf.modifyGet fun s => ((), f s) : m PUnit⟧ Q := rfl
@[simp]
theorem modifyThe_MonadStateOf [WP m ps] [MonadStateOf σ m] (f : σσ) :
wp⟦modifyThe σ f : m PUnit⟧ Q = wp⟦MonadStateOf.modifyGet fun s => ((), f s) : m PUnit⟧ Q := rfl
-- instances
@[simp]
theorem read_ReaderT [Monad m] [WPMonad m ps] :
wp⟦MonadReaderOf.read : ReaderT ρ m ρ⟧ Q = fun r => Q.1 r r := by
simp [wp, MonadReaderOf.read, ReaderT.read]
@[simp]
theorem get_StateT [Monad m] [WPMonad m ps] :
wp⟦MonadStateOf.get : StateT σ m σ⟧ Q = fun s => Q.1 s s := by
simp [wp, MonadStateOf.get, StateT.get]
@[simp]
theorem set_StateT [Monad m] [WPMonad m ps] (x : σ) :
wp⟦MonadStateOf.set x : StateT σ m PUnit⟧ Q = fun _ => Q.1 ⟨⟩ x := by
simp [wp, set, StateT.set]
@[simp]
theorem modifyGet_StateT [Monad m] [WPMonad m ps] (f : σα × σ) :
wp⟦MonadStateOf.modifyGet f : StateT σ m α⟧ Q = fun s => Q.1 (f s).1 (f s).2 := by
simp [wp, MonadStateOf.modifyGet, StateT.modifyGet]
@[simp]
theorem get_EStateM :
wp⟦MonadStateOf.get : EStateM ε σ σ⟧ Q = fun s => Q.1 s s := by
simp [wp, MonadStateOf.get, EStateM.get]
@[simp]
theorem set_EStateM (x : σ) :
wp⟦MonadStateOf.set x : EStateM ε σ PUnit⟧ Q = fun _ => Q.1 ⟨⟩ x := by
simp [wp, set, EStateM.set]
@[simp]
theorem modifyGet_EStateM (f : σα × σ) :
wp⟦MonadStateOf.modifyGet f : EStateM ε σ α⟧ Q = fun s => Q.1 (f s).1 (f s).2 := by
simp [wp, MonadStateOf.modifyGet, EStateM.modifyGet]
end MonadLift
/-! ## `MonadFunctor`
The definitions that follow interpret `monadMap` and thus instances of, e.g., `MonadReaderWithOf`.
-/
section MonadFunctor
open MonadFunctor renaming monadMap → mmap
-- The following 3 theorems are analogous to *.monadLift_apply.
-- In the past, we experimented with a more tricky definition by rewriting to special monadMap defns on PredTrans, involving
-- wp1 : (∀ {α}, m α → m α) → PredTrans ps α → PredTrans ps α
-- that enjoys quite a tricky definition.
-- However, we found that relying on specialised lemmas is both much simpler and more reliable.
@[simp]
theorem monadMap_StateT (m : Type → Type u) [Monad m] [WP m ps]
(f : ∀{β}, m β → m β) {α} (x : StateT σ m α) (Q : PostCond α (.arg σ ps)) :
wp⟦mmap (m:=m) f x⟧ Q = fun s => wp⟦f (x.run s)⟧ (fun (a, s) => Q.1 a s, Q.2) := by
simp [wp, MonadFunctor.monadMap, StateT.run]
@[simp]
theorem monadMap_ReaderT (m : Type → Type u) [Monad m] [WP m ps]
(f : ∀{β}, m β → m β) {α} (x : ReaderT ρ m α) (Q : PostCond α (.arg ρ ps)) :
wp⟦mmap (m:=m) f x⟧ Q = fun s => wp⟦f (x.run s)⟧ (fun a => Q.1 a s, Q.2) := by
simp [wp, MonadFunctor.monadMap, ReaderT.run]
@[simp]
theorem monadMap_ExceptT (m : Type → Type u) [Monad m] [WP m ps]
(f : ∀{β}, m β → m β) {α} (x : ExceptT ε m α) (Q : PostCond α (.except ε ps)) :
wp⟦mmap (m:=m) f x⟧ Q = wp⟦f x.run⟧ (fun | .ok a => Q.1 a | .error e => Q.2.1 e, Q.2.2) := by
simp [wp, MonadFunctor.monadMap, ExceptT.run]
congr; ext; split <;> rfl
@[simp]
theorem monadMap_trans [WP o ps] [MonadFunctor n o] [MonadFunctorT m n] :
wp⟦MonadFunctorT.monadMap f x : o α⟧ Q = wp⟦MonadFunctor.monadMap (m:=n) (MonadFunctorT.monadMap (m:=m) f) x : o α⟧ Q := rfl
@[simp]
theorem monadMap_refl [WP m ps] :
wp⟦MonadFunctorT.monadMap f x : m α⟧ Q = wp⟦f x : m α⟧ Q := rfl
@[simp]
theorem withReader_ReaderT [WP m ps] :
wp⟦MonadWithReaderOf.withReader f x : ReaderT ρ m α⟧ Q = fun r => wp⟦x⟧ (fun a _ => Q.1 a r, Q.2) (f r) := rfl
@[simp]
theorem withReader_MonadWithReaderOf [MonadWithReaderOf ρ m] [WP n nsh] [MonadFunctor m n] (f : ρρ) (x : n α) :
wp⟦MonadWithReaderOf.withReader f x⟧ Q = wp⟦mmap (m:=m) (MonadWithReaderOf.withReader f) x⟧ Q := rfl
@[simp]
theorem withReader_MonadWithReader [MonadWithReaderOf ρ m] [WP m ps] (f : ρρ) (x : m α) :
wp⟦MonadWithReader.withReader f x⟧ Q = wp⟦MonadWithReaderOf.withReader f x⟧ Q := rfl
@[simp]
theorem withTheReader [MonadWithReaderOf ρ m] [WP m ps] (f : ρρ) (x : m α) :
wp⟦withTheReader ρ f x⟧ Q = wp⟦MonadWithReaderOf.withReader f x⟧ Q := rfl
end MonadFunctor
/-! ## `MonadExceptOf`
The definitions that follow interpret `throw`, `throwThe`, `tryCatch`, etc.
Since `MonadExceptOf` cannot be lifted through `MonadLift` or `MonadFunctor`, there is one lemma per
`MonadExceptOf` operation and instance to lift through; the classic m*n instances problem.
-/
section MonadExceptOf
@[simp]
theorem throw_MonadExcept [MonadExceptOf ε m] [WP m ps] :
wp⟦throw e : m α⟧ Q = wp⟦MonadExceptOf.throw e : m α⟧ Q := rfl
@[simp]
theorem throwThe [MonadExceptOf ε m] [WP m ps] :
wp⟦throwThe ε e : m α⟧ Q = wp⟦MonadExceptOf.throw e : m α⟧ Q := rfl
@[simp]
theorem throw_Except :
wp⟦MonadExceptOf.throw e : Except ε α⟧ Q = Q.2.1 e := by
simp [wp, MonadExceptOf.throw, Id.run]
@[simp]
theorem throw_ExceptT [Monad m] [WPMonad m ps] :
wp⟦MonadExceptOf.throw e : ExceptT ε m α⟧ Q = Q.2.1 e := by
simp [wp, MonadExceptOf.throw, ExceptT.mk]
@[simp]
theorem throw_EStateM :
wp⟦MonadExceptOf.throw e : EStateM ε σ α⟧ Q = Q.2.1 e := by
simp [wp, MonadExceptOf.throw, EStateM.throw]
@[simp]
theorem throw_ReaderT [WP m sh] [Monad m] [MonadExceptOf ε m] :
wp⟦MonadExceptOf.throw (ε:=ε) e : ReaderT ρ m α⟧ Q = wp⟦MonadLift.monadLift (MonadExceptOf.throw (ε:=ε) e : m α) : ReaderT ρ m α⟧ Q := rfl
@[simp]
theorem throw_StateT [WP m sh] [Monad m] [MonadExceptOf ε m] :
wp⟦MonadExceptOf.throw (ε:=ε) e : StateT ρ m α⟧ Q = wp⟦MonadLift.monadLift (MonadExceptOf.throw (ε:=ε) e : m α) : StateT ρ m α⟧ Q := rfl
-- The following lemma is structurally different to StateT and others because of weird definitions
-- for lifting throw
@[simp]
theorem throw_lift_ExceptT [WP m sh] [Monad m] [MonadExceptOf ε m] :
wp⟦MonadExceptOf.throw (ε:=ε) e : ExceptT ε' m α⟧ Q = wp⟦MonadExceptOf.throw (ε:=ε) e : m (Except ε' α)⟧ (fun | .ok a => Q.1 a | .error e => Q.2.1 e, Q.2.2) := by
simp only [wp, MonadExceptOf.throw, PredTrans.pushExcept_apply]
congr
ext x
split <;> rfl
@[simp]
theorem tryCatch_MonadExcept [MonadExceptOf ε m] [WP m ps] :
wp⟦tryCatch x h : m α⟧ Q = wp⟦MonadExceptOf.tryCatch x h : m α⟧ Q := rfl
@[simp]
theorem tryCatchThe [MonadExceptOf ε m] [WP m ps] :
wp⟦tryCatchThe ε x h : m α⟧ Q = wp⟦MonadExceptOf.tryCatch x h : m α⟧ Q := rfl
@[simp]
theorem tryCatch_Except :
wp⟦MonadExceptOf.tryCatch x h : Except ε α⟧ Q = wp⟦x⟧ (Q.1, fun e => wp⟦h e⟧ Q, Q.2.2) := by
simp only [wp, PredTrans.pure, Id.run, MonadExceptOf.tryCatch, Except.tryCatch,
PredTrans.pushExcept_apply]
cases x <;> simp
@[simp]
theorem tryCatch_ExceptT [Monad m] [WPMonad m ps] :
wp⟦MonadExceptOf.tryCatch x h : ExceptT ε m α⟧ Q = wp⟦x⟧ (Q.1, fun e => wp⟦h e⟧ Q, Q.2.2) := by
simp only [wp, MonadExceptOf.tryCatch, ExceptT.tryCatch, ExceptT.mk, bind, PredTrans.pushExcept_apply]
congr
ext x
split <;> simp
open EStateM.Backtrackable in
@[simp]
theorem tryCatch_EStateM {ε σ δ α x h Q} [EStateM.Backtrackable δ σ]:
wp⟦MonadExceptOf.tryCatch x h : EStateM ε σ α⟧ Q = fun s => wp⟦x⟧ (Q.1, fun e s' => wp⟦h e⟧ Q (restore s' (save s)), Q.2.2) s := by
ext s
simp only [wp, MonadExceptOf.tryCatch, EStateM.tryCatch]
cases x s <;> simp
@[simp]
theorem tryCatch_ReaderT [WP m sh] [Monad m] [MonadExceptOf ε m] :
wp⟦MonadExceptOf.tryCatch (ε:=ε) x h : ReaderT ρ m α⟧ Q = fun r => wp⟦MonadExceptOf.tryCatch (ε:=ε) (x.run r) (fun e => (h e).run r) : m α⟧ (fun a => Q.1 a r, Q.2) := by
simp [wp, MonadExceptOf.tryCatch, tryCatchThe, ReaderT.run]
@[simp]
theorem tryCatch_StateT [WP m sh] [Monad m] [MonadExceptOf ε m] :
wp⟦MonadExceptOf.tryCatch (ε:=ε) x h : StateT σ m α⟧ Q = fun s => wp⟦MonadExceptOf.tryCatch (ε:=ε) (x.run s) (fun e => (h e).run s) : m (α × σ)⟧ (fun xs => Q.1 xs.1 xs.2, Q.2) := by
simp [wp, MonadExceptOf.tryCatch, tryCatchThe, StateT.run]
@[simp]
theorem tryCatch_lift_ExceptT [WP m sh] [Monad m] [MonadExceptOf ε m] :
wp⟦MonadExceptOf.tryCatch (ε:=ε) x h : ExceptT ε' m α⟧ Q = wp⟦MonadExceptOf.tryCatch (ε:=ε) x h : m (Except ε' α)⟧ (fun | .ok a => Q.1 a | .error e => Q.2.1 e, Q.2.2) := by
simp only [wp, MonadExceptOf.tryCatch, tryCatchThe, PredTrans.pushExcept_apply, ExceptT.mk]
congr
ext x
split <;> rfl
/-
example :
wp (m:= ReaderT Char (StateT Bool (ExceptT Nat Id))) (do set true; throw 42; set false; get) =
wp (m:= ReaderT Char (StateT Bool (ExceptT Nat Id))) (do set true; throw 42; get) := by
ext Q : 4
simp
example :
wp (m:= ReaderT Char (StateT Bool (ExceptT Nat Id))) (do try { set true; throw 42 } catch _ => set false; get) =
wp (m:= ReaderT Char (StateT Bool (ExceptT Nat Id))) (do set false; get) := by
ext Q : 4
simp
-- This gets stuck because ExceptT.instMonad is not defeq to Except.instMonad and thus the `bind` rewrite does not apply.
admit
-/
end MonadExceptOf

View file

@ -6,7 +6,25 @@ Authors: Sebastian Graf
prelude
import Init.NotationExtra
namespace Lean.Parser.Tactic
namespace Lean.Parser
namespace Attr
/--
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.
-/
syntax (name := specAttr) "spec" (Tactic.simpPre <|> Tactic.simpPost)? patternIgnore("← " <|> "<- ")? (ppSpace prio)? : attr
end Attr
namespace Tactic
@[inherit_doc Lean.Parser.Tactic.massumptionMacro]
syntax (name := massumption) "massumption" : tactic
@ -152,3 +170,46 @@ syntax "∀" binderIdent : mintroPat
@[inherit_doc Lean.Parser.Tactic.mintroMacro]
syntax (name := mintro) "mintro" (ppSpace colGt mintroPat)+ : tactic
@[inherit_doc Lean.Parser.Tactic.mspecMacro]
syntax (name := mspec) "mspec" (ppSpace colGt term)? : tactic
/--
Like `mspec`, but does not attempt slight simplification and closing of trivial sub-goals.
`mspec $spec` is roughly (the set of simp lemmas below might not be up to date)
```
mspec_no_simp $spec
all_goals
((try simp only [SPred.true_intro_simp, SPred.true_intro_simp_nil, SVal.curry_cons,
SVal.uncurry_cons, SVal.getThe_here, SVal.getThe_there]);
(try mpure_intro; trivial))
```
-/
syntax (name := mspecNoSimp) "mspec_no_simp" (ppSpace colGt term)? : tactic
/--
`mspec_no_simp $spec` first tries to decompose `Bind.bind`s before applying `$spec`.
This variant of `mspec_no_simp` does not; `mspec_no_bind $spec` is defined as
```
try with_reducible mspec_no_bind Std.Do.Spec.bind
mspec_no_bind $spec
```
-/
syntax (name := mspecNoBind) "mspec_no_bind" (ppSpace colGt term)? : tactic
@[inherit_doc Lean.Parser.Tactic.mvcgenMacro]
syntax (name := mvcgen) "mvcgen" optConfig
(" [" withoutPosition((simpStar <|> simpErase <|> simpLemma),*,?) "]")? : tactic
/--
Like `mvcgen`, but does not attempt to prove trivial VCs via `mpure_intro; trivial`.
-/
syntax (name := mvcgenNoTrivial) "mvcgen_no_trivial" optConfig
(" [" withoutPosition((simpStar <|> simpErase <|> simpLemma),*,?) "]")? : tactic
/--
Like `mvcgen_no_trivial`, but `mvcgen_step 42` will only do 42 steps of the VC generation procedure.
This is helpful for bisecting bugs in `mvcgen` and tracing its execution.
-/
syntax (name := mvcgenStep) "mvcgen_step" optConfig
(num)? (" [" withoutPosition((simpStar <|> simpErase <|> simpLemma),*,?) "]")? : tactic