Skip to content

Commit ec18d2f

Browse files
kim-emclaude
andcommitted
feat: add +all option to exact? and apply?
This PR adds a `+all` option to `exact?` and `apply?` that collects all successful lemmas instead of stopping at the first complete solution. This is useful when exploring what lemmas are available to prove a goal. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 3c100ad commit ec18d2f

File tree

5 files changed

+133
-27
lines changed

5 files changed

+133
-27
lines changed

src/Init/Tactics.lean

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1704,6 +1704,9 @@ structure LibrarySearchConfig where
17041704
like `[*]`) are searched as a fallback when no concrete-keyed lemmas are found.
17051705
Use `-star` to disable this fallback. -/
17061706
star : Bool := true
1707+
/-- If true, collect all successful lemmas instead of stopping at the first complete solution.
1708+
Use `+all` to enable this behavior. -/
1709+
all : Bool := false
17071710

17081711
/--
17091712
Searches environment for definitions or theorems that can solve the goal using `exact`
@@ -1716,6 +1719,7 @@ ways to resolve the goal, and one wants to guide which lemma is used.
17161719
Use `+grind` to enable `grind` as a fallback discharger for subgoals.
17171720
Use `+try?` to enable `try?` as a fallback discharger for subgoals.
17181721
Use `-star` to disable fallback to star-indexed lemmas (like `Empty.elim`, `And.left`).
1722+
Use `+all` to collect all successful lemmas instead of stopping at the first.
17191723
-/
17201724
syntax (name := exact?) "exact?" optConfig (" using " (colGt ident),+)? : tactic
17211725

@@ -1729,6 +1733,7 @@ used when closing the goal.
17291733
Use `+grind` to enable `grind` as a fallback discharger for subgoals.
17301734
Use `+try?` to enable `try?` as a fallback discharger for subgoals.
17311735
Use `-star` to disable fallback to star-indexed lemmas.
1736+
Use `+all` to collect all successful lemmas instead of stopping at the first.
17321737
-/
17331738
syntax (name := apply?) "apply?" optConfig (" using " (colGt term),+)? : tactic
17341739

