Skip to content

Commit bce47c6

Browse files
authored
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.
1 parent b28daa6 commit bce47c6

File tree

9 files changed

+140
-38
lines changed

9 files changed

+140
-38
lines changed

src/Lean/Elab/Tactic/Do/ProofMode/Cases.lean

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ Authors: Lars König, Mario Carneiro, Sebastian Graf
66
module
77

88
prelude
9-
public import Lean.Elab.Tactic.Do.ProofMode.Pure
10-
public import Lean.Elab.Tactic.Do.ProofMode.Intro
9+
public import Lean.Elab.Tactic.Do.ProofMode.MGoal
10+
public import Std.Tactic.Do.Syntax
11+
import Lean.Elab.Tactic.Do.ProofMode.Pure
12+
import Lean.Elab.Tactic.Do.ProofMode.Intro
13+
import Lean.Elab.Tactic.Do.ProofMode.Focus
1114

1215
public section
1316

src/Lean/Elab/Tactic/Do/ProofMode/Have.lean

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ Authors: Sebastian Graf
66
module
77

88
prelude
9-
public import Lean.Elab.Tactic.Do.ProofMode.Cases
10-
public import Lean.Elab.Tactic.Do.ProofMode.Specialize
9+
public import Std.Tactic.Do.Syntax
10+
public import Lean.Elab.Tactic.Basic
11+
import Lean.Elab.Tactic.Do.ProofMode.Focus
12+
import Lean.Elab.Tactic.Do.ProofMode.Cases
13+
import Lean.Elab.Tactic.Do.ProofMode.Specialize
1114

1215
public section
1316

src/Lean/Elab/Tactic/Do/ProofMode/Pure.lean

Lines changed: 95 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ Authors: Lars König, Mario Carneiro, Sebastian Graf
66
module
77

88
prelude
9-
public import Std.Tactic.Do.Syntax
10-
public import Lean.Elab.Tactic.Do.ProofMode.Focus
11-
public import Lean.Elab.Tactic.Meta
9+
public import Lean.Elab.Tactic.Basic
10+
public import Lean.Elab.Tactic.Do.ProofMode.MGoal
11+
import Std.Tactic.Do.Syntax
12+
import Lean.Elab.Tactic.Meta
13+
import Lean.Elab.Tactic.Do.ProofMode.Basic
14+
import Lean.Elab.Tactic.Do.ProofMode.Focus
15+
import Lean.Meta.Tactic.Rfl
1216

1317
public section
1418

@@ -21,14 +25,16 @@ open Lean Elab Tactic Meta
2125
-- It will provide a proof for Q ∧ H ⊢ₛ T
2226
-- if `k` produces a proof for Q ⊢ₛ T that may range over a pure proof h : φ.
2327
-- It calls `k` with the φ in H = ⌜φ⌝ and a proof `h : φ` thereof.
24-
def mPureCore (σs : Expr) (hyp : Expr) (name : TSyntax ``binderIdent)
25-
(k : Expr /-φ:Prop-/ → Expr /-h:φ-/ → MetaM (α × MGoal × Expr)) : MetaM (α × MGoal × Expr) := do
28+
def mPureCore
29+
[Monad m] [MonadControlT MetaM m] [MonadLiftT MetaM m]
30+
(σs : Expr) (hyp : Expr) (name : TSyntax ``binderIdent)
31+
(k : Expr /-φ:Prop-/ → Expr /-h:φ-/ → m (α × MGoal × Expr)) : m (α × MGoal × Expr) := do
2632
let u ← mkFreshLevelMVar
2733
let φ ← mkFreshExprMVar (mkSort .zero)
2834
let inst ← synthInstance (mkApp3 (mkConst ``IsPure [u]) σs hyp φ)
29-
let (name, ref) ← getFreshHypName name
35+
let (name, ref) ← liftMetaM <| getFreshHypName name
3036
withLocalDeclD name φ fun h => do
31-
addLocalVarInfo ref (← getLCtx) h φ
37+
addLocalVarInfo ref (← liftMetaM <| getLCtx) h φ
3238
let (a, goal, prf /- : goal.toExpr -/) ← k φ h
3339
let prf ← mkLambdaFVars #[h] prf
3440
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)
3844
@[builtin_tactic Lean.Parser.Tactic.mpure]
3945
def elabMPure : Tactic
4046
| `(tactic| mpure $hyp) => do
41-
let mvar ← getMainGoal
47+
let (mvar, goal) ← mStartMainGoal
4248
mvar.withContext do
43-
let g ← instantiateMVars <| ← mvar.getType
44-
let some goal := parseMGoal? g | throwError "not in proof mode"
4549
let res ← goal.focusHypWithInfo hyp
4650
let (m, _new_goal, prf) ← mPureCore goal.σs res.focusHyp (← `(binderIdent| $hyp:ident)) fun _ _ => do
4751
let goal := res.restGoal goal
@@ -52,8 +56,87 @@ def elabMPure : Tactic
5256
replaceMainGoal [m.mvarId!]
5357
| _ => throwUnsupportedSyntax
5458

