feat: add library suggestions support to try? tactic (#11132)

This PR adds support for `grind +suggestions` and `simp_all?
+suggestions` in `try?`. It outputs `grind only [X, Y, Z]` or `simp_all
only [X, Y, Z]` suggestions (rather than just `+suggestions`).

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Kim Morrison 2025-11-11 17:38:28 +11:00 committed by GitHub
parent fe8238c76c
commit 02b141ca15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 173 additions and 5 deletions

View file

@ -197,6 +197,18 @@ private def evalSuggestAtomic (tac : TSyntax `tactic) : TacticM (TSyntax `tactic
else
return tac
/-- Check if a config contains `+suggestions` -/
private def configHasSuggestions (config : TSyntax ``Lean.Parser.Tactic.optConfig) : Bool :=
let configItems := config.raw.getArgs
configItems.any fun item =>
match item[0]? with
| some configItem => match configItem[0]? with
| some posConfigItem => match posConfigItem[1]? with
| some ident => posConfigItem.getKind == ``Lean.Parser.Tactic.posConfigItem && ident.getId == `suggestions
| none => false
| none => false
| none => false
private def grindTraceToGrind (tac : TSyntax `tactic) : TacticM (TSyntax `tactic) := do
match tac with
| `(tactic| grind? $config:optConfig $[only%$only]? $[ [$params:grindParam,*] ]?) =>
@ -389,25 +401,85 @@ private def evalSuggestGrindTrace : TryTactic := fun tac => do
trace[try.debug] "`grind` succeeded"
if (← read).config.only then
let tac' ← mkGrindOnly configStx trace
mkTrySuggestions #[tac, tac']
-- If config has +suggestions, only return the 'only' version, not the original
if configHasSuggestions configStx then
mkTrySuggestions #[tac']
else
mkTrySuggestions #[tac, tac']
else
return tac
| _ => throwUnsupportedSyntax
private def evalSuggestSimpTrace : TryTactic := fun tac => do (← getMainGoal).withContext do
match tac with
| `(tactic| simp? $_:optConfig $[only%$only]? $[[$args,*]]? $(loc)?) =>
| `(tactic| simp? $configStx:optConfig $[only%$only]? $[[$args,*]]? $(loc)?) =>
let tac ← simpTraceToSimp tac
let { ctx, simprocs, .. } ← mkSimpContext tac (eraseLocal := false)
let stats ← simpLocation ctx (simprocs := simprocs) none <| (loc.map expandLocation).getD (.targets #[] true)
trace[try.debug] "`simp` succeeded"
if (← read).config.only then
let tac' ← mkSimpCallStx tac stats.usedTheorems
mkTrySuggestions #[tac, tac']
-- If config has +suggestions, only return the 'only' version, not the original
if configHasSuggestions configStx then
mkTrySuggestions #[tac']
else
mkTrySuggestions #[tac, tac']
else
return tac
| _ => throwUnsupportedSyntax
private def evalSuggestSimpAllTrace : TryTactic := fun tac => do
match tac with
| `(tactic| simp_all? $[!%$_bang]? $configStx:optConfig $(_discharger)? $[only%$_only]? $[[$_args,*]]?) =>
(← getMainGoal).withContext do
let hasSuggestions := configHasSuggestions configStx
-- Get library suggestions if +suggestions is present
let config ← elabSimpConfig configStx (kind := .simpAll)
let mut argsArray : TSyntaxArray [`Lean.Parser.Tactic.simpErase, `Lean.Parser.Tactic.simpLemma] := #[]
if config.suggestions then
let suggestions ← Lean.LibrarySuggestions.select (← getMainGoal)
for sugg in suggestions do
let ident := mkIdent sugg.name
let candidates ← resolveGlobalConst ident
for candidate in candidates do
let arg ← `(Parser.Tactic.simpLemma| $(mkCIdentFrom ident candidate (canonical := true)):term)
argsArray := argsArray.push arg
-- Build tactic with resolved suggestions for execution
-- If +suggestions was present, we need to create a tactic without +suggestions,
-- either with the resolved suggestions or without arguments if none were found
-- Note: We use simp_all (without ?) for execution, not simp_all?
let tacForExec ← if config.suggestions then
if argsArray.isEmpty then
`(tactic| simp_all)
else
`(tactic| simp_all [$argsArray,*])
else
pure tac
let { ctx, simprocs, .. } ← mkSimpContext tacForExec (eraseLocal := true) (kind := .simpAll) (ignoreStarArg := true)
let (result?, stats) ← simpAll (← getMainGoal) ctx (simprocs := simprocs)
match result? with
| none => replaceMainGoal []
| some mvarId => replaceMainGoal [mvarId]
trace[try.debug] "`simp_all` succeeded"
if (← read).config.only then
-- Remove +suggestions from config for the output (similar to SimpTrace.lean)
let filteredCfg ← filterSuggestionsFromSimpConfig configStx
-- Convert simp_all? to simp_all for mkSimpCallStx (similar to simpTraceToSimp)
let tacWithoutTrace ← `(tactic| simp_all $filteredCfg:optConfig $[only%$_only]? $[[$_args,*]]?)
let tac' ← mkSimpCallStx tacWithoutTrace stats.usedTheorems
-- If config has +suggestions, only return the 'only' version, not the original
if hasSuggestions then
mkTrySuggestions #[tac']
else
mkTrySuggestions #[tac, tac']
else
return tac
| _ => throwUnsupportedSyntax
@[extern "lean_eval_suggest_tactic"] -- forward definition to avoid mutual block
opaque evalSuggest : TryTactic
@ -547,6 +619,8 @@ private partial def evalSuggestImpl : TryTactic := fun tac => do
evalSuggestGrindTrace tac
else if k == ``Parser.Tactic.simpTrace then
evalSuggestSimpTrace tac
else if k == ``Parser.Tactic.simpAllTrace then
evalSuggestSimpAllTrace tac
else if k == ``Parser.Tactic.exact? then
evalSuggestExact
else
@ -629,6 +703,11 @@ set_option hygiene false in -- Avoid tagger at `+arith`
private def mkSimpStx : CoreM (TSyntax `tactic) :=
`(tactic| first | simp? | simp? [*] | simp? +arith | simp? +arith [*])
set_option hygiene false in -- Avoid tagger at `+suggestions`
/-- Atomic tactics with library suggestions -/
private def mkAtomicWithSuggestionsStx : CoreM (TSyntax `tactic) :=
`(tactic| attempt_all | grind? +suggestions | simp_all? +suggestions)
/-- `simple` tactics -/
private def mkSimpleTacStx : CoreM (TSyntax `tactic) :=
`(tactic| attempt_all | rfl | assumption)
@ -668,13 +747,13 @@ private def mkTryEvalSuggestStx (info : Try.Info) : MetaM (TSyntax `tactic) := d
let simp ← mkSimpStx
let grind ← mkGrindStx info
let atomic ← `(tactic| attempt_all | $simple:tactic | $simp:tactic | $grind:tactic | simp_all)
let atomicSuggestions ← mkAtomicWithSuggestionsStx
let funInds ← mkAllFunIndStx info atomic
let extra ← `(tactic| (intros; first | $simple:tactic | $simp:tactic | exact?))
`(tactic| first | $atomic:tactic | $funInds:tactic | $extra:tactic)
`(tactic| first | $atomic:tactic | $atomicSuggestions:tactic | $funInds:tactic | $extra:tactic)
-- TODO: vanilla `induction`.
-- TODO: make it extensible.
-- TODO: library suggestions.
@[builtin_tactic Lean.Parser.Tactic.tryTrace] def evalTryTrace : Tactic := fun stx => do
match stx with