src/Lean/Elab/Tactic/LibrarySearch.lean

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,37 @@ def exact? (ref : Syntax) (config : Parser.Tactic.LibrarySearchConfig)
4242
let allowFailure := fun g => do
4343
let g ← g.withContext (instantiateMVars (.mvar g))
4444
return required.all fun e => e.occurs g
45-
match (← librarySearch goal tactic allowFailure (includeStar := config.star)) with
46-
-- Found goal that closed problem
45+
match (← librarySearch goal tactic allowFailure (includeStar := config.star)
46+
(collectAll := config.all)) with
47+
-- Found goal that closed problem (only when collectAll = false)
4748
| none =>
4849
addExactSuggestion ref (← instantiateMVars (mkMVar mvar)).headBeta (checkState? := initialState)
49-
-- Found suggestions
50+
-- Found suggestions (includes complete solutions when collectAll = true)
5051
| some suggestions =>
51-
if requireClose then
52+
-- Separate complete solutions (empty remaining goals) from incomplete ones
53+
let (complete, incomplete) := suggestions.partition (·.1.isEmpty)
54+
if requireClose && !config.all then
5255
let hint := if suggestions.isEmpty then "" else " Try `apply?` to see partial suggestions."
5356
throwError "`exact?` could not close the goal.{hint}"
57+
if requireClose && config.all && complete.isEmpty then
58+
let hint := if incomplete.isEmpty then "" else " Try `apply?` to see partial suggestions."
59+
throwError "`exact?` could not close the goal.{hint}"
5460
reportOutOfHeartbeats `apply? ref
55-
for (_, suggestionMCtx) in suggestions do
61+
-- Collect complete solutions and show as a single "Try these:" message
62+
let completeExprs ← complete.mapM fun (_, suggestionMCtx) =>
5663
withMCtx suggestionMCtx do
57-
addExactSuggestion ref (← instantiateMVars (mkMVar mvar)).headBeta
58-
(checkState? := initialState) (addSubgoalsMsg := true) (tacticErrorAsInfo := true)
59-
if suggestions.isEmpty then logError "apply? didn't find any relevant lemmas"
64+
return (← instantiateMVars (mkMVar mvar)).headBeta
65+
if !completeExprs.isEmpty then
66+
addExactSuggestions ref completeExprs (checkState? := initialState)
67+
-- Show incomplete solutions only if not requireClose (i.e., for apply?)
68+
if !requireClose then
69+
let incompleteExprs ← incomplete.mapM fun (_, suggestionMCtx) =>
70+
withMCtx suggestionMCtx do
71+
return (← instantiateMVars (mkMVar mvar)).headBeta
72+
if !incompleteExprs.isEmpty then
73+
addExactSuggestions ref incompleteExprs
74+
(checkState? := initialState) (addSubgoalsMsg := true)
75+
if suggestions.isEmpty then logError "apply? didn't find any relevant lemmas"
6076
admitGoal goal (synthetic := false)
6177

6278
@[builtin_tactic Lean.Parser.Tactic.exact?]

src/Lean/Meta/Tactic/LibrarySearch.lean

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -334,15 +334,17 @@ Sequentially invokes a tactic `act` on each value in candidates on the current s
334334
335335
The tactic `act` should return a list of meta-variables that still need to be resolved.
336336
If this list is empty, then no variables remain to be solved, and `tryOnEach` returns
337-
`none` with the environment set so each goal is resolved.
337+
`none` with the environment set so each goal is resolved (unless `collectAll` is true,
338+
in which case it continues searching and collects complete solutions in the array).
338339
339340
If the action throws an internal exception with the `abortSpeculationId` id then
340341
further computation is stopped and intermediate results returned. If any other
341342
exception is thrown, then it is silently discarded.
342343
-/
343344
def tryOnEach
344345
(act : Candidate → MetaM (List MVarId))
345-
(candidates : Array Candidate) :
346+
(candidates : Array Candidate)
347+
(collectAll : Bool := false) :
346348
MetaM (Option (Array (List MVarId × MetavarContext))) := do
347349
let mut a := #[]
348350
let s ← saveState
@@ -353,7 +355,7 @@ def tryOnEach
353355
if isAbortSpeculation e then
354356
break
355357
| .ok remaining =>
356-
if remaining.isEmpty then
358+
if remaining.isEmpty && !collectAll then
357359
return none
358360
let ctx ← getMCtx
359361
restoreState s
@@ -364,7 +366,8 @@ private def librarySearch' (goal : MVarId)
364366
(tactic : List MVarId → MetaM (List MVarId))
365367
(allowFailure : MVarId → MetaM Bool)
366368
(leavePercentHeartbeats : Nat)
367-
(includeStar : Bool := true) :
369+
(includeStar : Bool := true)
370+
(collectAll : Bool := false) :
368371
MetaM (Option (Array (List MVarId × MetavarContext))) := do
369372
withTraceNode `Tactic.librarySearch (return m!"{librarySearchEmoji ·} {← goal.getType}") do
370373
profileitM Exception "librarySearch" (← getOptions) do
@@ -376,32 +379,35 @@ private def librarySearch' (goal : MVarId)
376379
librarySearchLemma cfg tactic allowFailure cand
377380
-- First pass: search with droppedKeys (excludes star-indexed lemmas)
378381
let candidates ← librarySearchSymm libSearchFindDecls goal
379-
match ← tryOnEach act candidates with
380-
| none => return none -- Found a complete solution
382+
match ← tryOnEach act candidates collectAll with
383+
| none => return none -- Found a complete solution (only when collectAll = false)
381384
| some results =>
382385
-- Only do star fallback if:
383-
-- 1. No results from primary search
386+
-- 1. No complete solutions from primary search (when collectAll, check for empty remaining)
384387
-- 2. includeStar is true
385-
if !results.isEmpty || !includeStar then
388+
let hasCompleteSolution := results.any (·.1.isEmpty)
389+
if hasCompleteSolution || !results.isEmpty || !includeStar then
386390
return some results
387391
-- Second pass: try star-indexed lemmas (those with [*] or [Eq,*,*,*] keys)
388392
-- No need for librarySearchSymm since getStarLemmas ignores the goal type
389393
let starLemmas ← getStarLemmas
390394
if starLemmas.isEmpty then return some results
391395
let mctx ← getMCtx
392396
let starCandidates := starLemmas.map ((goal, mctx), ·)
393-
tryOnEach act starCandidates
397+
tryOnEach act starCandidates collectAll
394398

395399
/--
396400
Tries to solve the goal by applying a library lemma
397401
then calling `tactic` on the resulting goals.
398402
399403
Typically here `tactic` is `solveByElim`.
400404
401-
If it successfully closes the goal, returns `none`.
405+
If it successfully closes the goal, returns `none` (unless `collectAll` is true).
402406
Otherwise, it returns `some a`, where `a : Array (List MVarId × MetavarContext)`,
403407
with an entry for each library lemma which was successfully applied,
404408
containing a list of the subsidiary goals, and the metavariable context after the application.
409+
When `collectAll` is true, complete solutions (with empty remaining goals) are also included
410+
in the array instead of returning early.
405411
406412
(Always succeeds, and the metavariable context stored in the monad is reverted,
407413
unless the goal was completely solved.)
@@ -414,9 +420,10 @@ def librarySearch (goal : MVarId)
414420
fun g => solveByElim [] (maxDepth := 6) (exfalso := false) g)
415421
(allowFailure : MVarId → MetaM Bool := fun _ => pure true)
416422
(leavePercentHeartbeats : Nat := 10)
417-
(includeStar : Bool := true) :
423+
(includeStar : Bool := true)
424+
(collectAll : Bool := false) :
418425
MetaM (Option (Array (List MVarId × MetavarContext))) := do
419-
librarySearch' goal tactic allowFailure leavePercentHeartbeats includeStar
426+
librarySearch' goal tactic allowFailure leavePercentHeartbeats includeStar collectAll
420427