55-
def MGoal.triviallyPure (goal : MGoal) : OptionT MetaM Expr := do
59+
-- NB: We do not use MVarId.intro because that would mean we require all callers to supply an MVarId.
60+
-- This function only knows about the hypothesis H=⌜φ⌝ to destruct.
61+
-- It will provide a proof for Q ∧ H ⊢ₛ T
62+
-- if `k` produces a proof for Q ⊢ₛ T that may range over a pure proof h : φ.
63+
-- It calls `k` with the φ in H = ⌜φ⌝ and a proof `h : φ` thereof.
64+
def mPureIntroCore [Monad m] [MonadLiftT MetaM m]
65+
(goal : MGoal)
66+
(k : Expr /-φ:Prop-/ → m (α × Expr)) : m (α × Expr) := do
67+
let φ ← mkFreshExprMVar (mkSort .zero)
68+
let inst ← synthInstance (mkApp3 (mkConst ``IsPure [goal.u]) goal.σs goal.target φ)
69+
let (a, hφ) ← k φ
70+
let prf := mkApp6 (mkConst ``Pure.intro [goal.u]) goal.σs goal.hyps goal.target φ inst hφ
71+
return (a, prf)
72+
73+
@[builtin_tactic Lean.Parser.Tactic.mpureIntro]
74+
def elabMPureIntro : Tactic
75+
| `(tactic| mpure_intro) => do
76+
let (mvar, goal) ← mStartMainGoal
77+
mvar.withContext do
78+
let (mv, prf) ← mPureIntroCore goal fun φ => do
79+
let m ← mkFreshExprSyntheticOpaqueMVar φ (← mvar.getTag)
80+
return (m.mvarId!, m)
81+
mvar.assign prf
82+
replaceMainGoal [mv]
83+
| _ => throwUnsupportedSyntax
84+
85+
partial def _root_.Lean.MVarId.applyRflAndAndIntro (mvar : MVarId) : MetaM Unit := do
86+
-- The target might look like `(⌜?n = nₛ ∧ ?m = b⌝ s).down`, which we reduce to
87+
-- `?n = nₛ ∧ ?m = b` by `whnfD`.
88+
-- (Recall that `⌜s = 4⌝ s` is `SPred.pure (σs:=[Nat]) (s = 4) s` and `SPred.pure` is
89+
-- semi-reducible.)
90+
let ty ← whnfD (← mvar.getType)
91+
trace[Elab.Tactic.Do.spec] "whnf: {ty}"
92+
if ty.isAppOf ``True then
93+
mvar.assign (mkConst ``True.intro)
94+
else if let some (lhs, rhs) := ty.app2? ``And then
95+
let hlhs ← mkFreshExprMVar lhs
96+
let hrhs ← mkFreshExprMVar rhs
97+
applyRflAndAndIntro hlhs.mvarId!
98+
applyRflAndAndIntro hrhs.mvarId!
99+
mvar.assign (mkApp4 (mkConst ``And.intro) lhs rhs hlhs hrhs)
100+
else
101+
mvar.setType ty
102+
mvar.applyRfl
103+
104+
def MGoal.pureRflAndAndIntro (goal : MGoal) : OptionT MetaM Expr := do
105+
trace[Elab.Tactic.Do.spec] "pureRflAndAndIntro: {goal.target}"
106+
try
107+
let (_, prf) ← mPureIntroCore goal fun φ => do
108+
trace[Elab.Tactic.Do.spec] "discharge? {φ}"
109+
let m ← mkFreshExprMVar φ
110+
m.mvarId!.applyRflAndAndIntro
111+
trace[Elab.Tactic.Do.spec] "discharged: {φ}"
112+
return ((), m)
113+
return prf
114+
catch _ => failure
115+
116+
def MGoal.pureTrivial (goal : MGoal) : OptionT MetaM Expr := do
117+
try
118+
let (_, prf) ← mPureIntroCore goal fun φ => do
119+
let m ← mkFreshExprMVar φ
120+
try
121+
-- First try to use rfl and And.intro directly.
122+
-- This is more efficient than to elaborate the `trivial` tactic.
123+
m.mvarId!.applyRflAndAndIntro
124+
catch _ =>
125+
let ([], _) ← runTactic m.mvarId! (← `(tactic| trivial))
126+
| failure
127+
return ((), m)
128+
return prf
129+
catch _ => failure
130+
131+
/-
132+
def MGoal.pureRfl (goal : MGoal) : OptionT MetaM Expr := do
56133
let mv ← mkFreshExprMVar goal.toExpr
57-
let ([], _) ← try runTactic mv.mvarId! (← `(tactic| apply $(mkIdent ``Std.Do.SPred.Tactic.Pure.intro); trivial)) catch _ => failure
134+
let ([], _) ← try runTactic mv.mvarId! (← `(tactic| apply $(mkIdent ``Std.Do.SPred.Tactic.Pure.intro); rfl)) catch _ => failure
58135
| failure
59136
return mv
137+
def MGoal.pureRfl (goal : MGoal) : OptionT MetaM Expr := do
138+
let mv ← mkFreshExprMVar goal.toExpr
139+
let ([], _) ← try runTactic mv.mvarId! (← `(tactic| apply $(mkIdent ``Std.Do.SPred.Tactic.Pure.intro); rfl)) catch _ => failure
140+
| failure
141+
return mv
142+
-/

src/Lean/Elab/Tactic/Do/ProofMode/Specialize.lean

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ Authors: Lars König, Mario Carneiro, Sebastian Graf
66
module
77

88
prelude
9-
public import Lean.Elab.Tactic.Do.ProofMode.Basic
10-
public import Lean.Elab.Tactic.Do.ProofMode.Pure
119
public import Lean.Elab.Tactic.ElabTerm
10+
public import Lean.Elab.Tactic.Do.ProofMode.MGoal
11+
import Lean.Elab.Tactic.Do.ProofMode.Basic
12+
import Lean.Elab.Tactic.Do.ProofMode.Focus
13+
import Lean.Elab.Tactic.Do.ProofMode.Pure
1214

1315
public section
1416

src/Lean/Elab/Tactic/Do/Spec.lean

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ Authors: Lars König, Mario Carneiro, Sebastian Graf
66
module
77

88
prelude
9+
public import Lean.Elab.Tactic.Do.Attr
10+
public import Lean.Elab.Tactic.Do.ProofMode.MGoal
11+
12+
-- All these should become private imports in the future:
13+
import Std.Tactic.Do.Syntax
914
public import Lean.Elab.Tactic.Do.ProofMode.Intro
1015
public import Lean.Elab.Tactic.Do.ProofMode.Pure
1116
public import Lean.Elab.Tactic.Do.ProofMode.Frame
1217
public import Lean.Elab.Tactic.Do.ProofMode.Assumption
13-
public import Lean.Elab.Tactic.Do.Attr
1418

1519
namespace Lean.Elab.Tactic.Do
1620
open Lean Elab Tactic Meta
@@ -82,7 +86,7 @@ mutual
8286
partial def dischargePostEntails (α : Expr) (ps : Expr) (Q : Expr) (Q' : Expr) (goalTag : Name) : n Expr := do
8387
-- Often, Q' is fully instantiated while Q contains metavariables. Try refl
8488
let u ← liftMetaM <| getLevel α >>= decLevel
85-
if (← isDefEq Q Q') then
89+
if (← withDefault <| isDefEqGuarded Q Q') then
8690
return mkApp3 (mkConst ``PostCond.entails.refl [u]) α ps Q'
8791
let Q ← whnfR Q
8892
let Q' ← whnfR Q'
@@ -104,11 +108,11 @@ partial def dischargePostEntails (α : Expr) (ps : Expr) (Q : Expr) (Q' : Expr)
104108
partial def dischargeFailEntails (u : Level) (ps : Expr) (Q : Expr) (Q' : Expr) (goalTag : Name) : n Expr := do
105109
if ps.isAppOf ``PostShape.pure then
106110
return mkConst ``True.intro
107-
ifisDefEq Q Q' then
111+
ifwithDefault <| isDefEqGuarded Q Q' then
108112
return mkApp2 (mkConst ``ExceptConds.entails.refl [u]) ps Q
109-
ifisDefEq Q (mkApp (mkConst ``ExceptConds.false [u]) ps) then
113+
ifwithDefault <| isDefEqGuarded Q (mkApp (mkConst ``ExceptConds.false [u]) ps) then
110114
return mkApp2 (mkConst ``ExceptConds.entails_false [u]) ps Q'
111-
ifisDefEq Q' (mkApp (mkConst ``ExceptConds.true [u]) ps) then
115+
ifwithDefault <| isDefEqGuarded Q' (mkApp (mkConst ``ExceptConds.true [u]) ps) then
112116
return mkApp2 (mkConst ``ExceptConds.entails_true [u]) ps Q
113117
-- the remaining cases are recursive.
114118
if let some (_σ, ps) := ps.app2? ``PostShape.arg then
@@ -129,26 +133,32 @@ partial def dischargeFailEntails (u : Level) (ps : Expr) (Q : Expr) (Q' : Expr)
129133
mkFreshExprSyntheticOpaqueMVar (mkApp3 (mkConst ``ExceptConds.entails [u]) ps Q Q') goalTag
130134
end
131135

132-
def dischargeMGoal (goal : MGoal) (goalTag : Name) : n Expr := do
136+
def dischargeMGoal (goal : MGoal) (goalTag : Name) (tryTrivial : Bool) : n Expr := do
133137
liftMetaM <| do trace[Elab.Tactic.Do.spec] "dischargeMGoal: {goal.target}"
134138
-- simply try one of the assumptions for now. Later on we might want to decompose conjunctions etc; full xsimpl
135139
-- The `withDefault` ensures that a hyp `⌜s = 4⌝` can be used to discharge `⌜s = 4⌝ s`.
136140
-- (Recall that `⌜s = 4⌝ s` is `SPred.pure (σs:=[Nat]) (s = 4) s` and `SPred.pure` is
137141
-- semi-reducible.)
138-
-- We also try `mpure_intro; trivial` through `goal.triviallyPure` here because later on an
139-
-- assignment like `⌜s = ?c⌝` becomes impossible to discharge because `?c` will get abstracted
140-
-- over local bindings that depend on synthetic opaque MVars (such as loop invariants), and then
141-
-- the type of the new `?c` will not be defeq to itself. A bug, but we need to work around it for
142-
-- now.
143-
let some prf ← liftMetaM (withDefault <| goal.assumption <|> goal.assumptionPure <|> goal.triviallyPure)
142+
-- We also try `mpure_intro; trivial` through `goal.pureTrivial` here because later on an
143+
-- assignment like `⌜s = ?c ∧ s₂ = ?d⌝` becomes impossible to discharge because `?c` will get
144+
-- abstracted over local bindings that depend on synthetic opaque MVars (such as loop invariants),
145+
-- and then the type of the new `?c` will not be defeq to itself. A bug, but we need to work
146+
-- around it for now.
147+
-- Even when we *do not* want to `tryTrivial`, we still use `goal.pureRflAndAndIntro` because
148+
-- it might assign metavariables as above.
149+
let some prf ← liftMetaM <| -- withDefault <|
150+
if tryTrivial then
151+
goal.pureTrivial <|> goal.assumption <|> goal.assumptionPure
152+
else
153+
goal.pureRflAndAndIntro
144154
| mkFreshExprSyntheticOpaqueMVar goal.toExpr goalTag
145155
liftMetaM <| do trace[Elab.Tactic.Do.spec] "proof: {prf}"
146156
return prf
147157

148158
/--
149159
Returns the proof and the list of new unassigned MVars.
150160
-/
151-
public def mSpec (goal : MGoal) (elabSpecAtWP : Expr → n SpecTheorem) (goalTag : Name) : n Expr := do
161+
public def mSpec (goal : MGoal) (elabSpecAtWP : Expr → n SpecTheorem) (goalTag : Name) (tryTrivial : Bool := true) : n Expr := do
152162
-- First instantiate `fun s => ...` in the target via repeated `mintro ∀s`.
153163
mIntroForallN goal goal.target.consumeMData.getNumHeadLambdas fun goal => do
154164
-- 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
189199
let_expr f@Triple m ps instWP α prog P Q := specTy
190200
| liftMetaM <| throwError "target not a Triple application {specTy}"
191201
let wp' := mkApp5 (mkConst ``WP.wp f.constLevels!) m ps instWP α prog
192-
unless (← withAssignableSyntheticOpaque <| isDefEq wp wp') do
202+
unless (← withAssignableSyntheticOpaque <| isDefEqGuarded wp wp') do
193203
Term.throwTypeMismatchError none wp wp' spec
194204

195205
-- 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
215225
-- Often P or Q are schematic (i.e. an MVar app). Try to solve by rfl.
216226
-- We do `fullApproxDefEq` here so that `constApprox` is active; this is useful in
217227
-- `need_const_approx` of `doLogicTests.lean`.
218-
let (HPRfl, QQ'Rfl) ← fullApproxDefEq <| do
228+
let (HPRfl, QQ'Rfl) ← withDefault <| fullApproxDefEq <| do
219229
return (← isDefEqGuarded P goal.hyps, ← isDefEqGuarded Q Q')
220230

221231
-- Discharge the validity proof for the spec if not rfl
@@ -224,7 +234,7 @@ public def mSpec (goal : MGoal) (elabSpecAtWP : Expr → n SpecTheorem) (goalTag
224234
-- let P := (← reduceProjBeta? P).getD P
225235
-- Try to avoid creating a longer name if the postcondition does not need to create a goal
226236
let tag := if !QQ'Rfl then goalTag ++ `pre else goalTag
227-
let HPPrf ← dischargeMGoal { goal with target := P } tag
237+
let HPPrf ← dischargeMGoal { goal with target := P } tag tryTrivial
228238
prePrf := mkApp6 (mkConst ``SPred.entails.trans [u]) goal.σs goal.hyps P goal.target HPPrf
229239

230240
-- Discharge the entailment on postconditions if not rfl

src/Lean/Elab/Tactic/Do/VCGen.lean

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

src/Std/Do/SPred/DerivedLaws.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ theorem Pure.thm {P Q T : SPred σs} {φ : Prop} [IsPure Q φ] (h : φ → P ⊢
236236
· intro hp
237237
exact and_elim_l.trans (h hp)
238238
/-- A generalization of `pure_intro` exploiting `IsPure`. -/
239-
theorem Pure.intro {P Q : SPred σs} {φ : Prop} [IsPure Q φ] (hp : φ) : P ⊢ₛ Q := (pure_intro hp).trans IsPure.to_pure.mpr
239+
theorem Pure.intro {P Q : SPred σs} {φ : Prop} [IsPure Q φ] ( : φ) : P ⊢ₛ Q := (pure_intro ).trans IsPure.to_pure.mpr
240240
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)
241241
theorem Specialize.imp_stateful {P P' Q R : SPred σs}
242242
(hrefocus : P ∧ (Q → R) ⊣⊢ₛ P' ∧ Q) : P ∧ (Q → R) ⊢ₛ P ∧ R := by

src/Std/Tactic/Do/Syntax.lean

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,7 @@ syntax (name := mleft) "mleft" : tactic
105105
syntax (name := mpure) "mpure" colGt ident : tactic
106106

107107
@[tactic_alt Lean.Parser.Tactic.mpureIntroMacro]
108-
macro (name := mpureIntro) "mpure_intro" : tactic =>
109-
`(tactic| apply $(mkIdent ``Std.Do.SPred.Tactic.Pure.intro))
108+
syntax (name := mpureIntro) "mpure_intro" : tactic
110109

111110
@[tactic_alt Lean.Parser.Tactic.mrenameIMacro]
112111
syntax (name := mrenameI) "mrename_i" (ppSpace colGt binderIdent)+ : tactic

tests/lean/run/doLogicTests.lean

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,8 @@ theorem add_unfold [Monad m] [WPMonad m sh] :
429429

430430
theorem mkFreshPair_triple : ⦃⌜True⌝⦄ mkFreshPair ⦃⇓ (a, b) => ⌜a ≠ b⌝⦄ := by
431431
mvcgen -elimLets +trivial [mkFreshPair]
432+
-- this tests whether `mSpec` immediately discharges by `rfl` and `And.intro` and in the process
433+
-- eagerly instantiates some schematic variables.
432434
simp_all
433435

434436
theorem sum_loop_spec :

0 commit comments

Comments
 (0)