View file

@ -0,0 +1,89 @@
import Lean.LibrarySuggestions
-- Test that try? can find solutions using grind? +suggestions and simp_all? +suggestions
-- Test 1: Regular tactics should be tried first (rfl should win)
/--
info: Try these:
[apply] rfl
[apply] simp
[apply] simp only
[apply] grind
[apply] grind only
[apply] simp_all
-/
#guard_msgs in
example : 5 = 5 := by
try?
-- Test 2: A theorem that requires library suggestions
-- Define an opaque constant so grind can't unfold it
axiom SpecialProperty : Nat → Prop
-- A specific instance that's provable
axiom special_7 : SpecialProperty 7
-- Set up a premise selector that suggests special_7
set_library_suggestions (fun _ _ => pure #[{ name := `special_7, score := 1.0 }])
-- Expected: try? should find grind only [special_7] and simp_all only [special_7]
/--
info: Try these:
[apply] grind only [special_7]
[apply] simp_all only [special_7]
-/
#guard_msgs in
example : SpecialProperty 7 := by
try?
-- Test 3: For simp_all testing, use an axiom-based approach
axiom CustomOp : Nat → Nat → Nat
-- A property about CustomOp that can't be proven by unfolding
axiom custom_comm : ∀ x y, CustomOp x y = CustomOp y x
-- Set up a premise selector that suggests custom_comm
set_library_suggestions (fun _ _ => pure #[{ name := `custom_comm, score := 1.0 }])
-- Expected: try? should find grind only [custom_comm] and simp_all only [custom_comm]
/--
info: Try these:
[apply] grind only [custom_comm]
[apply] simp_all only [custom_comm]
-/
#guard_msgs in
example (a b : Nat) : CustomOp a b = CustomOp b a := by
try?
-- Test 4: With a hypothesis that needs library suggestions
/--
info: Try these:
[apply] grind only [custom_comm]
[apply] simp_all only [custom_comm]
-/
#guard_msgs in
example (a b c : Nat) (h : CustomOp a b = c) : CustomOp b a = c := by
try?
-- Test 5: Multiple library suggestions with grind
axiom Property1 : Nat → Prop
axiom Property2 : Nat → Prop
axiom prop1_5 : Property1 5
axiom prop2_5 : Property2 5
-- Set up a premise selector with multiple suggestions
set_library_suggestions (fun _ _ => pure #[
{ name := `prop1_5, score := 0.9 },
{ name := `prop2_5, score := 0.7 }
])
-- Expected: try? should use the best applicable one
/--
info: Try these:
[apply] grind only [prop1_5]
[apply] simp_all only [prop1_5]
-/
#guard_msgs in
example : Property1 5 := by
try?