421428
end LibrarySearch
422429

tests/lean/librarySearch.lean

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,31 @@ info: Try this:
9090
#guard_msgs in
9191
example (X : Type) (P : Prop) (x : X) (h : ∀ x : X, x = x → P) : P := by show_term solve_by_elim
9292

93-
-- Could be any number of results (`fun x => x`, `id`, etc)
94-
#guard_msgs (drop info) in
93+
/--
94+
info: Try this:
95+
[apply] exact fun a => a
96+
-/
97+
#guard_msgs in
9598
example (α : Prop) : α → α := by show_term solve_by_elim
9699

97100
-- These examples work via star-indexed fallback.
98-
#guard_msgs (drop info) in
101+
/--
102+
info: Try this:
103+
[apply] exact fun a => Classical.byContradiction a
104+
-/
105+
#guard_msgs in
99106
example (p : Prop) : (¬¬p) → p := by apply?
100-
#guard_msgs (drop info) in
107+
/--
108+
info: Try this:
109+
[apply] exact h.left
110+
-/
111+
#guard_msgs in
101112
example (a b : Prop) (h : a ∧ b) : a := by apply?
102-
#guard_msgs (drop info) in
113+
/--
114+
info: Try this:
115+
[apply] exact fun a a_1 => Classical.byContradiction fun a_2 => a a_2 a_1
116+
-/
117+
#guard_msgs in
103118
example (P Q : Prop) : (¬ Q → ¬ P) → (P → Q) := by apply?
104119

105120
/--
@@ -264,9 +279,13 @@ info: Try this:
264279
example {a b c : Nat} (h₁ : a ∣ c) (h₂ : a ∣ b + c) : a ∣ b := by apply?
265280

266281
-- These examples work via star-indexed fallback.
267-
#guard_msgs (drop info) in
282+
/--
283+
info: Try this:
284+
[apply] exact h.elim
285+
-/
286+
#guard_msgs in
268287
example {α : Sort u} (h : Empty) : α := by apply?
269-
#guard_msgs (drop info) in
288+
#guard_msgs in
270289
example (f : A → C) (g : B → C) : (A ⊕ B) → C := by apply?
271290

272291
opaque f : Nat → Nat
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/-!
2+
# Tests for `exact? +all`
3+
4+
This tests the `+all` option which collects all successful lemmas
5+
instead of stopping at the first complete solution.
6+
-/
7+
8+
set_option linter.unusedVariables false
9+
10+
-- Create a custom inductive with limited lemmas for controlled testing
11+
inductive MyTrue : Prop where
12+
| intro
13+
14+
-- Define exactly three ways to prove MyTrue
15+
theorem myTrue1 : MyTrue := MyTrue.intro
16+
theorem myTrue2 : MyTrue := MyTrue.intro
17+
theorem myTrue3 : MyTrue := MyTrue.intro
18+
19+
-- Test: without +all, we get a single suggestion and the goal is solved
20+
/--
21+
info: Try this:
22+
[apply] exact myTrue3
23+
-/
24+
#guard_msgs in
25+
example : MyTrue := by exact?
26+
27+
-- Test: with +all, exact? finds all lemmas (including the constructor)
28+
-- The goal is admitted (sorry), and all suggestions are shown in one message
29+
/--
30+
info: Try these:
31+
[apply] exact myTrue3
32+
[apply] exact MyTrue.intro
33+
[apply] exact myTrue1
34+
[apply] exact myTrue2
35+
---
36+
warning: declaration uses 'sorry'
37+
-/
38+
#guard_msgs in
39+
example : MyTrue := by exact? +all
40+
41+
-- Test: apply? also supports +all
42+
/--
43+
info: Try these:
44+
[apply] exact myTrue3
45+
[apply] exact MyTrue.intro
46+
[apply] exact myTrue1
47+
[apply] exact myTrue2
48+
---
49+
warning: declaration uses 'sorry'
50+
-/
51+
#guard_msgs in
52+
example : MyTrue := by apply? +all
53+
54+
-- Test: verify that +all without any matches still errors appropriately
55+
/--
56+
error: `exact?` could not close the goal. Try `apply?` to see partial suggestions.
57+
-/
58+
#guard_msgs in
59+
example (n : Nat) (h : n = n + 1) : False := by exact? +all

0 commit comments

Comments
 (0)