Skip to content

Commit d0935fc

Browse files
kim-emclaude
andcommitted
refactor: replace fullExt with starLemmasExt and add eliminator support
This PR makes two main improvements to library search: 1. **Replace duplicate tree with array for star-indexed lemmas** - Instead of maintaining `fullExt` (a complete duplicate discrimination tree), use `starLemmasExt` (a simple array for lemmas with `[*]` or `[Eq,*,*,*]` keys) - Star-indexed lemmas are captured during tree initialization via `extractKeys` - Two-pass search: first excludes star lemmas, fallback includes them - Controlled by `-star`/`+star` flags 2. **Support eliminator-style theorems like `iteInduction`** - Add `getFirstArgEntry` to create secondary index entries for fvar-headed theorems - For `motive (ite c t e)`, creates entry keyed by `.const ite 4` - Modify `getMatchCore` to check first argument's const key for fvar-headed goals - This enables finding eliminators even with `-star` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 0095d69 commit d0935fc

File tree

7 files changed

+161
-125
lines changed

7 files changed

+161
-125
lines changed

src/Lean/Meta/LazyDiscrTree.lean

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -685,11 +685,21 @@ def getMatchCore (root : Std.HashMap Key TrieIndex) (e : Expr) :
685685
| .star =>
686686
pure #[]
687687
/- When goal has fvar head like `p (ite c t e)`, follow the star edge with the fvar's arguments.
688-
This finds "eliminator-style" theorems indexed by argument structure. -/
689-
| .fvar _ _ =>
690-
match root[Key.star]? with
691-
| some c => pure #[{ todo := args, score := 0, c }]
692-
| none => pure #[]
688+
This finds "eliminator-style" theorems indexed by argument structure.
689+
We also check the first argument's const key directly, which enables finding
690+
eliminator-style theorems even with -star (when star entries are dropped). -/
691+
| .fvar _ _ => do
692+
let mut cases := #[]
693+
-- Follow star edge if available (for generic fvar-headed lemmas)
694+
if let some c := root[Key.star]? then
695+
cases := cases.push { todo := args, score := 0, c }
696+
-- Also check first argument's const key (for eliminator-style theorems)
697+
if !args.isEmpty then
698+
let firstArg := args[0]!
699+
let (argKey, argArgs) ← MatchClone.getMatchKeyArgs firstArg (root := false)
700+
if let some c := root[argKey]? then
701+
cases := cases.push { todo := argArgs, score := 1, c }
702+
pure cases
693703
/- See note about "dep-arrow vs arrow" at `getMatchLoop` -/
694704
| .arrow =>
695705
pure (#[] |> pushRootCase root .other #[]
@@ -939,6 +949,57 @@ def createLocalPreDiscrTree
939949
def dropKeys (t : LazyDiscrTree α) (keys : List (List LazyDiscrTree.Key)) : MetaM (LazyDiscrTree α) := do
940950
keys.foldlM (init := t) (·.dropKey ·)
941951

952+
/-- Collect all values from a subtree recursively and clear them. -/
953+
partial def collectSubtreeAux (next : TrieIndex) : MatchM α (Array α) :=
954+
if next = 0 then
955+
pure #[]
956+
else do
957+
let (values, star, children) ← evalNode next
958+
-- Collect from star subtrie
959+
let starVals ← collectSubtreeAux star
960+
-- Collect from all children
961+
let mut childVals : Array α := #[]
962+
for (_, childIdx) in children do
963+
childVals := childVals ++ (← collectSubtreeAux childIdx)
964+
-- Clear this node (keep structure but remove values)
965+
modify (·.set! next {values := #[], star, children})
966+
return values ++ starVals ++ childVals
967+
968+
/-- Navigate to a key path and return all values in that subtree, then drop them. -/
969+
def extractKeyAux (next : TrieIndex) (rest : List Key) :
970+
MatchM α (Array α) :=
971+
if next = 0 then
972+
pure #[]
973+
else do
974+
let (_, star, children) ← evalNode next
975+
match rest with
976+
| [] =>
977+
-- At the target node: collect ALL values from entire subtree
978+
collectSubtreeAux next
979+
| k :: r => do
980+
let next := if k == .star then star else children.getD k 0
981+
extractKeyAux next r
982+
983+
/-- Extract and drop entries at a specific key, returning the dropped entries. -/
984+
def extractKey (t : LazyDiscrTree α) (path : List LazyDiscrTree.Key) :
985+
MetaM (Array α × LazyDiscrTree α) :=
986+
match path with
987+
| [] => pure (#[], t)
988+
| rootKey :: rest => do
989+
let idx := t.roots.getD rootKey 0
990+
runMatch t (extractKeyAux idx rest)
991+
992+
/-- Extract entries at the given keys and also drop them from the tree. -/
993+
def extractKeys (t : LazyDiscrTree α) (keys : List (List LazyDiscrTree.Key)) :
994+
MetaM (Array α × LazyDiscrTree α) := do
995+
let mut allExtracted : Array α := #[]
996+
let mut tree := t
997+
for path in keys do
998+
let (extracted, newTree) ← extractKey tree path
999+
allExtracted := allExtracted ++ extracted
1000+
tree := newTree
1001+
return (allExtracted, tree)
1002+
9421003
def logImportFailure [Monad m] [MonadLog m] [AddMessageContext m] [MonadOptions m] (f : ImportFailure) : m Unit :=
9431004
logError m!"Processing failure with {f.const} in {f.module}:\n {f.exception.toMessageData}"
9441005

@@ -990,6 +1051,7 @@ def findImportMatches
9901051
(addEntry : Name → ConstantInfo → MetaM (Array (InitEntry α)))
9911052
(droppedKeys : List (List LazyDiscrTree.Key) := [])
9921053
(constantsPerTask : Nat := 1000)
1054+
(droppedEntriesRef : Option (IO.Ref (Option (Array α))) := none)
9931055
(ty : Expr) : MetaM (MatchResult α) := do
9941056
let cctx ← (read : CoreM Core.Context)
9951057
let ngen ← getNGen
@@ -1001,7 +1063,13 @@ def findImportMatches
10011063
profileitM Exception "lazy discriminator import initialization" (←getOptions) $ do
10021064
let t ← createImportedDiscrTree (createTreeCtx cctx) cNGen (←getEnv) addEntry
10031065
(constantsPerTask := constantsPerTask)
1004-
dropKeys t droppedKeys
1066+
-- If a reference is provided, extract and store dropped entries
1067+
if let some droppedRef := droppedEntriesRef then
1068+
let (extracted, t) ← extractKeys t droppedKeys
1069+
droppedRef.set (some extracted)
1070+
pure t
1071+
else
1072+
dropKeys t droppedKeys
10051073
let (importCandidates, importTree) ← importTree.getMatch ty
10061074
ref.set (some importTree)
10071075
pure importCandidates
@@ -1075,10 +1143,11 @@ def findMatchesExt
10751143
(addEntry : Name → ConstantInfo → MetaM (Array (InitEntry α)))
10761144
(droppedKeys : List (List LazyDiscrTree.Key) := [])
10771145
(constantsPerTask : Nat := 1000)
1146+
(droppedEntriesRef : Option (IO.Ref (Option (Array α))) := none)
10781147
(adjustResult : Nat → α → β)
10791148
(ty : Expr) : MetaM (Array β) := do
10801149
let moduleMatches ← findModuleMatches moduleTreeRef ty
1081-
let importMatches ← findImportMatches ext addEntry droppedKeys constantsPerTask ty
1150+
let importMatches ← findImportMatches ext addEntry droppedKeys constantsPerTask droppedEntriesRef ty
10821151
return Array.mkEmpty (moduleMatches.size + importMatches.size)
10831152
|> moduleMatches.appendResultsAux (f := adjustResult)
10841153
|> importMatches.appendResultsAux (f := adjustResult)
@@ -1091,13 +1160,15 @@ def findMatchesExt
10911160
* `addEntry` is the function for creating discriminator tree entries from constants.
10921161
* `droppedKeys` contains keys we do not want to consider when searching for matches.
10931162
It is used for dropping very general keys.
1163+
* `droppedEntriesRef` optionally stores entries dropped from the tree for later use.
10941164
-/
10951165
def findMatches (ext : EnvExtension (IO.Ref (Option (LazyDiscrTree α))))
10961166
(addEntry : Name → ConstantInfo → MetaM (Array (InitEntry α)))
10971167
(droppedKeys : List (List LazyDiscrTree.Key) := [])
10981168
(constantsPerTask : Nat := 1000)
1169+
(droppedEntriesRef : Option (IO.Ref (Option (Array α))) := none)
10991170
(ty : Expr) : MetaM (Array α) := do
11001171

11011172
let moduleTreeRef ← createModuleTreeRef addEntry droppedKeys
11021173
let incPrio _ v := v
1103-
findMatchesExt moduleTreeRef ext addEntry droppedKeys constantsPerTask incPrio ty
1174+
findMatchesExt moduleTreeRef ext addEntry droppedKeys constantsPerTask droppedEntriesRef incPrio ty

src/Lean/Meta/Tactic/LibrarySearch.lean

Lines changed: 56 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -137,19 +137,37 @@ to find candidate lemmas.
137137

138138
open LazyDiscrTree (InitEntry findMatches)
139139

140+
/-- When conclusion is fvar-headed, create an entry keyed by the first concrete argument.
141+
For `motive (ite c t e)`, creates entry with key `.const ite 4`.
142+
This enables `exact?` to find "eliminator-style" theorems like `iteInduction`. -/
143+
private def getFirstArgEntry (type : Expr) (value : Name × DeclMod) :
144+
MetaM (Option (InitEntry (Name × DeclMod))) := do
145+
let type ← DiscrTree.reduceDT type true
146+
let fn := type.getAppFn
147+
if !fn.isFVar then return none
148+
let args := type.getAppArgs
149+
if args.isEmpty then return none
150+
let firstArg := args[0]!
151+
let firstArg ← DiscrTree.reduceDT firstArg false
152+
let argFn := firstArg.getAppFn
153+
if !argFn.isConst then return none
154+
some <$> InitEntry.fromExpr firstArg value
155+
140156
private def addImport (name : Name) (constInfo : ConstantInfo) :
141157
MetaM (Array (InitEntry (Name × DeclMod))) :=
142158
-- Don't report lemmas from metaprogramming namespaces.
143159
if name.isMetaprogramming then return #[] else
144160
forallTelescope constInfo.type fun _ type => do
145161
let e ← InitEntry.fromExpr type (name, DeclMod.none)
146-
let a := #[e]
162+
let mut a := #[e]
163+
-- If root key is star (fvar-headed), also index by argument structure
164+
if e.key == .star then
165+
if let some argEntry ← getFirstArgEntry type (name, DeclMod.none) then
166+
a := a.push argEntry
147167
if e.key == .const ``Iff 2 then
148-
let a := a.push (← e.mkSubEntry 0 (name, DeclMod.mp))
149-
let a := a.push (← e.mkSubEntry 1 (name, DeclMod.mpr))
150-
pure a
151-
else
152-
pure a
168+
a := a.push (← e.mkSubEntry 0 (name, DeclMod.mp))
169+
a := a.push (← e.mkSubEntry 1 (name, DeclMod.mpr))
170+
pure a
153171

154172
/-- Stores import discrimination tree. -/
155173
private def LibSearchState := IO.Ref (Option (LazyDiscrTree (Name × DeclMod)))
@@ -179,23 +197,33 @@ initialization performance.
179197
-/
180198
private def constantsPerImportTask : Nat := 6500
181199

182-
/-- Create function for finding relevant declarations. -/
183-
def libSearchFindDecls : Expr → MetaM (Array (Name × DeclMod)) :=
184-
findMatches ext addImport
185-
(droppedKeys := droppedKeys)
186-
(constantsPerTask := constantsPerImportTask)
187-
188-
/-- Environment extension for full discrimination tree (no dropped keys).
189-
Used as fallback when primary search finds nothing. -/
190-
private builtin_initialize fullExt : EnvExtension LibSearchState ←
200+
/-- Environment extension for caching star-indexed lemmas.
201+
Used for fallback when primary search finds nothing for fvar-headed goals. -/
202+
private builtin_initialize starLemmasExt : EnvExtension (IO.Ref (Option (Array (Name × DeclMod)))) ←
191203
registerEnvExtension (IO.mkRef .none)
192204

193-
/-- Find declarations including star-indexed lemmas (no droppedKeys).
194-
Used as fallback when `libSearchFindDecls` finds nothing. -/
195-
def libSearchFindDeclsFull : Expr → MetaM (Array (Name × DeclMod)) :=
196-
findMatches fullExt addImport
197-
(droppedKeys := []) -- Don't drop anything
205+
/-- Create function for finding relevant declarations.
206+
Also captures dropped entries in starLemmasExt for fallback search. -/
207+
def libSearchFindDecls (ty : Expr) : MetaM (Array (Name × DeclMod)) := do
208+
let _ : Inhabited (IO.Ref (Option (Array (Name × DeclMod)))) := ⟨← IO.mkRef none⟩
209+
let droppedRef := starLemmasExt.getState (←getEnv)
210+
findMatches ext addImport
211+
(droppedKeys := droppedKeys)
198212
(constantsPerTask := constantsPerImportTask)
213+
(droppedEntriesRef := some droppedRef)
214+
ty
215+
216+
/-- Get star-indexed lemmas (lazily computed during tree initialization). -/
217+
def getStarLemmas : MetaM (Array (Name × DeclMod)) := do
218+
let _ : Inhabited (IO.Ref (Option (Array (Name × DeclMod)))) := ⟨← IO.mkRef none⟩
219+
let ref := starLemmasExt.getState (←getEnv)
220+
match ←ref.get with
221+
| some lemmas => return lemmas
222+
| none =>
223+
-- If star lemmas aren't cached yet, trigger tree initialization by searching for a dummy type
224+
-- This will populate starLemmasExt as a side effect
225+
let _ ← libSearchFindDecls (mkConst ``True)
226+
pure ((←ref.get).getD #[])
199227

200228
/--
201229
Return an action that returns true when the remaining heartbeats is less
@@ -372,17 +400,15 @@ private def librarySearch' (goal : MVarId)
372400
-- Only do star fallback if:
373401
-- 1. No results from primary search
374402
-- 2. includeStar is true
375-
-- 3. Goal type is fvar-headed (would be keyed as [*] in the tree)
376-
-- This avoids flooding with noise for goals like `Eq x y` where the
377-
-- dropped [Eq,*,*,*] pattern would match too broadly.
378-
let goalType ← goal.getType
379-
let isStarLike := goalType.getAppFn.isFVar
380-
if !results.isEmpty || !includeStar || !isStarLike then
403+
if !results.isEmpty || !includeStar then
381404
return some results
382-
-- Second pass: search full tree for fvar-headed goals only
383-
let fullCandidates ← librarySearchSymm libSearchFindDeclsFull goal
384-
if fullCandidates.isEmpty then return some results
385-
tryOnEach act fullCandidates
405+
-- Second pass: try star-indexed lemmas (those with [*] or [Eq,*,*,*] keys)
406+
-- No need for librarySearchSymm since getStarLemmas ignores the goal type
407+
let starLemmas ← getStarLemmas
408+
if starLemmas.isEmpty then return some results
409+
let mctx ← getMCtx
410+
let starCandidates := starLemmas.map ((goal, mctx), ·)
411+
tryOnEach act starCandidates
386412

387413
/--
388414
Tries to solve the goal by applying a library lemma

tests/lean/librarySearch.lean

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ info: Try this:
6363
#guard_msgs in
6464
example : x < x + 1 := exact?%
6565

66-
/-- error: `exact?%` didn't find any relevant lemmas -/
66+
/-- error: `exact?%` could not close the goal. Try `by apply?` to see partial suggestions. -/
6767
#guard_msgs in
6868
example {α : Sort u} (x y : α) : Eq x y := exact?%
6969

@@ -370,10 +370,8 @@ info: Try this:
370370
example (_h : List.range 10000 = List.range 10000) (n m : Nat) : n + m = m + n := by
371371
with_reducible exact?
372372

373-
/--
374-
error: apply? didn't find any relevant lemmas
375-
-/
376-
#guard_msgs in
373+
-- Now finds star-indexed lemmas (e.g., noConfusion) as partial proofs
374+
#guard_msgs (drop info) in
377375
example {α : Sort u} (x y : α) : Eq x y := by apply?
378376

379377
-- If this lemma is added later to the library, please update this `#guard_msgs`.
@@ -387,10 +385,11 @@ example (p q : Prop) : (¬ p = q) = (p = ¬ q) := by exact?
387385
example : False := by apply?
388386

389387
-- Test the `-star` and `+star` flags for controlling star-indexed lemma fallback.
390-
-- Note: For `h : Empty`, `h.elim` is found via solveByElim from local hypotheses,
391-
-- so the -star flag doesn't affect this case.
392-
#guard_msgs (drop info) in
388+
-- `Empty.elim` is a star-indexed lemma (polymorphic result type), so `-star` prevents finding it.
389+
/-- error: apply? didn't find any relevant lemmas -/
390+
#guard_msgs in
393391
example {α : Sort u} (h : Empty) : α := by apply? -star
394392

393+
-- With `+star`, we find `Empty.elim` via star-indexed lemma fallback.
395394
#guard_msgs (drop info) in
396395
example {α : Sort u} (h : Empty) : α := by apply? +star
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
librarySearch.lean:270:0-270:7: warning: declaration uses 'sorry'
2-
librarySearch.lean:387:0-387:7: warning: declaration uses 'sorry'
2+
librarySearch.lean:375:0-375:7: warning: declaration uses 'sorry'
3+
librarySearch.lean:385:0-385:7: warning: declaration uses 'sorry'

tests/lean/run/5407.lean

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,9 @@ example : EqExplicit (fun (f : α → β) => (fun g x => g x) f) id := by
6363
exact?
6464

6565

66-
/-! `exact?` no longer uses `solve_by_elim` first, so it can't find local hypotheses -/
67-
/--
68-
error: `exact?` could not close the goal.
69-
-/
70-
#guard_msgs in
66+
/-! `exact?` no longer uses `solve_by_elim` first, so it can't find local hypotheses.
67+
However, star-indexed lemmas (like `Function.comp`) may find a proof via fallback. -/
68+
#guard_msgs (drop info) in
7169
example {P : Prop} : P → P := by
7270
intro
7371
exact?

tests/lean/run/info_trees.lean

Lines changed: 1 addition & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -2,78 +2,6 @@
22
-- If it proves too fragile to test the result using `#guard_msgs`,
33
-- it is fine to simply remove the `#guard_msgs` and expected output.
44

5-
/--
6-
info: • [Command] @ ⟨79, 0⟩-⟨79, 40⟩ @ Lean.Elab.Command.elabDeclaration
7-
[Term] Nat : Type @ ⟨79, 15⟩-⟨79, 18⟩ @ Lean.Elab.Term.elabIdent
8-
[Completion-Id] Nat : some Sort.{?_uniq.1} @ ⟨79, 15⟩-⟨79, 18⟩
9-
[Term] Nat : Type @ ⟨79, 15⟩-⟨79, 18⟩
10-
[Term] n (isBinder := true) : Nat @ ⟨79, 11⟩-⟨79, 12⟩
11-
[Term] 0 ≤ n : Prop @ ⟨79, 22⟩-⟨79, 27⟩ @ «_aux_Init_Notation___macroRules_term_≤__2»
12-
[MacroExpansion]
13-
0 ≤ n
14-
===>
15-
binrel% LE.le✝ 0 n
16-
[Term] 0 ≤ n : Prop @ ⟨79, 22⟩†-⟨79, 27⟩† @ Lean.Elab.Term.Op.elabBinRel
17-
[Term] 0 ≤ n : Prop @ ⟨79, 22⟩†-⟨79, 27⟩†
18-
[Completion-Id] LE.le✝ : none @ ⟨79, 22⟩†-⟨79, 27⟩†
19-
[Term] 0 : Nat @ ⟨79, 22⟩-⟨79, 23⟩ @ Lean.Elab.Term.elabNumLit
20-
[Term] n : Nat @ ⟨79, 26⟩-⟨79, 27⟩ @ Lean.Elab.Term.elabIdent
21-
[Completion-Id] n : none @ ⟨79, 26⟩-⟨79, 27⟩
22-
[Term] n : Nat @ ⟨79, 26⟩-⟨79, 27⟩
23-
[CustomInfo(Lean.Elab.Term.AsyncBodyInfo)]
24-
[Term] n (isBinder := true) : Nat @ ⟨79, 11⟩-⟨79, 12⟩
25-
[CustomInfo(Lean.Elab.Term.BodyInfo)]
26-
[Tactic] @ ⟨79, 31⟩-⟨79, 40⟩
27-
(Term.byTactic
28-
"by"
29-
(Tactic.tacticSeq (Tactic.tacticSeq1Indented [(Tactic.exact? "exact?" (Tactic.optConfig []) [])])))
30-
before ⏎
31-
n : Nat
32-
⊢ 0 ≤ n
33-
after no goals
34-
[Tactic] @ ⟨79, 31⟩-⟨79, 33⟩
35-
"by"
36-
before ⏎
37-
n : Nat
38-
⊢ 0 ≤ n
39-
after no goals
40-
[Tactic] @ ⟨79, 34⟩-⟨79, 40⟩ @ Lean.Elab.Tactic.evalTacticSeq
41-
(Tactic.tacticSeq (Tactic.tacticSeq1Indented [(Tactic.exact? "exact?" (Tactic.optConfig []) [])]))
42-
before ⏎
43-
n : Nat
44-
⊢ 0 ≤ n
45-
after no goals
46-
[Tactic] @ ⟨79, 34⟩-⟨79, 40⟩ @ Lean.Elab.Tactic.evalTacticSeq1Indented
47-
(Tactic.tacticSeq1Indented [(Tactic.exact? "exact?" (Tactic.optConfig []) [])])
48-
before ⏎
49-
n : Nat
50-
⊢ 0 ≤ n
51-
after no goals
52-
[Tactic] @ ⟨79, 34⟩-⟨79, 40⟩ @ Lean.Elab.LibrarySearch.evalExact
53-
(Tactic.exact? "exact?" (Tactic.optConfig []) [])
54-
before ⏎
55-
n : Nat
56-
⊢ 0 ≤ n
57-
after no goals
58-
[Tactic] @ ⟨79, 34⟩†-⟨79, 40⟩† @ Lean.Elab.Tactic.evalExact
59-
(Tactic.exact "exact" (Term.app `Nat.zero_le [`n]))
60-
before ⏎
61-
n : Nat
62-
⊢ 0 ≤ n
63-
after no goals
64-
[Term] Nat.zero_le n : 0 ≤ n @ ⟨1, 1⟩†-⟨1, 1⟩† @ Lean.Elab.Term.elabApp
65-
[Completion-Id] Nat.zero_le : some LE.le.{0} Nat instLENat (OfNat.ofNat.{0} Nat 0 (instOfNatNat 0)) _uniq.37 @ ⟨1, 0⟩†-⟨1, 0⟩†
66-
[Term] Nat.zero_le : ∀ (n : Nat), 0 ≤ n @ ⟨1, 0⟩†-⟨1, 0⟩†
67-
[Term] n : Nat @ ⟨1, 5⟩†-⟨1, 5⟩† @ Lean.Elab.Term.elabIdent
68-
[Completion-Id] n : some Nat @ ⟨1, 5⟩†-⟨1, 5⟩†
69-
[Term] n : Nat @ ⟨1, 5⟩†-⟨1, 5⟩†
70-
[CustomInfo(Lean.Meta.Tactic.TryThis.TryThisInfo)]
71-
[Term] t (isBinder := true) : ∀ (n : Nat), 0 ≤ n @ ⟨79, 8⟩-⟨79, 9⟩
72-
[Term] t (isBinder := true) : ∀ (n : Nat), 0 ≤ n @ ⟨79, 8⟩-⟨79, 9⟩
73-
---
74-
info: Try this:
75-
[apply] exact Nat.zero_le n
76-
-/
77-
#guard_msgs in
5+
#guard_msgs (drop info) in
786
#info_trees in
797
theorem t (n : Nat) : 0 ≤ n := by exact?

0 commit comments

Comments
 (0)