lean4-htt/tests/bench/mvcgen/sym/lib/VCGen.lean
Sebastian Graf 204555ba83
test: add Sym vcgen benchmarks get_throw_set and deep_add_sub_cancel (#12447)
This PR adds two more benchmarks for the Sym-based mvcgen prototype in
the style of `add_sub_cancel`.

The first is `deep_add_sub_cancel`, which is like `add_sub_cancel` but
with a much deeper monad stack:
```lean
abbrev M := ExceptT String <| ReaderT String <| ExceptT Nat <| StateT Nat <| ExceptT Unit <| StateM Unit
```
By specializing the specs for `get` and `set`, we get competitive
performance:
```
goal_100: 180.365086 ms, kernel: 79.634989 ms
goal_200: 313.465611 ms, kernel: 187.808631 ms
goal_300: 478.278585 ms, kernel: 270.210634 ms
goal_400: 638.884320 ms, kernel: 380.381127 ms
goal_500: 759.802772 ms, kernel: 472.662882 ms
goal_600: 933.575180 ms, kernel: 649.040746 ms
goal_700: 1174.367200 ms, kernel: 759.470010 ms
goal_800: 1298.866482 ms, kernel: 864.420171 ms
goal_900: 1475.315552 ms, kernel: 1008.662783 ms
goal_1000: 1627.957444 ms, kernel: 1078.627830 ms
```
Recall that `add_sub_cancel` had `goal_1000: 824.476962 ms, kernel:
477.069045 ms`, but that doesn't need to repeatedly unwrap 3 layers of
the monad.

The second benchmark is `get_throw_set`. Its kernel is
```lean
def step (lim : Nat) : ExceptT String (StateM Nat) Unit := do
  let s ← get
  if s > lim then
    throw "s is too large"
  set (s + 1)

def loop (n : Nat) : ExceptT String (StateM Nat) Unit := do
  match n with
  | 0 => pure ()
  | n+1 => loop n; step n

def Goal (n : Nat) : Prop := ⦃fun s => ⌜s = 0⌝⦄ loop n ⦃⇓_ s => ⌜s = n⌝⦄
```
It will generate `n+1` VCs. We get `n` VCs of the form 
```
s✝ : Nat
_ : ¬0 < s✝
...
_ : n < s✝ + 1 ...<n times>... + 1
⊢ ⌜s✝ = 0⌝ ⊢ₛ ⌜False⌝ (s✝ + ...<n times>...)
```
and one VC of the form
```
⌜s✝ = 0⌝ ⊢ₛ ⌜s✝ + 1 + <n times> ... + 1 = n⌝
```
which can be discharged by `grind`, but presently are discharged with
`sorry`.
Statistics:
```
goal_100: 209.435869 ms, kernel: 128.768919 ms
goal_200: 386.639441 ms, kernel: 482.244717 ms
goal_300: 559.795137 ms, kernel: 1251.777405 ms
goal_400: 753.243978 ms, kernel: 3020.878177 ms
goal_500: 1014.939522 ms, kernel: 5182.120327 ms
goal_600: 1229.173622 ms, kernel: 9296.551442 ms
goal_700: 1410.024180 ms, kernel: 16655.954682 ms
goal_800: 1684.059305 ms, kernel: 32065.951705 ms
goal_900: 1905.602401 ms, kernel: 55299.942894 ms
goal_1000: 2172.823244 ms, kernel: 84082.492485 ms
```

Need to look at kernel times here, but tactic time looks about alright.

Using `grind` to discharge just `n=100` goals took 8s.
2026-02-11 19:54:23 +00:00

641 lines
29 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/-
Copyright (c) 2026 Lean FRO LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Sebastian Graf
-/
module
public import Lean.Elab
public import Lean.Meta
public import Lean.Parser
public import Lean.Expr
public meta import Lean.Elab
public meta import Lean.Meta
public meta import Lean.Meta.Match.Rewrite
public meta import Lean.Elab.Tactic.Do.VCGen.Split
open Lean Parser Meta Elab Tactic Sym
open Lean.Elab.Tactic.Do.SpecAttr
open Std.Do
-- The following spec is necessary because the VC gen currently has no support for unfolding spec
-- theorems, which is what we usually do for `MonadState.get`.
@[spec]
theorem Spec.MonadState_get_StateT {m ps} [Monad m] [WPMonad m ps] {σ} {Q : PostCond σ (.arg σ ps)} :
⦃fun s => Q.fst s s⦄ get (m := StateT σ m) ⦃Q⦄ := by
simp only [Triple, WP.get_MonadState, WP.get_StateT, SPred.entails.refl]
-- Normally, we'd support the following two specs by unfolding:
-- TODO: Need to figure out why the trans rule does not apply.
-- @[spec]
-- theorem Spec.monadLift_trans [Monad o] [WPMonad o ps] [MonadLift n o] [MonadLiftT m n] (x : m α) :
-- ⦃wp⟦MonadLift.monadLift (m := n) (n := o) (monadLift x)⟧ Q⦄ (MonadLiftT.monadLift x : o α) ⦃Q⦄ := SPred.entails.rfl
--
-- @[spec]
-- theorem Spec.monadLift_refl [WP m ps] (x : m α) :
-- ⦃wp⟦x⟧ Q⦄ (MonadLiftT.monadLift (n := m) x) ⦃Q⦄ := SPred.entails.rfl
@[spec]
theorem Spec.MonadState_get_ExceptT [Monad m] [MonadStateOf σ m] [WP m ps] :
⦃wp⟦monadLift (n := ExceptT ε m) (get : m σ)⟧ Q⦄ (get : ExceptT ε m σ) ⦃Q⦄ := SPred.entails.rfl
@[spec]
theorem Spec.MonadStateOf_get_ExceptT [Monad m] [MonadStateOf σ m] [WP m ps] :
⦃wp⟦monadLift (n := ExceptT ε m) (get : m σ)⟧ Q⦄ (MonadStateOf.get : ExceptT ε m σ) ⦃Q⦄ := SPred.entails.rfl
@[spec]
theorem Spec.MonadStateOf_get_StateT_lift {m ps} [Monad m] [MonadStateOf σ m] [WP m ps] {Q : PostCond σ (.arg σ' ps)} :
⦃wp⟦monadLift (n := StateT σ' m) (get : m σ)⟧ Q⦄ (MonadStateOf.get (σ := σ) : StateT σ' m σ) ⦃Q⦄ := SPred.entails.rfl
@[spec]
theorem Spec.MonadStateOf_set_ExceptT [Monad m] [MonadStateOf σ m] [WP m ps] {s : σ} :
⦃wp⟦monadLift (n := ExceptT ε m) (set (m := m) s)⟧ Q⦄ set (m := ExceptT ε m) s ⦃Q⦄ := SPred.entails.rfl
@[spec]
theorem Spec.MonadStateOf_set_StateT_lift [Monad m] [MonadStateOf σ m] [WP m ps] {s : σ} :
⦃wp⟦monadLift (n := StateT σ' m) (set (m := m) s)⟧ Q⦄ set (m := StateT σ' m) s ⦃Q⦄ := SPred.entails.rfl
/-!
Creating backward rules for registered specifications
-/
namespace Lean.Elab.Tactic.Do.SpecAttr
/--
Look up `SpecTheorem`s in the `@[spec]` database.
Takes all specs that match the given program `e` and sorts by descending priority.
-/
meta def SpecTheorems.findSpecs (database : SpecTheorems) (e : Expr) : MetaM (Array SpecTheorem) := do
let candidates ← database.specs.getMatch e
let candidates := candidates.filter fun spec => !database.erased.contains spec.proof
return candidates.insertionSort fun s₁ s₂ => s₁.priority > s₂.priority
end Lean.Elab.Tactic.Do.SpecAttr
/--
Create a backward rule for the `SpecTheorem` that was looked up in the database.
In order for the backward rule to apply, we need to instantiate both `m` and `ps` with the ones
given by the use site, and perhaps emit verification conditions for spec lemmas that would not
apply everywhere.
### General idea
Consider the spec theorem `Spec.bind`:
```
Spec.bind : ∀ {m : Type u → Type v} {ps : PostShape} [inst : Monad m] [inst_1 : WPMonad m ps]
{α β : Type u} {x : m α} {f : α → m β} {Q : PostCond β ps},
⦃wp⟦x⟧ (fun a => wp⟦f a⟧ Q, Q.snd)⦄ (x >>= f) ⦃Q⦄
```
This theorem is already in "WP-form", so its postcondition `Q` is schematic (i.e., a ∀-bound var).
However, its precondition `wp⟦x⟧ ...` is not. Hence we must emit a VC for the precondition:
```
prf : ∀ {m : Type u → Type v} {ps : PostShape} [inst : Monad m] [inst_1 : WPMonad m ps]
{α β : Type u} {x : m α} {f : α → m β} {Q : PostCond β ps}
(P : Assertion ps) (hpre : P ⊢ₛ wp⟦x⟧ (fun a => wp⟦f a⟧ Q, Q.snd)),
P ⊢ₛ wp⟦x >>= f⟧ Q
```
(Note that `P ⊢ₛ wp⟦x >>= f⟧ Q` is the definition of `⦃P⦄ (x >>= f) ⦃Q⦄`.)
Where `prf` is constructed by doing `SPred.entails.trans hpre spec` under the forall telescope.
The conclusion of this rule applies to any situation where `bind` is the top-level symbol in the
program.
Similarly, a VC is generated for the postcondition if it isn't schematic.
Furthermore, when there are excess state arguments `[s₁, ..., sₙ]` involved, we rather need to
specialize the backward rule for that:
```
... : ∀ {m : Type u → Type v} {ps : PostShape} [inst : Monad m] [inst_1 : WPMonad m ps]
{α β : Type u} {x : m α} {f : α → m β} {Q : PostCond β ps}
(P : Assertion ...) (hpre : P ⊢ₛ wp⟦x⟧ (fun a => wp⟦f a⟧ Q, Q.snd) s₁ ... sₙ),
P ⊢ₛ wp⟦x >>= f⟧ Q s₁ ... sₙ
```
### Caching
It turns out we can cache backward rules for the cache key `(specThm, m, excessArgs.size)`.
This is very important for performance and helps getting rid of the overhead imposed by the
generality of `Std.Do`. We do that in the `VCGenM` wrapper `mkBackwardRuleFromSpecCached`.
Furthermore, in order to avoid re-checking the same proof in the kernel, we generate an auxiliary
lemma for the backward rule.
### Specialization and unfolding of `Std.Do` abbreviations and defs
It is unnecessary to use the `bind` rule in full generality. It is much more efficient to specialize
it for the particular monad, postshape and `WP` instance. In doing so we can also unfold many
`Std.Do` abbrevations, such as `Assertion ps` and `PostCond α ps`.
We do that by doing `unfoldReducible` on the forall telescope. The type for `StateM Nat` and one
excess state arg `s` becomes
```
prf : ∀ (α : Type) (x : StateT Nat Id α) (β : Type) (f : α → StateT Nat Id β)
(Q : (β → Nat → ULift Prop) × ExceptConds (PostShape.arg Nat PostShape.pure)) (s : Nat)
(P : ULift Prop) (hpre : P ⊢ₛ wp⟦x⟧ (fun a => wp⟦f a⟧ Q, Q.snd) s),
P ⊢ₛ wp⟦x >>= f⟧ Q s
```
We are still investigating how to get rid of more unfolding overhead, such as for `wp` and
`List.rec`.
-/
meta def mkBackwardRuleFromSpecs (specThms : Array SpecTheorem) (m σs ps instWP : Expr) (excessArgs : Array Expr) : SymM (Option (SpecTheorem × BackwardRule)) := do
let preprocessExpr : Expr → SymM Expr := shareCommon <=< liftMetaM ∘ unfoldReducible
for specThm in specThms do
-- Create a backward rule for the spec we look up in the database.
-- In order for the backward rule to apply, we need to instantiate both `m` and `ps` with the ones
-- given by the use site.
let (xs, _bs, spec, specTy) ← specThm.proof.instantiate
let_expr f@Triple m' ps' instWP' α prog P Q := specTy
| liftMetaM <| throwError "target not a Triple application {specTy}"
-- Reject the spec and try the next if the monad doesn't match.
unless ← isDefEqGuarded m m' do -- TODO: Try isDefEqS?
continue
unless ← isDefEqGuarded ps ps' do
continue
unless ← isDefEqGuarded instWP instWP' do
continue
-- We must ensure that P and Q are pattern variables so that the spec matches for every potential
-- P and Q. We do so by introducing VCs accordingly.
-- The following code could potentially be extracted into a definition at @[spec] attribute
-- annotation time. That might help a bit with kernel checking time.
let excessArgNamesTypes ← excessArgs.mapM fun arg =>
return (← mkFreshUserName `s, ← Sym.inferType arg)
let spec ← withLocalDeclsDND excessArgNamesTypes fun ss => do
let needPreVC := !excessArgs.isEmpty || !xs.contains P
let needPostVC := !xs.contains Q
let us := f.constLevels!
let u := us[0]!
let wp := mkApp5 (mkConst ``WP.wp us) m ps instWP α prog
let wpApplyQ := mkApp4 (mkConst ``PredTrans.apply [u]) ps α wp Q -- wp⟦prog⟧ Q
let Pss := mkAppN P ss -- P s₁ ... sₙ
let typeP ← preprocessExpr (mkApp (mkConst ``SPred [u]) σs)
-- Note that this is the type of `P s₁ ... sₙ`,
-- which is `Assertion ps'`, but we don't know `ps'`
let typeQ ← preprocessExpr (mkApp2 (mkConst ``PostCond [u]) α ps)
let mut declInfos := #[]
if needPreVC then
let nmP' ← mkFreshUserName `P
let nmHPre ← mkFreshUserName `hpre
let entailment P' := preprocessExpr <| mkApp3 (mkConst ``SPred.entails [u]) σs P' Pss
declInfos := #[(nmP', .default, fun _ => pure typeP),
(nmHPre, .default, fun xs => entailment xs[0]!)]
if needPostVC then
let nmQ' ← mkFreshUserName `Q
let nmHPost ← mkFreshUserName `hpost
let entailment Q' := pure <| mkApp3 (mkConst ``PostCond.entails [u]) ps Q Q'
declInfos := declInfos ++
#[(nmQ', .default, fun _ => pure typeQ),
(nmHPost, .default, fun xs => entailment xs[0]!)]
withLocalDecls declInfos fun ys => liftMetaM ∘ mkLambdaFVars (ss ++ ys) =<< do
if !needPreVC && !needPostVC && excessArgs.isEmpty then
-- Still need to unfold the triple in the spec type
let entailment ← preprocessExpr <| mkApp3 (mkConst ``SPred.entails [u]) σs P wpApplyQ
let prf ← mkExpectedTypeHint spec entailment
-- check prf
return prf
let mut prf := spec
let P := Pss -- P s₁ ... sₙ
let wpApplyQ := mkAppN wpApplyQ ss -- wp⟦prog⟧ Q s₁ ... sₙ
prf := mkAppN prf ss -- Turn `⦃P⦄ prog ⦃Q⦄` into `P s₁ ... sₙ ⊢ₛ wp⟦prog⟧ Q s₁ ... sₙ`
let mut newP := P
let mut newQ := Q
if needPreVC then
-- prf := hpre.trans prf
let P' := ys[0]!
let hpre := ys[1]!
prf := mkApp6 (mkConst ``SPred.entails.trans [u]) σs P' P wpApplyQ hpre prf
newP := P'
-- check prf
if needPostVC then
-- prf := prf.trans <| (wp x).mono _ _ hpost
let wp := mkApp5 (mkConst ``WP.wp f.constLevels!) m ps instWP α prog
let Q' := ys[ys.size-2]!
let hpost := ys[ys.size-1]!
let wpApplyQ' := mkApp4 (mkConst ``PredTrans.apply [u]) ps α wp Q' -- wp⟦prog⟧ Q'
let wpApplyQ' := mkAppN wpApplyQ' ss -- wp⟦prog⟧ Q' s₁ ... sₙ
let hmono := mkApp6 (mkConst ``PredTrans.mono [u]) ps α wp Q Q' hpost
let hmono := mkAppN hmono ss
prf := mkApp6 (mkConst ``SPred.entails.trans [u]) σs newP wpApplyQ wpApplyQ' prf hmono
newQ := Q'
-- check prf
return prf
let res ← abstractMVars spec
let type ← preprocessExpr (← Sym.inferType res.expr)
trace[Elab.Tactic.Do.vcgen] "Type of new auxiliary spec apply theorem: {type}"
let spec ← Meta.mkAuxLemma res.paramNames.toList type res.expr
return some (specThm, ← mkBackwardRuleFromDecl spec)
return none
open Lean.Elab.Tactic.Do in
/--
Creates a reusable backward rule for `ite`. It proves a theorem of the following form:
```
example {m} {σ} {ps} [WP m (.arg σ ps)] -- These are fixed. The other arguments are parameters of the rule:
{α} {c : Prop} [Decidable c] {t e : m α} {s : σ} {P : Assertion ps} {Q : PostCond α (.arg σ ps)}
(hthen : P ⊢ₛ wp⟦t⟧ Q s) (helse : P ⊢ₛ wp⟦e⟧ Q s)
: P ⊢ₛ wp⟦ite c t e⟧ Q s
```
-/
meta def mkBackwardRuleForIte (m σs ps instWP : Expr) (excessArgs : Array Expr) : SymM BackwardRule := do
let preprocessExpr : Expr → SymM Expr := shareCommon <=< liftMetaM ∘ unfoldReducible
let prf ← do
let us := instWP.getAppFn.constLevels!
let u := us[0]!
let v := us[1]!
withLocalDeclD `α (mkSort u.succ) fun α => do
let mα ← preprocessExpr <| mkApp m α
withLocalDeclD `c (mkSort 0) fun c => do
withLocalDeclD `dec (mkApp (mkConst ``Decidable) c) fun dec => do
withLocalDeclD `t mα fun t => do
withLocalDeclD `e mα fun e => do
let prog ← preprocessExpr (mkApp5 (mkConst ``ite [v.succ]) mα c dec t e)
let excessArgNamesTypes ← excessArgs.mapM fun arg =>
return (`s, ← Sym.inferType arg)
withLocalDeclsDND excessArgNamesTypes fun ss => do
withLocalDeclD `P (← preprocessExpr <| mkApp (mkConst ``SPred [u]) σs) fun P => do
withLocalDeclD `Q (← preprocessExpr <| mkApp2 (mkConst ``PostCond [u]) α ps) fun Q => do
let goalWithProg prog :=
let wp := mkApp5 (mkConst ``WP.wp [u, v]) m ps instWP α prog
let wpApplyQ := mkApp4 (mkConst ``PredTrans.apply [u]) ps α wp Q -- wp⟦prog⟧ Q
let wpApplyQ := mkAppN wpApplyQ ss -- wp⟦prog⟧ Q s₁ ... sₙ
mkApp3 (mkConst ``SPred.entails [u]) σs P wpApplyQ
let thenType ← mkArrow c (goalWithProg t)
withLocalDeclD `hthen (← preprocessExpr thenType) fun hthen => do
let elseType ← mkArrow (mkNot c) (goalWithProg e)
withLocalDeclD `helse (← preprocessExpr elseType) fun helse => do
let onAlt (hc : Expr) (hcase : Expr) := do
let res ← rwIfWith hc prog
-- When `rw` fails, it returns `proof? := none`. We throw an error.
if res.proof?.isNone then
throwError "`rwIfWith` failed to rewrite {indentExpr e}."
-- context = fun e => P ⊢ₛ wp⟦e⟧ Q s₁ ... sₙ
let context ← withLocalDecl `e .default mα fun e => mkLambdaFVars #[e] (goalWithProg e)
let res ← Simp.mkCongrArg context res
res.mkEqMPR hcase
let ht ← withLocalDecl `h .default c fun h => do mkLambdaFVars #[h] (← onAlt h (mkApp hthen h))
let he ← withLocalDecl `h .default (mkNot c) fun h => do mkLambdaFVars #[h] (← onAlt h (mkApp helse h))
let prf := mkApp5 (mkConst ``dite [0]) (goalWithProg prog) c dec ht he
mkLambdaFVars (#[α, c, dec, t, e] ++ ss ++ #[P, Q, hthen, helse]) prf
let res ← abstractMVars prf
let type ← preprocessExpr (← Sym.inferType res.expr)
let prf ← Meta.mkAuxLemma res.paramNames.toList type res.expr
trace[Elab.Tactic.Do.vcgen] "Type of new auxiliary spec apply theorem for `ite`: {type}"
mkBackwardRuleFromDecl prf
/-!
VC generation
-/
public structure VCGen.Context where
specThms : SpecTheorems
/-- The backward rule for `SPred.entails_cons_intro`. -/
entailsConsIntroRule : BackwardRule
public structure VCGen.State where
/--
A cache mapping registered SpecThms to their backward rule to apply.
The particular rule depends on the theorem name, the monad and the number of excess state
arguments that the weakest precondition target is applied to.
-/
specBackwardRuleCache : Std.HashMap (Array Name × Expr × Nat) (Option (SpecTheorem × BackwardRule)) := {}
/--
A cache mapping matchers to their splitting backward rule to apply.
The particular rule depends on the matcher name, the monad and the number of excess state
arguments that the weakest precondition target is applied to.
-/
splitBackwardRuleCache : Std.HashMap (Name × Expr × Nat) BackwardRule := {}
/--
Holes of type `Invariant` that have been generated so far.
-/
invariants : Array MVarId := #[]
/--
The verification conditions that have been generated so far.
-/
vcs : Array MVarId := #[]
abbrev VCGenM := ReaderT VCGen.Context (StateRefT VCGen.State SymM)
namespace VCGen
@[inline]
meta def _root_.Std.HashMap.getDM [Monad m] [BEq α] [Hashable α]
(cache : Std.HashMap α β) (key : α) (fallback : m β) : m (β × Std.HashMap α β) := do
if let some b := cache.get? key then
return (b, cache)
let b ← fallback
return (b, cache.insert key b)
meta def SpecTheorem.global? (specThm : SpecTheorem) : Option Name :=
match specThm.proof with | .global decl => some decl | _ => none
/-- See the documentation for `SpecTheorem.mkBackwardRuleFromSpec` for more details. -/
meta def mkBackwardRuleFromSpecsCached (specThms : Array SpecTheorem) (m σs ps instWP : Expr) (excessArgs : Array Expr) : VCGenM (Option (SpecTheorem × BackwardRule)) := do
let mkRuleSlow := mkBackwardRuleFromSpecs specThms m σs ps instWP excessArgs
let s ← get
let some decls := specThms.mapM SpecTheorem.global? | mkRuleSlow
let (res, specBackwardRuleCache) ← s.specBackwardRuleCache.getDM (decls, m, excessArgs.size) mkRuleSlow
set { s with specBackwardRuleCache }
return res
open Lean.Elab.Tactic.Do in
/-- See the documentation for `SpecTheorem.mkBackwardRuleForIte` for more details. -/
meta def mkBackwardRuleFromSplitInfoCached (splitInfo : SplitInfo) (m σs ps instWP : Expr) (excessArgs : Array Expr) : _root_.VCGenM BackwardRule := do
unless splitInfo matches .ite .. do throwError "Only `ite` is currently supported for splitting."
let mkRuleSlow := mkBackwardRuleForIte m σs ps instWP excessArgs
let s ← get
let (res, splitBackwardRuleCache) ← s.splitBackwardRuleCache.getDM (``ite, m, excessArgs.size) mkRuleSlow
set { s with splitBackwardRuleCache }
return res
/-- Unfold `⦃P⦄ x ⦃Q⦄` into `P ⊢ₛ wp⟦x⟧ Q`. -/
meta def unfoldTriple (goal : MVarId) : SymM MVarId := goal.withContext do
let type ← goal.getType
unless type.isAppOf ``Triple do return goal
let type ← unfoldDefinition type
let goal ← goal.replaceTargetDefEq (← shareCommon type)
preprocessMVar goal -- need to reinstate subterm sharing
open Lean.Elab.Tactic.Do in
/--
Do a very targeted simplification to turn `P ⊢ₛ (fun _ => T, Q.snd).fst s` into `P ⊢ₛ T`.
This often arises as follows during backward reasoning:
```
P ⊢ₛ wp⟦get >>= set⟧ Q
= P ⊢ₛ wp⟦get⟧ (fun a => wp⟦set a⟧ Q, Q.snd)
= P ⊢ₛ (fun s => (fun a => wp⟦set a⟧ Q, Q.snd).fst s s)
= P s ⊢ₛ (fun a => wp⟦set a⟧ Q, Q.snd).fst s s
-- This is where we simplify!
= P s ⊢ₛ wp⟦set s⟧ Q s
= P s ⊢ₛ Q.fst s s
-/
meta def simplifyTarget (goal : MVarId) : _root_.VCGenM MVarId := goal.withContext do
let target ← goal.getType
let_expr ent@SPred.entails σs P T := target | return goal
let some T ← reduceProjBeta? T | return goal -- very slight simplification
goal.replaceTargetDefEq (mkApp3 ent σs P T)
/--
Preprocess a goal, potentially closing it. This function assumes and preserves that the goal has is
normalized according to `Sym.preprocessMVar`.
-/
meta def preprocessGoal (goal : MVarId) : VCGenM (Option MVarId) := do
let mut goal := goal
if (← goal.getType).isForall then
let IntrosResult.goal _ goal' ← Sym.intros goal | failure
goal := goal'
goal ← unfoldTriple goal
goal ← simplifyTarget goal
return goal
inductive SolveResult where
/-- `target` was not of the form `H ⊢ₛ T`. -/
| noEntailment (target : Expr)
/-- The `T` in `H ⊢ₛ T` was not of the form `wp⟦e⟧ Q s₁ ... sₙ`. -/
| noProgramFoundInTarget (T : Expr)
/-- Don't know how to handle `e` in `H ⊢ₛ wp⟦e⟧ Q s₁ ... sₙ`. -/
| noStrategyForProgram (e : Expr)
/--
Did not find a spec for the `e` in `H ⊢ₛ wp⟦e⟧ Q s₁ ... sₙ`.
Candidates were `thms`, but none of them matched the monad.
-/
| noSpecFoundForProgram (e : Expr) (monad : Expr) (thms : Array SpecTheorem)
/-- Successfully discharged the goal. These are the subgoals. -/
| goals (subgoals : List MVarId)
/--
The main VC generation function.
Looks at a goal of the form `P ⊢ₛ T`. Then
* If `T` is a lambda, introduce another state variable.
* If `T` is of the form `wp⟦e⟧ Q s₁ ... sₙ`, look up a spec theorem for `e`. Produce the backward
rule to apply this spec theorem and then apply it ot the goal.
-/
meta def solve (goal : MVarId) : VCGenM SolveResult := goal.withContext do
let target ← goal.getType
trace[Elab.Tactic.Do.vcgen] "target: {target}"
let_expr ent@SPred.entails σs H T := target | return .noEntailment target
-- The goal is of the form `H ⊢ₛ T`. Look for program syntax in `T`.
if T.isLambda then
-- This happens after applying the `get` spec. We have `T = (fun s => (wp⟦e⟧ Q, Q.snd).fst s s)`.
-- Do what `mIntroForall` does, that is, eta-expand. Note that this introduces an
-- extra state arg `s` to reduce away the lambda.
let .goals goals ← (← read).entailsConsIntroRule.apply goal
| throwError "Applying {.ofConstName ``SPred.entails_cons_intro} to {target} failed. It should not."
return .goals goals
T.withApp fun head args => do
if head.isMVar then
if ← withAssignableSyntheticOpaque <| isDefEq H T then -- TODO: Figure out why `isDefEqS` doesn't work here
goal.assign (mkApp2 (mkConst ``SPred.entails.refl ent.constLevels!) σs H)
return .goals []
unless head.isConstOf ``PredTrans.apply do return .noProgramFoundInTarget T
let wp := args[2]!
let_expr wpConst@WP.wp m ps instWP α e := wp | return .noProgramFoundInTarget T
-- `T` is of the form `wp⟦e⟧ Q s₁ ... sₙ`, where `e` is the program.
-- We call `s₁ ... sₙ` the excess state args; the backward rules need to account for these.
-- Excess state args are introduced by the spec of `get` (see lambda case above).
let excessArgs := args.drop 4
let f := e.getAppFn
withTraceNode `Elab.Tactic.Do.vcgen (msg := fun _ => return m!"Program: {e}") do
-- let-expressions. Zeta aggressively for now.
if let .letE _x _ty val body _nonDep := f then
let e' := (body.instantiate1 val).betaRev e.getAppRevArgs
let wp := mkApp5 wpConst m ps instWP α e'
let T := mkAppN head (args.set! 2 wp)
let target := match target with | .app head _T => mkApp head T | _ => unreachable!
return .goals [← goal.replaceTargetDefEq target]
-- Hard-code match splitting for `ite` for now.
if f.isAppOf ``ite then
let some info ← Lean.Elab.Tactic.Do.getSplitInfo? e | return .noStrategyForProgram e
let rule ← mkBackwardRuleFromSplitInfoCached info m σs ps instWP excessArgs
let ApplyResult.goals goals ← rule.apply goal
| throwError "Failed to apply split rule for {indentExpr e}"
return .goals goals
-- Apply registered specifications.
if f.isConst || f.isFVar then
trace[Elab.Tactic.Do.vcgen] "Applying a spec for {e}. Excess args: {excessArgs}"
let thms ← (← read).specThms.findSpecs e
trace[Elab.Tactic.Do.vcgen] "Candidates for {e}: {thms.map (·.proof)}"
let some (thm, rule) ← mkBackwardRuleFromSpecsCached thms m σs ps instWP excessArgs
| return .noSpecFoundForProgram e m thms
trace[Elab.Tactic.Do.vcgen] "Applying rule {rule.pattern.pattern} at {target}"
let ApplyResult.goals goals ← rule.apply goal
| throwError "Failed to apply rule {thm.proof} for {indentExpr e}"
return .goals goals
return .noStrategyForProgram e
/--
Called when decomposing the goal further did not succeed; in this case we emit a VC for the goal.
-/
meta def emitVC (goal : MVarId) : VCGenM Unit := do
let ty ← goal.getType
goal.setKind .syntheticOpaque
if ty.isAppOf ``Std.Do.Invariant then
modify fun s => { s with invariants := s.invariants.push goal }
else
modify fun s => { s with vcs := s.vcs.push goal }
meta def work (goal : MVarId) : VCGenM Unit := do
let mut worklist := Std.Queue.empty.enqueue (← preprocessMVar goal)
-- while let some (goal, worklist') := worklist.dequeue? do
repeat do
let some (goal, worklist') := worklist.dequeue? | break
worklist := worklist'
let some goal ← preprocessGoal goal | continue
let res ← solve goal
match res with
| .noEntailment .. | .noProgramFoundInTarget .. =>
emitVC goal
| .noSpecFoundForProgram prog _ #[] =>
throwError "No spec found for program {prog}."
| .noSpecFoundForProgram prog monad thms =>
throwError "No spec matching the monad {monad} found for program {prog}. Candidates were {thms.map (·.proof)}."
| .noStrategyForProgram prog =>
throwError "Did not know how to decompose weakest precondition for {prog}"
| .goals subgoals =>
worklist := worklist.enqueueAll subgoals
public structure Result where
invariants : Array MVarId
vcs : Array MVarId
/--
Generate verification conditions for a goal of the form `P ⊢ₛ wp⟦e⟧ Q s₁ ... sₙ` by repeatedly
decomposing `e` using registered `@[spec]` theorems.
Return the VCs and invariant goals.
-/
public meta partial def main (goal : MVarId) (ctx : Context) : SymM Result := do
let ((), state) ← StateRefT'.run (ReaderT.run (work goal) ctx) {}
for h : idx in [:state.invariants.size] do
let mv := state.invariants[idx]
mv.setTag (Name.mkSimple ("inv" ++ toString (idx + 1)))
for h : idx in [:state.vcs.size] do
let mv := state.vcs[idx]
mv.setTag (Name.mkSimple ("vc" ++ toString (idx + 1)) ++ (← mv.getTag).eraseMacroScopes)
return { invariants := state.invariants, vcs := state.vcs }
/--
This function is best ignored; it's copied from `Lean.Elab.Tactic.Do.mkSpecContext`
and is more complex than necessary ATM.
-/
meta def mkSpecContext (lemmas : Syntax) (ignoreStarArg := false) : TacticM VCGen.Context := do
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.erase 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 := specThms.add thm
catch _ =>
simpStuff := simpStuff.push ⟨arg⟩
| some (.fvar fvar) =>
let decl ← getFVarLocalDecl (.fvar fvar)
try
let thm ← mkSpecTheoremFromLocal fvar
specThms := specThms.add 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 -zeta [$(Syntax.TSepArray.ofElems simpStuff),*])
-- logInfo s!"{stx}"
let res ← mkSimpContext stx.raw
(eraseLocal := false)
(simpTheorems := getSpecSimpTheorems)
(ignoreStarArg := ignoreStarArg)
-- trace[Elab.Tactic.Do.vcgen] "{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 := specThms.add thm
catch _ => continue
let entailsConsIntroRule ← mkBackwardRuleFromDecl ``SPred.entails_cons_intro
return { specThms, entailsConsIntroRule }
end VCGen
syntax (name := mvcgen') "mvcgen'"
(" [" withoutPosition((simpStar <|> simpErase <|> simpLemma),*,?) "] ")? : tactic
@[tactic mvcgen']
public meta def elabMVCGen' : Tactic := fun stx => withMainContext do
let ctx ← VCGen.mkSpecContext stx[1]
let goal ← getMainGoal
let { invariants, vcs } ← SymM.run <| VCGen.main goal ctx
replaceMainGoal (invariants ++ vcs).toList
/-!
Local tests for faster iteration:
-/
/-
def step (lim : Nat) : ExceptT String (StateM Nat) Unit := do
let s ← get
if s > lim then
throw "s is too large"
set (s + 1)
def loop (n : Nat) : ExceptT String (StateM Nat) Unit := do
match n with
| 0 => pure ()
| n+1 => loop n; step n
set_option maxRecDepth 10000
set_option maxHeartbeats 10000000
-- set_option trace.Elab.Tactic.Do.vcgen true in
set_option trace.profiler true in
example : ⦃fun s => ⌜s = 0⌝⦄ loop 50 ⦃⇓_ s => ⌜s = 50⌝⦄ := by
simp only [loop, step]
mvcgen'
-- all_goals grind
all_goals sorry
set_option trace.Elab.Tactic.Do.vcgen true in
example :
⦃⌜True⌝⦄
do
let s ← get (m := ExceptT String (StateM Nat))
if s > 20 then
throw "s is too large"
set (m := ExceptT String (StateM Nat)) (s + 1)
⦃post⟨fun _r s => ⌜s ≤ 21⌝, fun _err s => ⌜s > 20⌝⟩⦄ := by
mvcgen' <;> grind
-/