Skip to content

Commit 189f5d4

Browse files
leodemouraLeonardo de Moura
and
Leonardo de Moura
authored
feat: case splitting in grind (#6717)
This PR introduces a new feature that allows users to specify which inductive datatypes the `grind` tactic should perform case splits on. The configuration option `splitIndPred` is now set to `false` by default. The attribute `[grind cases]` is used to mark inductive datatypes and predicates that `grind` may case split on during the search. Additionally, the attribute `[grind cases eager]` can be used to mark datatypes and predicates for case splitting both during pre-processing and the search. Users can also write `grind [HasType]` or `grind [cases HasType]` to instruct `grind` to perform case splitting on the inductive predicate `HasType` in a specific instance. Similarly, `grind [-Or]` can be used to instruct `grind` not to case split on disjunctions. Co-authored-by: Leonardo de Moura <[email protected]>
1 parent c07f64a commit 189f5d4

14 files changed

+76
-124
lines changed

src/Init/Grind/Cases.lean

+3-7
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@ Authors: Leonardo de Moura
55
-/
66
prelude
77
import Init.Core
8+
import Init.Grind.Tactics
89

9-
attribute [grind_cases] And Prod False Empty True Unit Exists
10-
11-
namespace Lean.Grind.Eager
12-
13-
attribute [scoped grind_cases] Or
14-
15-
end Lean.Grind.Eager
10+
attribute [grind cases eager] And Prod False Empty True Unit Exists
11+
attribute [grind cases] Or

src/Init/Grind/Tactics.lean

+2-2
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ structure Config where
4848
splitIte : Bool := true
4949
/--
5050
If `splitIndPred` is `true`, `grind` performs case-splitting on inductive predicates.
51-
Otherwise, it performs case-splitting only on types marked with `[grind_split]` attribute. -/
52-
splitIndPred : Bool := true
51+
Otherwise, it performs case-splitting only on types marked with `[grind cases]` attribute. -/
52+
splitIndPred : Bool := false
5353
/-- By default, `grind` halts as soon as it encounters a sub-goal where no further progress can be made. -/
5454
failures : Nat := 1
5555
/-- Maximum number of heartbeats (in thousands) the canonicalizer can spend per definitional equality test. -/

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

-51
Original file line numberDiff line numberDiff line change
@@ -8,57 +8,6 @@ import Lean.Meta.Tactic.Grind.EMatchTheorem
88
import Lean.Meta.Tactic.Grind.Cases
99

1010
namespace Lean.Meta.Grind
11-
--- TODO: delete
12-
builtin_initialize grindCasesExt : SimpleScopedEnvExtension Name NameSet ←
13-
registerSimpleScopedEnvExtension {
14-
initial := {}
15-
addEntry := fun s declName => s.insert declName
16-
}
17-
18-
/--
19-
Returns `true` if `declName` has been tagged with attribute `[grind_cases]`.
20-
-/
21-
def isGrindCasesTarget (declName : Name) : CoreM Bool :=
22-
return grindCasesExt.getState (← getEnv) |>.contains declName
23-
24-
private def getAlias? (value : Expr) : MetaM (Option Name) :=
25-
lambdaTelescope value fun _ body => do
26-
if let .const declName _ := body.getAppFn' then
27-
return some declName
28-
else
29-
return none
30-
31-
/--
32-
Throws an error if `declName` cannot be annotated with attribute `[grind_cases]`.
33-
We support the following cases:
34-
- `declName` is a non-recursive datatype.
35-
- `declName` is an abbreviation for a non-recursive datatype.
36-
-/
37-
private partial def validateGrindCasesAttr (declName : Name) : CoreM Unit := do
38-
match (← getConstInfo declName) with
39-
| .inductInfo info =>
40-
if info.isRec then
41-
throwError "`{declName}` is a recursive datatype"
42-
| .defnInfo info =>
43-
let failed := throwError "`{declName}` is a definition, but it is not an alias/abbreviation for an inductive datatype"
44-
let some declName ← getAlias? info.value |>.run' {} {}
45-
| failed
46-
try
47-
validateGrindCasesAttr declName
48-
catch _ =>
49-
failed
50-
| _ =>
51-
throwError "`{declName}` is not an inductive datatype or an alias for one"
52-
53-
builtin_initialize
54-
registerBuiltinAttribute {
55-
name := `grind_cases
56-
descr := "`grind` tactic applies `cases` to (non-recursive) inductives during pre-processing step"
57-
add := fun declName _ attrKind => do
58-
validateGrindCasesAttr declName
59-
grindCasesExt.add declName attrKind
60-
}
61-
--- END of TODO: detele
6211

6312
inductive AttrKind where
6413
| ematch (k : TheoremKind)

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

+6
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ def CasesTypes.insert (s : CasesTypes) (declName : Name) (eager : Bool) : CasesT
3232
def CasesTypes.find? (s : CasesTypes) (declName : Name) : Option Bool :=
3333
s.casesMap.find? declName
3434

35+
def CasesTypes.isEagerSplit (s : CasesTypes) (declName : Name) : Bool :=
36+
s.casesMap.find? declName |>.getD false
37+
38+
def CasesTypes.isSplit (s : CasesTypes) (declName : Name) : Bool :=
39+
s.casesMap.find? declName |>.isSome
40+
3541
builtin_initialize casesExt : SimpleScopedEnvExtension CasesEntry CasesTypes ←
3642
registerSimpleScopedEnvExtension {
3743
initial := {}

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

+28-21
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ private def addSplitCandidate (e : Expr) : GoalM Unit := do
5353
trace_goal[grind.split.candidate] "{e}"
5454
modify fun s => { s with splitCandidates := e :: s.splitCandidates }
5555

56-
-- TODO: add attribute to make this extensible
5756
private def forbiddenSplitTypes := [``Eq, ``HEq, ``True, ``False]
5857

5958
/-- Returns `true` if `e` is of the form `@Eq Prop a b` -/
@@ -63,29 +62,37 @@ def isMorallyIff (e : Expr) : Bool :=
6362

6463
/-- Inserts `e` into the list of case-split candidates if applicable. -/
6564
private def checkAndAddSplitCandidate (e : Expr) : GoalM Unit := do
66-
unless e.isApp do return ()
67-
if (← getConfig).splitIte && (e.isIte || e.isDIte) then
68-
addSplitCandidate e
69-
return ()
70-
if isMorallyIff e then
71-
addSplitCandidate e
72-
return ()
73-
if (← getConfig).splitMatch then
74-
if (← isMatcherApp e) then
75-
if let .reduced _ ← reduceMatcher? e then
76-
-- When instantiating `match`-equations, we add `match`-applications that can be reduced,
77-
-- and consequently don't need to be splitted.
65+
match e with
66+
| .app .. =>
67+
if (← getConfig).splitIte && (e.isIte || e.isDIte) then
68+
addSplitCandidate e
69+
return ()
70+
if isMorallyIff e then
71+
addSplitCandidate e
72+
return ()
73+
if (← getConfig).splitMatch then
74+
if (← isMatcherApp e) then
75+
if let .reduced _ ← reduceMatcher? e then
76+
-- When instantiating `match`-equations, we add `match`-applications that can be reduced,
77+
-- and consequently don't need to be splitted.
78+
return ()
79+
else
80+
addSplitCandidate e
81+
return ()
82+
let .const declName _ := e.getAppFn | return ()
83+
if forbiddenSplitTypes.contains declName then
7884
return ()
79-
else
80-
addSplitCandidate e
85+
unless (← isInductivePredicate declName) do
8186
return ()
82-
let .const declName _ := e.getAppFn | return ()
83-
if forbiddenSplitTypes.contains declName then return ()
84-
-- We should have a mechanism for letting users to select types to case-split.
85-
-- Right now, we just consider inductive predicates that are not in the forbidden list
86-
if (← getConfig).splitIndPred then
87-
if (← isInductivePredicate declName) then
87+
if (← get).casesTypes.isSplit declName then
88+
addSplitCandidate e
89+
else if (← getConfig).splitIndPred then
8890
addSplitCandidate e
91+
| .fvar .. =>
92+
let .const declName _ := (← inferType e).getAppFn | return ()
93+
if (← get).casesTypes.isSplit declName then
94+
addSplitCandidate e
95+
| _ => pure ()
8996

9097
/--
9198
If `e` is a `cast`-like term (e.g., `cast h a`), add `HEq e a` to the to-do list.

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,12 @@ private def introNext (goal : Goal) (generation : Nat) : GrindM IntroResult := d
7474
else
7575
return .done
7676

77-
private def isCasesCandidate (type : Expr) : MetaM Bool := do
77+
def isEagerCasesCandidate (goal : Goal) (type : Expr) : Bool := Id.run do
7878
let .const declName _ := type.getAppFn | return false
79-
isGrindCasesTarget declName
79+
return goal.casesTypes.isEagerSplit declName
8080

8181
private def applyCases? (goal : Goal) (fvarId : FVarId) : MetaM (Option (List Goal)) := goal.mvarId.withContext do
82-
if (← isCasesCandidate (← fvarId.getType)) then
82+
if isEagerCasesCandidate goal (← fvarId.getType) then
8383
let mvarIds ← cases goal.mvarId (mkFVar fvarId)
8484
return mvarIds.map fun mvarId => { goal with mvarId }
8585
else
@@ -121,7 +121,7 @@ partial def intros (generation : Nat) : GrindTactic' := fun goal => do
121121

122122
/-- Asserts a new fact `prop` with proof `proof` to the given `goal`. -/
123123
def assertAt (proof : Expr) (prop : Expr) (generation : Nat) : GrindTactic' := fun goal => do
124-
if (← isCasesCandidate prop) then
124+
if isEagerCasesCandidate goal prop then
125125
let mvarId ← goal.mvarId.assert (← mkFreshUserName `h) prop proof
126126
let goal := { goal with mvarId }
127127
intros generation goal

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ private def mkGoal (mvarId : MVarId) (params : Params) : GrindM Goal := do
6767
let falseExpr ← getFalseExpr
6868
let natZeroExpr ← getNatZeroExpr
6969
let thmMap := params.ematch
70-
GoalM.run' { mvarId, thmMap } do
70+
let casesTypes := params.casesTypes
71+
GoalM.run' { mvarId, thmMap, casesTypes } do
7172
mkENodeCore falseExpr (interpreted := true) (ctor := false) (generation := 0)
7273
mkENodeCore trueExpr (interpreted := true) (ctor := false) (generation := 0)
7374
mkENodeCore natZeroExpr (interpreted := true) (ctor := false) (generation := 0)

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

+7
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,13 @@ private def checkCaseSplitStatus (e : Expr) : GoalM CaseSplitStatus := do
101101
if let some info ← isInductivePredicate? declName then
102102
if (← isEqTrue e) then
103103
return .ready info.ctors.length info.isRec
104+
if e.isFVar then
105+
let type ← whnfD (← inferType e)
106+
let report : GoalM Unit := do
107+
reportIssue "cannot perform case-split on {e}, unexpected type{indentExpr type}"
108+
let .const declName _ := type.getAppFn | report; return .resolved
109+
let .inductInfo info ← getConstInfo declName | report; return .resolved
110+
return .ready info.ctors.length info.isRec
104111
return .notReady
105112

106113
private inductive SplitCandidate where

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

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import Lean.Meta.Tactic.Util
1616
import Lean.Meta.Tactic.Ext
1717
import Lean.Meta.Tactic.Grind.ENodeKey
1818
import Lean.Meta.Tactic.Grind.Attr
19+
import Lean.Meta.Tactic.Grind.Cases
1920
import Lean.Meta.Tactic.Grind.Arith.Types
2021
import Lean.Meta.Tactic.Grind.EMatchTheorem
2122

@@ -362,6 +363,8 @@ structure Goal where
362363
nextIdx : Nat := 0
363364
/-- State of arithmetic procedures -/
364365
arith : Arith.State := {}
366+
/-- Inductive datatypes marked for case-splitting -/
367+
casesTypes : CasesTypes := {}
365368
/-- Active theorems that we have performed ematching at least once. -/
366369
thms : PArray EMatchTheorem := {}
367370
/-- Active theorems that we have not performed any round of ematching yet. -/

tests/lean/run/grind_cases.lean

-32
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,3 @@
1-
/--
2-
error: `List` is a recursive datatype
3-
-/
4-
#guard_msgs in
5-
attribute [grind_cases] List
6-
7-
/--
8-
error: `Prod.mk` is not an inductive datatype or an alias for one
9-
-/
10-
#guard_msgs in
11-
attribute [grind_cases] Prod.mk
12-
13-
/--
14-
error: `List.append` is a definition, but it is not an alias/abbreviation for an inductive datatype
15-
-/
16-
#guard_msgs in
17-
attribute [grind_cases] List.append
18-
19-
attribute [grind_cases] Prod
20-
21-
def Foo (α : Type u) := Sum α α
22-
23-
attribute [grind_cases] Foo
24-
25-
attribute [grind_cases] And
26-
27-
attribute [grind_cases] False
28-
29-
attribute [grind_cases] Empty
30-
31-
-- TODO: delete everything above
32-
331
/--
342
error: invalid `[grind cases eager]`, `List` is not a non-recursive inductive datatype or an alias for one
353
-/

tests/lean/run/grind_pre.lean

+5-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ h : c = true
3939
theorem ex (h : (f a && (b || f (f c))) = true) (h' : p ∧ q) : b && a := by
4040
grind
4141

42-
open Lean.Grind.Eager in
42+
section
43+
attribute [local grind cases eager] Or
44+
4345
/--
4446
error: `grind` failed
4547
case grind.2.1
@@ -69,6 +71,8 @@ h : b = false
6971
theorem ex2 (h : (f a && (b || f (f c))) = true) (h' : p ∧ q) : b && a := by
7072
grind
7173

74+
end
75+
7276
def g (i : Nat) (j : Nat) (_ : i > j := by omega) := i + j
7377

7478
/--

tests/lean/run/grind_split.lean

+6-3
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,13 @@ inductive HasType : Expr → Ty → Prop
5454

5555
set_option trace.grind true
5656
theorem HasType.det (h₁ : HasType e t₁) (h₂ : HasType e t₂) : t₁ = t₂ := by
57-
grind
57+
grind [HasType]
58+
59+
example (h₁ : HasType e t₁) (h₂ : HasType e t₂) : t₁ = t₂ := by
60+
grind +splitIndPred
5861

59-
theorem HasType.det' (h₁ : HasType e t₁) (h₂ : HasType e t₂) : t₁ = t₂ := by
60-
fail_if_success grind (splitIndPred := false)
62+
example (h₁ : HasType e t₁) (h₂ : HasType e t₂) : t₁ = t₂ := by
63+
fail_if_success grind
6164
sorry
6265

6366
end grind_test_induct_pred

tests/lean/run/grind_split_issue.lean

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,4 @@ h : HEq ⋯ ⋯
4747
-/
4848
#guard_msgs (error) in
4949
example {c : Nat} (q : X c 0) : False := by
50-
grind
50+
grind [X]

tests/lean/run/grind_t1.lean

+9-1
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,15 @@ example {α β} (f : α → β) (a : α) : ∃ a', f a' = f a := by
296296

297297
open List in
298298
example : (replicate n a).map f = replicate n (f a) := by
299-
grind only [Option.map_some', Option.map_none', getElem?_map, getElem?_replicate]
299+
grind +splitIndPred only [Option.map_some', Option.map_none', getElem?_map, getElem?_replicate]
300+
301+
open List in
302+
example : (replicate n a).map f = replicate n (f a) := by
303+
grind only [Exists, Option.map_some', Option.map_none', getElem?_map, getElem?_replicate]
304+
305+
open List in
306+
example : (replicate n a).map f = replicate n (f a) := by
307+
grind only [cases Exists, Option.map_some', Option.map_none', getElem?_map, getElem?_replicate]
300308

301309
open List in
302310
example : (replicate n a).map f = replicate n (f a) := by

0 commit comments

Comments
 (0)