Skip to content

Commit

Permalink
feat: improve case-split heuristic used in grind (#6658)
Browse files Browse the repository at this point in the history
This PR ensures that `grind` avoids case-splitting on terms congruent to
those that have already been case-split.
  • Loading branch information
leodemoura authored Jan 16, 2025
1 parent 0050e93 commit 6259b47
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 2 deletions.
11 changes: 11 additions & 0 deletions src/Lean/Meta/Tactic/Grind/Split.lean
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ private def checkIffStatus (e a b : Expr) : GoalM CaseSplitStatus := do
else
return .notReady

/-- Returns `true` is `c` is congruent to a case-split that was already performed. -/
private def isCongrToPrevSplit (c : Expr) : GoalM Bool := do
(← get).resolvedSplits.foldM (init := false) fun flag { expr := c' } => do
if flag then
return true
else
return isCongruent (← get).enodes c c'

private def checkCaseSplitStatus (e : Expr) : GoalM CaseSplitStatus := do
match_expr e with
| Or a b => checkDisjunctStatus e a b
Expand All @@ -85,6 +93,8 @@ private def checkCaseSplitStatus (e : Expr) : GoalM CaseSplitStatus := do
if (← isResolvedCaseSplit e) then
trace[grind.debug.split] "split resolved: {e}"
return .resolved
if (← isCongrToPrevSplit e) then
return .resolved
if let some info := isMatcherAppCore? (← getEnv) e then
return .ready info.numAlts
let .const declName .. := e.getAppFn | unreachable!
Expand Down Expand Up @@ -163,6 +173,7 @@ def splitNext : GrindTactic := fun goal => do
| return none
let gen ← getGeneration c
let genNew := if numCases > 1 || isRec then gen+1 else gen
markCaseSplitAsResolved c
trace_goal[grind.split] "{c}, generation: {gen}"
let mvarIds ← if (← isMatcherApp c) then
casesMatch (← get).mvarId c
Expand Down
5 changes: 3 additions & 2 deletions src/Lean/Meta/Tactic/Grind/Types.lean
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ structure Goal where
splitCandidates : List Expr := []
/-- Number of splits performed to get to this goal. -/
numSplits : Nat := 0
/-- Case-splits that do not have to be performed anymore. -/
/-- Case-splits that have already been performed, or that do not have to be performed anymore. -/
resolvedSplits : PHashSet ENodeKey := {}
/-- Next local E-match theorem idx. -/
nextThmIdx : Nat := 0
Expand Down Expand Up @@ -879,7 +879,8 @@ def isResolvedCaseSplit (e : Expr) : GoalM Bool :=

/--
Mark `e` as a case-split that does not need to be performed anymore.
Remark: we currently use this feature to disable `match`-case-splits
Remark: we currently use this feature to disable `match`-case-splits.
Remark: we also use this feature to record the case-splits that have already been performed.
-/
def markCaseSplitAsResolved (e : Expr) : GoalM Unit := do
unless (← isResolvedCaseSplit e) do
Expand Down
50 changes: 50 additions & 0 deletions tests/lean/run/grind_split_issue.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
variable (d : Nat) in
inductive X : Nat → Prop
| f {s : Nat} : X s
| g {s : Nat} : X d → X s

/--
error: `grind` failed
case grind.1
c : Nat
q : X c 0
s : Nat
h✝ : 0 = s
h : HEq ⋯ ⋯
⊢ False
[grind] Diagnostics
[facts] Asserted facts
[prop] X c 0
[prop] 0 = s
[prop] HEq ⋯ ⋯
[eqc] True propositions
[prop] X c 0
[prop] X c s
[eqc] Equivalence classes
[eqc] {s, 0}
case grind.2
c : Nat
q : X c 0
s : Nat
a✝¹ a✝ : X c c
h✝ : 0 = s
h : HEq ⋯ ⋯
⊢ False
[grind] Diagnostics
[facts] Asserted facts
[prop] X c 0
[prop] X c c
[prop] 0 = s
[prop] HEq ⋯ ⋯
[eqc] True propositions
[prop] X c 0
[prop] X c c
[prop] X c s
[eqc] Equivalence classes
[eqc] {s, 0}
[issues] Issues
[issue] this goal was not fully processed due to previous failures, threshold: `(failures := 1)`
-/
#guard_msgs (error) in
example {c : Nat} (q : X c 0) : False := by
grind

0 comments on commit 6259b47

Please sign in to comment.