Skip to content

Commit 21846eb

Browse files
authored
feat: non-chronological backtracking for grind (WIP) (#8440)
This PR implements non-chronological backtracking for the `grind` tactic. This feature ensures that `grind` does not need to process irrelevant branches after performing a case-split that is not relevant. It is not just about performance, but also the size of the final proof term. The new test demonstrates this feature in practice. ```lean -- In the following test, the first 8 case-splits are irrelevant, -- and non-choronological backtracking is used to avoid searching -- (2^8 - 1) irrelevant branches /-- trace: [grind.split] p8 ∨ q8, generation: 0 [grind.split] p7 ∨ q7, generation: 0 [grind.split] p6 ∨ q6, generation: 0 [grind.split] p5 ∨ q5, generation: 0 [grind.split] p4 ∨ q4, generation: 0 [grind.split] p3 ∨ q3, generation: 0 [grind.split] p2 ∨ q2, generation: 0 [grind.split] p1 ∨ q1, generation: 0 [grind.split] ¬p ∨ ¬q, generation: 0 -/ #guard_msgs (trace) in set_option trace.grind.split true in theorem ex : p ∨ q → ¬ p ∨ q → p ∨ ¬ q → ¬ p ∨ ¬ q → p1 ∨ q1 → p2 ∨ q2 → p3 ∨ q3 → p4 ∨ q4 → p5 ∨ q5 → p6 ∨ q6 → p7 ∨ q7 → p8 ∨ q8 → False := by grind (splits := 10) ```
1 parent 9ea4946 commit 21846eb

File tree

19 files changed

+535
-370
lines changed

19 files changed

+535
-370
lines changed

src/Init/MetaTypes.lean

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,11 @@ structure Config where
244244
`let x := v; e` simplifies to `e` when `x` does not occur in `e`.
245245
-/
246246
zetaUnused : Bool := true
247+
/--
248+
When `true` (default : `true`), then simps will catch runtime exceptions and
249+
convert them into `simp` exceptions.
250+
-/
251+
catchRuntime : Bool := true
247252
deriving Inhabited, BEq
248253

249254
-- Configuration object for `simp_all`

src/Lean/Meta/Tactic/Grind/Arith/Cutsat/MBTC.lean

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ Released under Apache 2.0 license as described in the file LICENSE.
44
Authors: Leonardo de Moura
55
-/
66
prelude
7-
import Lean.Meta.Tactic.Grind.Combinators
87
import Lean.Meta.Tactic.Grind.Canon
98
import Lean.Meta.Tactic.Grind.MBTC
109
import Lean.Meta.Tactic.Grind.Arith.Cutsat.Model
@@ -36,8 +35,8 @@ private def eqAssignment (a b : Expr) : GoalM Bool := do
3635
let some v₂ ← getAssignmentExt? b | return false
3736
return v₁ == v₂
3837

39-
def mbtcTac : GrindTactic :=
40-
Grind.mbtcTac {
38+
def mbtc : GoalM Bool := do
39+
Grind.mbtc {
4140
hasTheoryVar := hasTheoryVar
4241
isInterpreted := isInterpreted
4342
eqAssignment := eqAssignment

src/Lean/Meta/Tactic/Grind/Arith/Main.lean

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ Authors: Leonardo de Moura
55
-/
66
prelude
77
import Lean.Meta.Tactic.Grind.PropagatorAttr
8-
import Lean.Meta.Tactic.Grind.Combinators
98
import Lean.Meta.Tactic.Grind.Arith.Offset
109
import Lean.Meta.Tactic.Grind.Arith.Cutsat.LeCnstr
1110
import Lean.Meta.Tactic.Grind.Arith.Cutsat.Search
@@ -37,20 +36,13 @@ builtin_grind_propagator propagateLE ↓LE.le := fun e => do
3736
Offset.assertFalse c (← mkEqFalseProof e)
3837
Cutsat.propagateIfSupportedLe e (eqTrue := false)
3938

40-
def check : GrindTactic := fun goal => do
41-
let (progress, goal) ← GoalM.run goal do
42-
let c₁ ← Cutsat.check
43-
let c₂ ← CommRing.check
44-
if c₁ || c₂ then
45-
processNewFacts
46-
return true
47-
else
48-
return false
49-
unless progress do
50-
return none
51-
if goal.inconsistent then
52-
return some []
39+
def check : GoalM Bool := do
40+
let c₁ ← Cutsat.check
41+
let c₂ ← CommRing.check
42+
if c₁ || c₂ then
43+
processNewFacts
44+
return true
5345
else
54-
return some [goal]
46+
return false
5547

5648
end Lean.Meta.Grind.Arith

src/Lean/Meta/Tactic/Grind/Cases.lean

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ def cases (mvarId : MVarId) (e : Expr) : MetaM (List MVarId) := mvarId.withConte
150150
let k (mvarId : MVarId) (fvarId : FVarId) (indices : Array FVarId) : MetaM (List MVarId) := do
151151
let indicesExpr := indices.map mkFVar
152152
let recursor ← mkRecursorAppPrefix mvarId `grind.cases fvarId recursorInfo indicesExpr
153+
let lctx ← getLCtx
154+
let lctx := lctx.setKind fvarId .implDetail
155+
let lctx := indices.foldl (init := lctx) fun lctx fvarId => lctx.setKind fvarId .implDetail
156+
let localInsts ← getLocalInstances
153157
let mut recursor := mkApp (mkAppN recursor indicesExpr) (mkFVar fvarId)
154158
let mut recursorType ← inferType recursor
155159
let mut mvarIdsNew := #[]
@@ -159,10 +163,9 @@ def cases (mvarId : MVarId) (e : Expr) : MetaM (List MVarId) := mvarId.withConte
159163
| throwTacticEx `grind.cases mvarId "unexpected recursor type"
160164
recursorType := recursorTypeNew
161165
let tagNew := if recursorInfo.numMinors > 1 then Name.num tag idx else tag
162-
let mvar ← mkFreshExprSyntheticOpaqueMVar targetNew tagNew
166+
let mvar ← mkFreshExprMVarAt lctx localInsts targetNew .syntheticOpaque tagNew
163167
recursor := mkApp recursor mvar
164-
let mvarIdNew ← mvar.mvarId!.tryClearMany (indices.push fvarId)
165-
mvarIdsNew := mvarIdsNew.push mvarIdNew
168+
mvarIdsNew := mvarIdsNew.push mvar.mvarId!
166169
idx := idx + 1
167170
mvarId.assign recursor
168171
return mvarIdsNew.toList

src/Lean/Meta/Tactic/Grind/Combinators.lean

Lines changed: 0 additions & 43 deletions
This file was deleted.

src/Lean/Meta/Tactic/Grind/EMatch.lean

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -489,13 +489,10 @@ private def ematchCore : GoalM Unit := do
489489
ematch.num := s.ematch.num + 1
490490
}
491491

492-
/-- Performs one round of E-matching. -/
493-
def ematch : GrindTactic := fun goal => do
494-
let numInstances := goal.ematch.numInstances
495-
let goal ← GoalM.run' goal ematchCore
496-
if goal.ematch.numInstances == numInstances then
497-
return none
498-
else
499-
return [goal]
492+
/-- Performs one round of E-matching, and returns `true` if new instances were generated. -/
493+
def ematch : GoalM Bool := do
494+
let numInstances := (← get).ematch.numInstances
495+
ematchCore
496+
return (← get).ematch.numInstances != numInstances
500497

501498
end Lean.Meta.Grind

src/Lean/Meta/Tactic/Grind/Intro.lean

Lines changed: 86 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import Lean.Meta.Tactic.Grind.Cases
1212
import Lean.Meta.Tactic.Grind.CasesMatch
1313
import Lean.Meta.Tactic.Grind.Injection
1414
import Lean.Meta.Tactic.Grind.Core
15-
import Lean.Meta.Tactic.Grind.Combinators
15+
import Lean.Meta.Tactic.Grind.SearchM
1616

1717
namespace Lean.Meta.Grind
1818

@@ -184,23 +184,6 @@ private def isCheapInductive (type : Expr) : CoreM Bool := do
184184
let .inductInfo info ← getConstInfo declName | return false
185185
return info.numCtors <= 1
186186

187-
private def applyCases? (goal : Goal) (fvarId : FVarId) : GrindM (Option (List Goal)) := goal.mvarId.withContext do
188-
/-
189-
Remark: we used to use `whnfD`. This was a mistake, we don't want to unfold user-defined abstractions.
190-
Example: `a ∣ b` is defined as `∃ x, b = a * x`
191-
-/
192-
let type ← whnf (← fvarId.getType)
193-
if isEagerCasesCandidate goal type then
194-
if (← cheapCasesOnly) then
195-
unless (← isCheapInductive type) do
196-
return none
197-
if let .const declName _ := type.getAppFn then
198-
saveCases declName true
199-
let mvarIds ← cases goal.mvarId (mkFVar fvarId)
200-
return mvarIds.map fun mvarId => { goal with mvarId }
201-
else
202-
return none
203-
204187
private def applyInjection? (goal : Goal) (fvarId : FVarId) : MetaM (Option Goal) := do
205188
if let some mvarId ← injection? goal.mvarId fvarId then
206189
return some { goal with mvarId }
@@ -213,60 +196,104 @@ private def exfalsoIfNotProp (goal : Goal) : MetaM Goal := goal.mvarId.withConte
213196
else
214197
return { goal with mvarId := (← goal.mvarId.exfalso) }
215198

216-
/-- Introduce new hypotheses (and apply `by_contra`) until goal is of the form `... ⊢ False` -/
217-
partial def intros (generation : Nat) : GrindTactic' := fun goal => do
218-
let rec go (goal : Goal) : StateRefT (Array Goal) GrindM Unit := do
219-
if goal.inconsistent then
199+
private def applyCases? (fvarId : FVarId) (generation : Nat) : SearchM Bool := withCurrGoalContext do
200+
/-
201+
Remark: we used to use `whnfD`. This was a mistake, we don't want to unfold user-defined abstractions.
202+
Example: `a ∣ b` is defined as `∃ x, b = a * x`
203+
-/
204+
let type ← whnf (← fvarId.getType)
205+
if isEagerCasesCandidate (← getGoal) type then
206+
if (← cheapCasesOnly) then
207+
unless (← isCheapInductive type) do
208+
return false
209+
if let .const declName _ := type.getAppFn then
210+
saveCases declName true
211+
let mvarId ← mkAuxMVarForCurrGoal
212+
let mvarIds ← cases mvarId (mkFVar fvarId)
213+
let goal ← getGoal
214+
let goals := mvarIds.map fun mvarId => { goal with mvarId }
215+
mkChoice (mkMVar mvarId) goals generation
216+
return true
217+
return false
218+
219+
/--
220+
Introduce new hypotheses (and apply `by_contra`) until goal is of the form `... ⊢ False`
221+
or is inconsistent.
222+
-/
223+
def intros (generation : Nat) : SearchM Unit := withCurrGoalContext do
224+
repeat
225+
if (← isInconsistent) then
220226
return ()
221-
match (← introNext goal generation) with
227+
match (← introNext (← getGoal) generation) with
222228
| .done goal =>
223229
let goal ← exfalsoIfNotProp goal
224230
if let some mvarId ← goal.mvarId.byContra? then
225-
go { goal with mvarId }
231+
setGoal { goal with mvarId }
226232
else
227-
modify fun s => s.push goal
228-
| .newHyp fvarId goal =>
229-
if let some goals ← applyCases? goal fvarId then
230-
goals.forM go
231-
else if let some goal ← applyInjection? goal fvarId then
232-
go goal
233-
else
234-
go (← GoalM.run' goal <| addHypothesis fvarId generation)
233+
setGoal goal
234+
return ()
235235
| .newDepHyp goal =>
236-
go goal
236+
setGoal goal
237237
| .newLocal fvarId goal =>
238-
if let some goals ← applyCases? goal fvarId then
239-
goals.forM go
238+
setGoal goal
239+
discard <| applyCases? fvarId generation
240+
| .newHyp fvarId goal =>
241+
if let some goal ← applyInjection? goal fvarId then
242+
setGoal goal
240243
else
241-
go goal
242-
let (_, goals) ← (go goal).run #[]
243-
return goals.toList
244+
setGoal goal
245+
if (← applyCases? fvarId generation) then
246+
pure ()
247+
else
248+
addHypothesis fvarId generation
249+
250+
/--
251+
Similar to `intros`, but returns `true` if new hypotheses have been added,
252+
and `false` otherwise.
253+
-/
254+
def intros' (generation : Nat) : SearchM Bool := do
255+
let target ← (← getGoal).mvarId.getType
256+
if target.isFalse then return false
257+
intros generation
258+
return true
244259

245260
/-- Asserts a new fact `prop` with proof `proof` to the given `goal`. -/
246-
def assertAt (proof : Expr) (prop : Expr) (generation : Nat) : GrindTactic' := fun goal => do
247-
if isEagerCasesCandidate goal prop then
261+
def assertAt (proof : Expr) (prop : Expr) (generation : Nat) : SearchM Unit := do
262+
if isEagerCasesCandidate (← getGoal) prop then
263+
let goal ← getGoal
248264
let mvarId ← goal.mvarId.assert (← mkFreshUserName `h) prop proof
249-
let goal := { goal with mvarId }
250-
intros generation goal
251-
else
252-
let goal ← GoalM.run' goal do
253-
let r ← preprocess prop
254-
let prop' := r.expr
255-
let proof' := mkApp4 (mkConst ``Eq.mp [levelZero]) prop r.expr (← r.getProof) proof
256-
add prop' proof' generation
257-
if goal.inconsistent then return [] else return [goal]
265+
setGoal { goal with mvarId }
266+
intros generation
267+
else withCurrGoalContext do
268+
let r ← preprocess prop
269+
let prop' := r.expr
270+
let proof' := mkApp4 (mkConst ``Eq.mp [levelZero]) prop r.expr (← r.getProof) proof
271+
add prop' proof' generation
258272

259-
/-- Asserts next fact in the `goal` fact queue. -/
260-
def assertNext : GrindTactic := fun goal => do
273+
/--
274+
Asserts next fact in the `goal` fact queue.
275+
Returns `true` if the queue was not empty and `false` otherwise.
276+
-/
277+
def assertNext : SearchM Bool := do
278+
if (← isInconsistent) then return false
279+
let goal ← getGoal
261280
let some (fact, newRawFacts) := goal.newRawFacts.dequeue?
262-
| return none
263-
assertAt fact.proof fact.prop fact.generation { goal with newRawFacts }
281+
| return false
282+
setGoal { goal with newRawFacts }
283+
assertAt fact.proof fact.prop fact.generation
284+
return true
264285

265-
/-- Asserts all facts in the `goal` fact queue. -/
266-
partial def assertAll : GrindTactic := fun goal =>
267-
if goal.newRawFacts.isEmpty then
268-
return none
269-
else
270-
assertNext.iterate goal
286+
/--
287+
Asserts all facts in the `goal` fact queue.
288+
Returns `true` if the queue was not empty and `false` otherwise.
289+
-/
290+
def assertAll : SearchM Bool := do
291+
let mut progress := false
292+
repeat
293+
if (← assertNext) then
294+
progress := true
295+
else
296+
return progress
297+
unreachable!
271298

272299
end Lean.Meta.Grind

0 commit comments

Comments
 (0)