fix: improve performance of mvcgen by optimizing try (mpure_intro; trivial) (#10872)

This PR improves the performance of `mvcgen` by an optimized
implementation for `try (mpure_intro; trivial)`. This tactic sequence is
used to eagerly discharge VCs and in the process instantiates schematic
variables.
This commit is contained in:
Sebastian Graf 2025-10-21 13:32:24 +02:00 committed by GitHub
parent b28daa6d60
commit bce47c6e0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 140 additions and 38 deletions

View file

@ -6,8 +6,11 @@ Authors: Lars König, Mario Carneiro, Sebastian Graf
module
prelude
public import Lean.Elab.Tactic.Do.ProofMode.Pure
public import Lean.Elab.Tactic.Do.ProofMode.Intro
public import Lean.Elab.Tactic.Do.ProofMode.MGoal
public import Std.Tactic.Do.Syntax
import Lean.Elab.Tactic.Do.ProofMode.Pure
import Lean.Elab.Tactic.Do.ProofMode.Intro
import Lean.Elab.Tactic.Do.ProofMode.Focus
public section

View file

@ -6,8 +6,11 @@ Authors: Sebastian Graf
module
prelude
public import Lean.Elab.Tactic.Do.ProofMode.Cases
public import Lean.Elab.Tactic.Do.ProofMode.Specialize
public import Std.Tactic.Do.Syntax
public import Lean.Elab.Tactic.Basic
import Lean.Elab.Tactic.Do.ProofMode.Focus
import Lean.Elab.Tactic.Do.ProofMode.Cases
import Lean.Elab.Tactic.Do.ProofMode.Specialize
public section

View file

@ -6,9 +6,13 @@ Authors: Lars König, Mario Carneiro, Sebastian Graf
module
prelude
public import Std.Tactic.Do.Syntax
public import Lean.Elab.Tactic.Do.ProofMode.Focus
public import Lean.Elab.Tactic.Meta
public import Lean.Elab.Tactic.Basic
public import Lean.Elab.Tactic.Do.ProofMode.MGoal
import Std.Tactic.Do.Syntax
import Lean.Elab.Tactic.Meta
import Lean.Elab.Tactic.Do.ProofMode.Basic
import Lean.Elab.Tactic.Do.ProofMode.Focus
import Lean.Meta.Tactic.Rfl
public section
@ -21,14 +25,16 @@ open Lean Elab Tactic Meta
-- It will provide a proof for Q ∧ H ⊢ₛ T
-- if `k` produces a proof for Q ⊢ₛ T that may range over a pure proof h : φ.
-- It calls `k` with the φ in H = ⌜φ⌝ and a proof `h : φ` thereof.
def mPureCore (σs : Expr) (hyp : Expr) (name : TSyntax ``binderIdent)
(k : Expr /-φ:Prop-/ → Expr /-h:φ-/ → MetaM (α × MGoal × Expr)) : MetaM (α × MGoal × Expr) := do
def mPureCore
[Monad m] [MonadControlT MetaM m] [MonadLiftT MetaM m]
(σs : Expr) (hyp : Expr) (name : TSyntax ``binderIdent)
(k : Expr /-φ:Prop-/ → Expr /-h:φ-/ → m (α × MGoal × Expr)) : m (α × MGoal × Expr) := do
let u ← mkFreshLevelMVar
let φ ← mkFreshExprMVar (mkSort .zero)
let inst ← synthInstance (mkApp3 (mkConst ``IsPure [u]) σs hyp φ)
let (name, ref) ← getFreshHypName name
let (name, ref) ← liftMetaM <| getFreshHypName name
withLocalDeclD name φ fun h => do
addLocalVarInfo ref (← getLCtx) h φ
addLocalVarInfo ref (← liftMetaM <| getLCtx) h φ
let (a, goal, prf /- : goal.toExpr -/) ← k φ h
let prf ← mkLambdaFVars #[h] prf
let prf := mkApp7 (mkConst ``Pure.thm [u]) σs goal.hyps hyp goal.target φ inst prf
@ -38,10 +44,8 @@ def mPureCore (σs : Expr) (hyp : Expr) (name : TSyntax ``binderIdent)
@[builtin_tactic Lean.Parser.Tactic.mpure]
def elabMPure : Tactic
| `(tactic| mpure $hyp) => do
let mvar ← getMainGoal
let (mvar, goal) ← mStartMainGoal
mvar.withContext do
let g ← instantiateMVars <| ← mvar.getType
let some goal := parseMGoal? g | throwError "not in proof mode"
let res ← goal.focusHypWithInfo hyp
let (m, _new_goal, prf) ← mPureCore goal.σs res.focusHyp (← `(binderIdent| $hyp:ident)) fun _ _ => do
let goal := res.restGoal goal
@ -52,8 +56,87 @@ def elabMPure : Tactic
replaceMainGoal [m.mvarId!]
| _ => throwUnsupportedSyntax
def MGoal.triviallyPure (goal : MGoal) : OptionT MetaM Expr := do
-- NB: We do not use MVarId.intro because that would mean we require all callers to supply an MVarId.
-- This function only knows about the hypothesis H=⌜φ⌝ to destruct.
-- It will provide a proof for Q ∧ H ⊢ₛ T
-- if `k` produces a proof for Q ⊢ₛ T that may range over a pure proof h : φ.
-- It calls `k` with the φ in H = ⌜φ⌝ and a proof `h : φ` thereof.
def mPureIntroCore [Monad m] [MonadLiftT MetaM m]
(goal : MGoal)
(k : Expr /-φ:Prop-/ → m (α × Expr)) : m (α × Expr) := do
let φ ← mkFreshExprMVar (mkSort .zero)
let inst ← synthInstance (mkApp3 (mkConst ``IsPure [goal.u]) goal.σs goal.target φ)
let (a, hφ) ← k φ
let prf := mkApp6 (mkConst ``Pure.intro [goal.u]) goal.σs goal.hyps goal.target φ inst hφ
return (a, prf)
@[builtin_tactic Lean.Parser.Tactic.mpureIntro]
def elabMPureIntro : Tactic
| `(tactic| mpure_intro) => do
let (mvar, goal) ← mStartMainGoal
mvar.withContext do
let (mv, prf) ← mPureIntroCore goal fun φ => do
let m ← mkFreshExprSyntheticOpaqueMVar φ (← mvar.getTag)
return (m.mvarId!, m)
mvar.assign prf
replaceMainGoal [mv]
| _ => throwUnsupportedSyntax
partial def _root_.Lean.MVarId.applyRflAndAndIntro (mvar : MVarId) : MetaM Unit := do
-- The target might look like `(⌜?n = nₛ ∧ ?m = b⌝ s).down`, which we reduce to
-- `?n = nₛ ∧ ?m = b` by `whnfD`.
-- (Recall that `⌜s = 4⌝ s` is `SPred.pure (σs:=[Nat]) (s = 4) s` and `SPred.pure` is
-- semi-reducible.)
let ty ← whnfD (← mvar.getType)
trace[Elab.Tactic.Do.spec] "whnf: {ty}"
if ty.isAppOf ``True then
mvar.assign (mkConst ``True.intro)
else if let some (lhs, rhs) := ty.app2? ``And then
let hlhs ← mkFreshExprMVar lhs
let hrhs ← mkFreshExprMVar rhs
applyRflAndAndIntro hlhs.mvarId!
applyRflAndAndIntro hrhs.mvarId!
mvar.assign (mkApp4 (mkConst ``And.intro) lhs rhs hlhs hrhs)
else
mvar.setType ty
mvar.applyRfl
def MGoal.pureRflAndAndIntro (goal : MGoal) : OptionT MetaM Expr := do
trace[Elab.Tactic.Do.spec] "pureRflAndAndIntro: {goal.target}"
try
let (_, prf) ← mPureIntroCore goal fun φ => do
trace[Elab.Tactic.Do.spec] "discharge? {φ}"
let m ← mkFreshExprMVar φ
m.mvarId!.applyRflAndAndIntro
trace[Elab.Tactic.Do.spec] "discharged: {φ}"
return ((), m)
return prf
catch _ => failure
def MGoal.pureTrivial (goal : MGoal) : OptionT MetaM Expr := do
try
let (_, prf) ← mPureIntroCore goal fun φ => do
let m ← mkFreshExprMVar φ
try
-- First try to use rfl and And.intro directly.
-- This is more efficient than to elaborate the `trivial` tactic.
m.mvarId!.applyRflAndAndIntro
catch _ =>
let ([], _) ← runTactic m.mvarId! (← `(tactic| trivial))
| failure
return ((), m)
return prf
catch _ => failure
/-
def MGoal.pureRfl (goal : MGoal) : OptionT MetaM Expr := do
let mv ← mkFreshExprMVar goal.toExpr
let ([], _) ← try runTactic mv.mvarId! (← `(tactic| apply $(mkIdent ``Std.Do.SPred.Tactic.Pure.intro); trivial)) catch _ => failure
let ([], _) ← try runTactic mv.mvarId! (← `(tactic| apply $(mkIdent ``Std.Do.SPred.Tactic.Pure.intro); rfl)) catch _ => failure
| failure
return mv
def MGoal.pureRfl (goal : MGoal) : OptionT MetaM Expr := do
let mv ← mkFreshExprMVar goal.toExpr
let ([], _) ← try runTactic mv.mvarId! (← `(tactic| apply $(mkIdent ``Std.Do.SPred.Tactic.Pure.intro); rfl)) catch _ => failure
| failure
return mv
-/

View file

@ -6,9 +6,11 @@ Authors: Lars König, Mario Carneiro, Sebastian Graf
module
prelude
public import Lean.Elab.Tactic.Do.ProofMode.Basic
public import Lean.Elab.Tactic.Do.ProofMode.Pure
public import Lean.Elab.Tactic.ElabTerm
public import Lean.Elab.Tactic.Do.ProofMode.MGoal
import Lean.Elab.Tactic.Do.ProofMode.Basic
import Lean.Elab.Tactic.Do.ProofMode.Focus
import Lean.Elab.Tactic.Do.ProofMode.Pure
public section

View file

@ -6,11 +6,15 @@ Authors: Lars König, Mario Carneiro, Sebastian Graf
module
prelude
public import Lean.Elab.Tactic.Do.Attr
public import Lean.Elab.Tactic.Do.ProofMode.MGoal
-- All these should become private imports in the future:
import Std.Tactic.Do.Syntax
public import Lean.Elab.Tactic.Do.ProofMode.Intro
public import Lean.Elab.Tactic.Do.ProofMode.Pure
public import Lean.Elab.Tactic.Do.ProofMode.Frame
public import Lean.Elab.Tactic.Do.ProofMode.Assumption
public import Lean.Elab.Tactic.Do.Attr
namespace Lean.Elab.Tactic.Do
open Lean Elab Tactic Meta
@ -82,7 +86,7 @@ mutual
partial def dischargePostEntails (α : Expr) (ps : Expr) (Q : Expr) (Q' : Expr) (goalTag : Name) : n Expr := do
-- Often, Q' is fully instantiated while Q contains metavariables. Try refl
let u ← liftMetaM <| getLevel α >>= decLevel
if (← isDefEq Q Q') then
if (← withDefault <| isDefEqGuarded Q Q') then
return mkApp3 (mkConst ``PostCond.entails.refl [u]) α ps Q'
let Q ← whnfR Q
let Q' ← whnfR Q'
@ -104,11 +108,11 @@ partial def dischargePostEntails (α : Expr) (ps : Expr) (Q : Expr) (Q' : Expr)
partial def dischargeFailEntails (u : Level) (ps : Expr) (Q : Expr) (Q' : Expr) (goalTag : Name) : n Expr := do
if ps.isAppOf ``PostShape.pure then
return mkConst ``True.intro
if ← isDefEq Q Q' then
if ← withDefault <| isDefEqGuarded Q Q' then
return mkApp2 (mkConst ``ExceptConds.entails.refl [u]) ps Q
if ← isDefEq Q (mkApp (mkConst ``ExceptConds.false [u]) ps) then
if ← withDefault <| isDefEqGuarded Q (mkApp (mkConst ``ExceptConds.false [u]) ps) then
return mkApp2 (mkConst ``ExceptConds.entails_false [u]) ps Q'
if ← isDefEq Q' (mkApp (mkConst ``ExceptConds.true [u]) ps) then
if ← withDefault <| isDefEqGuarded Q' (mkApp (mkConst ``ExceptConds.true [u]) ps) then
return mkApp2 (mkConst ``ExceptConds.entails_true [u]) ps Q
-- the remaining cases are recursive.
if let some (_σ, ps) := ps.app2? ``PostShape.arg then
@ -129,18 +133,24 @@ partial def dischargeFailEntails (u : Level) (ps : Expr) (Q : Expr) (Q' : Expr)
mkFreshExprSyntheticOpaqueMVar (mkApp3 (mkConst ``ExceptConds.entails [u]) ps Q Q') goalTag
end
def dischargeMGoal (goal : MGoal) (goalTag : Name) : n Expr := do
def dischargeMGoal (goal : MGoal) (goalTag : Name) (tryTrivial : Bool) : n Expr := do
liftMetaM <| do trace[Elab.Tactic.Do.spec] "dischargeMGoal: {goal.target}"
-- simply try one of the assumptions for now. Later on we might want to decompose conjunctions etc; full xsimpl
-- The `withDefault` ensures that a hyp `⌜s = 4⌝` can be used to discharge `⌜s = 4⌝ s`.
-- (Recall that `⌜s = 4⌝ s` is `SPred.pure (σs:=[Nat]) (s = 4) s` and `SPred.pure` is
-- semi-reducible.)
-- We also try `mpure_intro; trivial` through `goal.triviallyPure` here because later on an
-- assignment like `⌜s = ?c⌝` becomes impossible to discharge because `?c` will get abstracted
-- over local bindings that depend on synthetic opaque MVars (such as loop invariants), and then
-- the type of the new `?c` will not be defeq to itself. A bug, but we need to work around it for
-- now.
let some prf ← liftMetaM (withDefault <| goal.assumption <|> goal.assumptionPure <|> goal.triviallyPure)
-- We also try `mpure_intro; trivial` through `goal.pureTrivial` here because later on an
-- assignment like `⌜s = ?c ∧ s₂ = ?d⌝` becomes impossible to discharge because `?c` will get
-- abstracted over local bindings that depend on synthetic opaque MVars (such as loop invariants),
-- and then the type of the new `?c` will not be defeq to itself. A bug, but we need to work
-- around it for now.
-- Even when we *do not* want to `tryTrivial`, we still use `goal.pureRflAndAndIntro` because
-- it might assign metavariables as above.
let some prf ← liftMetaM <| -- withDefault <|
if tryTrivial then
goal.pureTrivial <|> goal.assumption <|> goal.assumptionPure
else
goal.pureRflAndAndIntro
| mkFreshExprSyntheticOpaqueMVar goal.toExpr goalTag
liftMetaM <| do trace[Elab.Tactic.Do.spec] "proof: {prf}"
return prf
@ -148,7 +158,7 @@ def dischargeMGoal (goal : MGoal) (goalTag : Name) : n Expr := do
/--
Returns the proof and the list of new unassigned MVars.
-/
public def mSpec (goal : MGoal) (elabSpecAtWP : Expr → n SpecTheorem) (goalTag : Name) : n Expr := do
public def mSpec (goal : MGoal) (elabSpecAtWP : Expr → n SpecTheorem) (goalTag : Name) (tryTrivial : Bool := true) : n Expr := do
-- First instantiate `fun s => ...` in the target via repeated `mintro ∀s`.
mIntroForallN goal goal.target.consumeMData.getNumHeadLambdas fun goal => do
-- Elaborate the spec for the wp⟦e⟧ app in the target
@ -189,7 +199,7 @@ public def mSpec (goal : MGoal) (elabSpecAtWP : Expr → n SpecTheorem) (goalTag
let_expr f@Triple m ps instWP α prog P Q := specTy
| 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
unless (← withAssignableSyntheticOpaque <| isDefEqGuarded wp wp') do
Term.throwTypeMismatchError none wp wp' spec
-- Try synthesizing synthetic MVars. We don't have the convenience of `TermElabM`, hence
@ -215,7 +225,7 @@ public def mSpec (goal : MGoal) (elabSpecAtWP : Expr → n SpecTheorem) (goalTag
-- Often P or Q are schematic (i.e. an MVar app). Try to solve by rfl.
-- We do `fullApproxDefEq` here so that `constApprox` is active; this is useful in
-- `need_const_approx` of `doLogicTests.lean`.
let (HPRfl, QQ'Rfl) ← fullApproxDefEq <| do
let (HPRfl, QQ'Rfl) ← withDefault <| fullApproxDefEq <| do
return (← isDefEqGuarded P goal.hyps, ← isDefEqGuarded Q Q')
-- Discharge the validity proof for the spec if not rfl
@ -224,7 +234,7 @@ public def mSpec (goal : MGoal) (elabSpecAtWP : Expr → n SpecTheorem) (goalTag
-- 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 goalTag ++ `pre else goalTag
let HPPrf ← dischargeMGoal { goal with target := P } tag
let HPPrf ← dischargeMGoal { goal with target := P } tag tryTrivial
prePrf := mkApp6 (mkConst ``SPred.entails.trans [u]) goal.σs goal.hyps P goal.target HPPrf
-- Discharge the entailment on postconditions if not rfl

View file

@ -145,7 +145,7 @@ where
-- This is so that `mSpec` can frame hypotheses involving uninstantiated loop invariants.
-- It is absolutely crucial that we do not lose these hypotheses in the inductive step.
collectFreshMVars <| mIntroForallN goal (← TypeList.length goal.σs) fun goal =>
withDefault <| mSpec goal (fun _wp => return specThm) name
mSpec goal (fun _wp => return specThm) name (tryTrivial := false)
catch ex =>
trace[Elab.Tactic.Do.vcgen] "Failed to find spec for {wp}. Trying simp. Reason: {ex.toMessageData}"
-- Last resort: Simp and try again

View file

@ -236,7 +236,7 @@ theorem Pure.thm {P Q T : SPred σs} {φ : Prop} [IsPure Q φ] (h : φ → P ⊢
· intro hp
exact and_elim_l.trans (h hp)
/-- A generalization of `pure_intro` exploiting `IsPure`. -/
theorem Pure.intro {P Q : SPred σs} {φ : Prop} [IsPure Q φ] (hp : φ) : P ⊢ₛ Q := (pure_intro hp).trans IsPure.to_pure.mpr
theorem Pure.intro {P Q : SPred σs} {φ : Prop} [IsPure Q φ] (hφ : φ) : P ⊢ₛ Q := (pure_intro hφ).trans IsPure.to_pure.mpr
theorem Revert.revert {P Q H T : SPred σs} (hfoc : P ⊣⊢ₛ Q ∧ H) (h : Q ⊢ₛ H → T) : P ⊢ₛ T := hfoc.mp.trans (imp_elim h)
theorem Specialize.imp_stateful {P P' Q R : SPred σs}
(hrefocus : P ∧ (Q → R) ⊣⊢ₛ P' ∧ Q) : P ∧ (Q → R) ⊢ₛ P ∧ R := by

View file

@ -105,8 +105,7 @@ syntax (name := mleft) "mleft" : tactic
syntax (name := mpure) "mpure" colGt ident : tactic
@[tactic_alt Lean.Parser.Tactic.mpureIntroMacro]
macro (name := mpureIntro) "mpure_intro" : tactic =>
`(tactic| apply $(mkIdent ``Std.Do.SPred.Tactic.Pure.intro))
syntax (name := mpureIntro) "mpure_intro" : tactic
@[tactic_alt Lean.Parser.Tactic.mrenameIMacro]
syntax (name := mrenameI) "mrename_i" (ppSpace colGt binderIdent)+ : tactic

View file

@ -429,6 +429,8 @@ theorem add_unfold [Monad m] [WPMonad m sh] :
theorem mkFreshPair_triple : ⦃⌜True⌝⦄ mkFreshPair ⦃⇓ (a, b) => ⌜a ≠ b⌝⦄ := by
mvcgen -elimLets +trivial [mkFreshPair]
-- this tests whether `mSpec` immediately discharges by `rfl` and `And.intro` and in the process
-- eagerly instantiates some schematic variables.
simp_all
theorem sum_loop_spec :