Compare commits

...

7 Commits

Author SHA1 Message Date
Kim Morrison
302dc94544 fix tests 2025-11-13 08:14:14 +11:00
Kim Morrison
788b82cfe6 fix 2025-11-12 06:18:46 +01:00
Kim Morrison
535cdc1e6a comment 2025-11-12 06:13:05 +01:00
Kim Morrison
f2a95f6655 looks good 2025-11-12 06:13:05 +01:00
Kim Morrison
a214d8c432 capture try: this 2025-11-12 06:13:05 +01:00
Kim Morrison
e3ebd747b7 checkpoint 2025-11-12 06:13:05 +01:00
Kim Morrison
27b0a0f1cc checkpoint 2025-11-12 06:13:03 +01:00
5 changed files with 295 additions and 7 deletions

View File

@@ -57,3 +57,16 @@ syntax (name := attemptAll) "attempt_all " withPosition((ppDedent(ppLine) colGe
syntax (name := tryResult) "try_suggestions " tactic* : tactic
end Lean.Parser.Tactic
namespace Lean.Parser.Command
/--
`register_try?_tactic tac` registers a tactic `tac` as a suggestion generator for the `try?` tactic.
An optional priority can be specified with `register_try?_tactic (priority := 500) tac`.
Higher priority generators are tried first. The default priority is 1000.
-/
syntax (name := registerTryTactic) (docComment)?
"register_try?_tactic" ("(" &"priority" ":=" num ")")? tacticSeq : command
end Lean.Parser.Command

View File

@@ -11,6 +11,7 @@ public import Lean.Meta.Tactic.Try
public import Lean.Elab.Tactic.SimpTrace
public import Lean.Elab.Tactic.LibrarySearch
public import Lean.Elab.Tactic.Grind
meta import Lean.Elab.Command
public section
@@ -244,6 +245,115 @@ builtin_initialize tryTacticElabAttribute : KeyedDeclsAttribute TryTactic ← do
private def getEvalFns (kind : SyntaxNodeKind) : CoreM (List (KeyedDeclsAttribute.AttributeEntry TryTactic)) := do
return tryTacticElabAttribute.getEntries ( getEnv) kind
/-! User-extensible try suggestion generators -/
/-- A user-defined generator that proposes tactics for `try?` to attempt.
Takes the goal MVarId and collected info, returns array of tactics to try. -/
abbrev TrySuggestionGenerator := MVarId Try.Info MetaM (Array (TSyntax `tactic))
/-- Entry in the try suggestion registry -/
structure TrySuggestionEntry where
name : Name
prio : Nat
deriving Inhabited
/-- Environment extension for user try suggestion generators (supports local scoping) -/
builtin_initialize trySuggestionExtension : SimpleScopedEnvExtension TrySuggestionEntry (Array TrySuggestionEntry)
registerSimpleScopedEnvExtension {
addEntry := fun entries entry =>
-- Insert new entry and maintain sorted order by priority (higher = runs first)
(entries.push entry).qsort (·.prio > ·.prio)
initial := #[]
}
/-- Register the @[try_suggestion prio] attribute -/
builtin_initialize registerBuiltinAttribute {
name := `try_suggestion
descr := "Register a tactic suggestion generator for try? (runs after built-in tactics)"
add := fun declName stx kind => do
let prio match stx with
| `(attr| try_suggestion $n:num) => pure n.getNat
| `(attr| try_suggestion) => pure 1000 -- Default priority
| _ => throwError "invalid 'try_suggestion' attribute syntax"
let attrKind := if kind == AttributeKind.local then AttributeKind.local else AttributeKind.global
trySuggestionExtension.add {
name := declName,
prio := prio
} attrKind
}
/-- Elaborate `register_try?_tactic` command -/
@[builtin_command_elab registerTryTactic]
meta def elabRegisterTryTactic : Command.CommandElab := fun stx => do
if `Lean.Elab.Tactic.Try ( getEnv).header.moduleNames then
logWarning "Add `import Lean.Elab.Tactic.Try` before using the `register_try?_tactic` command."
return
-- stx[0]: optional docComment, stx[1]: "register_try?_tactic",
-- stx[2]: optional priority clause, stx[3]: tacticSeq
let doc? := stx[0]
let prio := if stx[2].isNone then 1000 else stx[2][0][3].isNatLit?.getD 1000
let tacStx := stx[3]
-- Generate a unique name based on a hash of the tactic syntax
let tacHash := hash tacStx.prettyPrint.pretty
let name := Name.mkSimple s!"auxTryTactic{tacHash}"
-- Generate code that parses the tactic at runtime
let prioStx := Syntax.mkNumLit (toString prio)
let nameId := mkIdent name
let tacText := Syntax.mkStrLit tacStx.prettyPrint.pretty
let cmd `(command|
open Lean Meta Elab Tactic Try in
@[try_suggestion $prioStx] meta def $nameId
(_goal : MVarId) (_info : Try.Info) : MetaM (Array (TSyntax `tactic)) := do
let env getEnv
match Parser.runParserCategory env `tactic $tacText with
| Except.ok stx => return #[stx]
| Except.error _ => return #[])
let finalCmd := if doc?.isNone then cmd else doc?.setArg 1 cmd.raw
Command.elabCommand finalCmd
/--
Evaluates a user-generated tactic and captures any "Try this" suggestions it produces
by examining the message log.
Returns an array of tactics: the original tactic followed by any extracted suggestions.
-/
private def expandUserTactic (tac : TSyntax `tactic) (goal : MVarId) : MetaM (Array (TSyntax `tactic)) := do
Term.TermElabM.run' <| do
let initialState saveState
let initialLog Core.getMessageLog
let initialMsgCount := initialLog.toList.length
let result try
-- Run the tactic to capture its "Try this" messages
discard <| Tactic.run goal do
evalTactic tac
-- Extract tactic suggestions from new messages
-- This parses the format produced by TryThis.addSuggestions: "Try this:\n [apply] tactic"
let newMsgs := ( Core.getMessageLog).toList.drop initialMsgCount
let mut suggestions : Array (TSyntax `tactic) := #[]
for msg in newMsgs do
if msg.severity == MessageSeverity.information then
let msgText msg.data.toString
for line in msgText.splitOn "\n" do
if line.startsWith " [apply] " then
let tacticText := line.drop " [apply] ".length
let env getEnv
if let .ok stx := Parser.runParserCategory env `tactic tacticText then
suggestions := suggestions.push stx
pure (some suggestions)
catch _ =>
pure none
initialState.restore
Core.setMessageLog initialLog
return #[tac] ++ (result.getD #[])
-- TODO: polymorphic `Tactic.focus`
abbrev focus (x : TryTacticM α) : TryTacticM α := fun ctx => Tactic.focus (x ctx)
@@ -653,10 +763,13 @@ private def addSuggestions (tk : Syntax) (s : Array Tactic.TryThis.Suggestion) :
Tactic.TryThis.addSuggestions tk (s.map fun stx => stx) (origSpan? := ( getRef))
def evalAndSuggest (tk : Syntax) (tac : TSyntax `tactic) (config : Try.Config := {}) : TacticM Unit := do
let initialLog Core.getMessageLog
let tac' try
evalSuggest tac |>.run { terminal := true, root := tac, config }
catch _ =>
throwEvalAndSuggestFailed config
-- Restore message log to suppress "Try this" messages from intermediate tactic executions
Core.setMessageLog initialLog
let s := (getSuggestions tac')[*...config.max].toArray
if s.isEmpty then
throwEvalAndSuggestFailed config
@@ -763,26 +876,49 @@ private def mkAllIndStx (info : Try.Info) (cont : TSyntax `tactic) : MetaM (TSyn
/-! Main code -/
/-- Returns tactic for `evalAndSuggest` -/
private def mkTryEvalSuggestStx (info : Try.Info) : MetaM (TSyntax `tactic) := do
/-- Returns tactic for `evalAndSuggest` (unsafe version that can evaluate user generators) -/
private unsafe def mkTryEvalSuggestStxUnsafe (goal : MVarId) (info : Try.Info) : MetaM (TSyntax `tactic) := do
let simple mkSimpleTacStx
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 inds mkAllIndStx info atomic
let extra `(tactic| (intros; first | $simple:tactic | $simp:tactic | exact?))
`(tactic| first | $atomic:tactic | $atomicSuggestions:tactic | $funInds:tactic | $inds:tactic | $extra:tactic)
-- TODO: make it extensible.
-- Collect user-defined suggestions (runs after built-in tactics)
let userEntries := trySuggestionExtension.getState ( getEnv)
let mut userTactics := #[]
for entry in userEntries do
try
let generator evalConst TrySuggestionGenerator entry.name
let suggestions generator goal info
-- Expand each tactic to capture nested "Try this" suggestions
for userTac in suggestions do
let expandedTacs expandUserTactic userTac goal
userTactics := userTactics ++ expandedTacs
catch e =>
logWarning m!"try_suggestion generator {entry.name} failed: {e.toMessageData}"
-- Build final tactic: built-ins first, then user suggestions as fallback
if userTactics.isEmpty then
`(tactic| first | $atomic:tactic | $atomicSuggestions:tactic | $funInds:tactic | $inds:tactic | $extra:tactic)
else
let userAttemptAll `(tactic| attempt_all $[| $userTactics:tactic]*)
`(tactic| first | $atomic:tactic | $atomicSuggestions:tactic | $funInds:tactic | $inds:tactic | $extra:tactic | $userAttemptAll:tactic)
@[implemented_by mkTryEvalSuggestStxUnsafe]
private opaque mkTryEvalSuggestStx (goal : MVarId) (info : Try.Info) : MetaM (TSyntax `tactic)
@[builtin_tactic Lean.Parser.Tactic.tryTrace] def evalTryTrace : Tactic := fun stx => do
match stx with
| `(tactic| try?%$tk $config:optConfig) => Tactic.focus do withMainContext do
let config elabTryConfig config
let info Try.collect ( getMainGoal) config
let stx mkTryEvalSuggestStx info
let goal getMainGoal
let info Try.collect goal config
let stx mkTryEvalSuggestStx goal info
evalAndSuggest tk stx config
| _ => throwUnsupportedSyntax

View File

@@ -0,0 +1,14 @@
/-
Test that register_try?_tactic works as a builtin command
The syntax is available without any imports, but it warns when Lean.Elab.Tactic.Try is not imported
-/
-- Custom predicate for testing
inductive MyProp : Prop where
| intro : MyProp
/--
warning: Add `import Lean.Elab.Tactic.Try` before using the `register_try?_tactic` command.
-/
#guard_msgs in
register_try?_tactic exact MyProp.intro

View File

@@ -0,0 +1,125 @@
/-
Test that try? supports user-defined suggestion generators via @[try_suggestion]
-/
module
public import Lean
public meta import Lean.Elab.Tactic.Try
open Lean Meta Elab Tactic Try
-- Define a custom inductive predicate that built-in tactics won't solve automatically
inductive CustomProp : Prop where
| mk : CustomProp
-- Lemma about CustomProp
theorem customPropHolds : CustomProp := CustomProp.mk
section BasicTest
-- Define a generator that suggests the custom lemma
@[local try_suggestion]
meta def customPropSolver (_goal : MVarId) (_info : Try.Info) : MetaM (Array (TSyntax `tactic)) := do
return #[ `(tactic| exact CustomProp.mk)]
-- Test that try? picks up the user-defined suggestion
-- Built-in tactics won't solve this, so only user suggestion appears
/--
info: Try this:
[apply] exact CustomProp.mk✝
-/
#guard_msgs in
example : CustomProp := by
try?
end BasicTest
section PriorityTest
-- Test priority ordering with multiple generators
@[local try_suggestion 2000]
meta def highPrioritySolver (_goal : MVarId) (_info : Try.Info) : MetaM (Array (TSyntax `tactic)) := do
return #[ `(tactic| apply CustomProp.mk)]
@[local try_suggestion 1000]
meta def midPrioritySolver (_goal : MVarId) (_info : Try.Info) : MetaM (Array (TSyntax `tactic)) := do
return #[ `(tactic| exact CustomProp.mk)]
@[local try_suggestion 500]
meta def lowPrioritySolver (_goal : MVarId) (_info : Try.Info) : MetaM (Array (TSyntax `tactic)) := do
return #[ `(tactic| constructor)]
-- All generators return successful tactics, ordered by priority (highest first)
/--
info: Try these:
[apply] apply CustomProp.mk✝
[apply] exact CustomProp.mk✝
[apply] constructor
-/
#guard_msgs in
example : CustomProp := by
try?
end PriorityTest
section BuiltInFallback
-- Test that user generators only run if built-ins fail
-- For True, built-ins succeed so user generators shouldn't run
@[local try_suggestion]
meta def shouldNotRunForTrue (_goal : MVarId) (_info : Try.Info) : MetaM (Array (TSyntax `tactic)) := do
return #[ `(tactic| exact True.intro)]
-- Built-ins succeed, so user suggestion doesn't appear
/--
info: Try these:
[apply] simp
[apply] simp only
[apply] grind
[apply] grind only
[apply] simp_all
-/
#guard_msgs in
example : True := by
try?
end BuiltInFallback
section DoubleSuggestion
-- Test double-suggestion: when a user tactic produces "Try this" messages,
-- both the original tactic and the suggestions should appear
-- Use CustomProp which built-ins can't solve
@[local try_suggestion]
meta def doubleSuggestionSolver (_goal : MVarId) (_info : Try.Info) : MetaM (Array (TSyntax `tactic)) := do
return #[ `(tactic| show_term apply CustomProp.mk)]
-- Double-suggestion: when show_term produces a "Try this" message,
-- both the original tactic and the extracted suggestion should appear
-- The message from show_term during extraction is suppressed
/--
info: Try these:
[apply] show_term apply CustomProp.mk✝
[apply] exact CustomProp.mk
-/
#guard_msgs in
example : CustomProp := by
try?
end DoubleSuggestion
section RegisterCommand
-- Test the register_try?_tactic convenience command
register_try?_tactic (priority := 500) constructor
/--
info: Try this:
[apply] constructor
-/
#guard_msgs in
example : CustomProp := by
try?
-- Test without explicit priority (should default to 1000, so appear before constructor at 500)
register_try?_tactic apply CustomProp.mk
/--
info: Try these:
[apply] apply CustomProp.mk
[apply] constructor
-/
#guard_msgs in
example : CustomProp := by
try?
end RegisterCommand

View File

@@ -1 +1 @@
versoDocMissing.lean:10:0: error: unexpected end of input; expected '#guard_msgs', 'abbrev', 'add_decl_doc', 'axiom', 'binder_predicate', 'builtin_dsimproc', 'builtin_dsimproc_decl', 'builtin_grind_propagator', 'builtin_initialize', 'builtin_simproc', 'builtin_simproc_decl', 'class', 'coinductive', 'declare_simp_like_tactic', 'declare_syntax_cat', 'def', 'dsimproc', 'dsimproc_decl', 'elab', 'elab_rules', 'example', 'grind_propagator', 'inductive', 'infix', 'infixl', 'infixr', 'initialize', 'instance', 'macro', 'macro_rules', 'notation', 'opaque', 'postfix', 'prefix', 'recommended_spelling', 'register_error_explanation', 'register_tactic_tag', 'simproc', 'simproc_decl', 'structure', 'syntax', 'tactic_extension', 'theorem' or 'unif_hint'
versoDocMissing.lean:10:0: error: unexpected end of input; expected '#guard_msgs', 'abbrev', 'add_decl_doc', 'axiom', 'binder_predicate', 'builtin_dsimproc', 'builtin_dsimproc_decl', 'builtin_grind_propagator', 'builtin_initialize', 'builtin_simproc', 'builtin_simproc_decl', 'class', 'coinductive', 'declare_simp_like_tactic', 'declare_syntax_cat', 'def', 'dsimproc', 'dsimproc_decl', 'elab', 'elab_rules', 'example', 'grind_propagator', 'inductive', 'infix', 'infixl', 'infixr', 'initialize', 'instance', 'macro', 'macro_rules', 'notation', 'opaque', 'postfix', 'prefix', 'recommended_spelling', 'register_error_explanation', 'register_tactic_tag', 'register_try?_tactic', 'simproc', 'simproc_decl', 'structure', 'syntax', 'tactic_extension', 'theorem' or 'unif_hint'