Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f2ee491
feat: auto-`try?` via post-elab hook with `autoTry.{onEmptyBy,onUnsol…
nomeata May 20, 2026
bcd5f6a
refactor(autoTry): drop sorry-term, skip multi-elab combinators, expa…
nomeata May 20, 2026
2c26e6d
refactor(autoTry): replace hard-coded combinator list with uniqueness…
nomeata May 20, 2026
60f6adc
refactor(autoTry): replace `TriggerKind` magic numbers with a real in…
nomeata May 20, 2026
0b9bd8b
chore(autoTry): drop unnecessary `public` and `meta` qualifiers from …
nomeata May 20, 2026
32dd66a
refactor(autoTry): apply review cleanup batch
nomeata May 20, 2026
53e9937
refactor(try?): factor out suggestion-collection entry points
nomeata May 20, 2026
f84e187
feat(autoTry): unsolved-goal trigger now appends instead of replacing
nomeata May 21, 2026
fba1aab
fix(autoTry): restore `autoTry.onEmptyBy` as narrower variant; surfac…
nomeata May 21, 2026
9c051e5
refactor(autoTry,try?): collapse redundant dedupe passes and helpers
nomeata May 21, 2026
3e92753
Merge remote-tracking branch 'origin/master' into joachim/auto-try
nomeata May 25, 2026
aa2eabb
chore(autoTry): drop "after a command is elaborated" from option descrs
nomeata May 25, 2026
0e3d5e5
chore(autoTry): rename autoTry.debug.showEdits to debug.autoTry.showE…
nomeata May 25, 2026
971aeb3
chore(autoTry): use ctx.options directly in runMetaMWithMessages
nomeata May 25, 2026
53009b8
refactor(autoTry): return suggestions directly instead of via IO.Ref
nomeata May 25, 2026
c912f1b
refactor(autoTry): emitAppendSuggestions runs in CoreM, not MetaM
nomeata May 25, 2026
6e5929e
chore(autoTry): docstring polish
nomeata May 25, 2026
4fdbf7d
feat(autoTry): broaden onUnsolvedGoal to all tactic-sequence scopes
nomeata May 25, 2026
e5c0932
refactor(autoTry): identify tactic-sequence containers generically
nomeata May 25, 2026
9359419
refactor(autoTry): emitAppendSuggestions runs in CommandElabM directly
nomeata May 25, 2026
157e3c9
test(autoTry): use #guard_msgs (positions := true) for multi-trigger …
nomeata May 25, 2026
a72b050
refactor(autoTry): collapse runCoreMWithMessages back into runMetaMWi…
nomeata May 25, 2026
72cc7c4
refactor(autoTry): drive TacticM via public ReaderT/StateRefT runners
nomeata May 25, 2026
3444e32
feat(autoTry): gate by unsolved-goals error; rename onEmptyBy to onEm…
nomeata May 25, 2026
009d2fc
chore(autoTry): plainer wording in onEmptyProof/onUnsolvedGoal docstr…
nomeata May 25, 2026
7e38fa9
fix(autoTry): correlate the unsolved-goals error with the responsible…
nomeata May 25, 2026
8c8eb2f
refactor(autoTry): explicitly detect multi-elab via tacticSeq-info-tr…
nomeata May 25, 2026
8d7b235
docs(autoTry): explain the goalsBefore fallback for admitted bodies
nomeata May 25, 2026
2796533
test(autoTry): document the admit-wrapped-body limitation
nomeata May 25, 2026
4719ce4
refactor(autoTry): replace error-position inference with elaborator h…
nomeata May 25, 2026
f22a138
fix(autoTry): hook on post-tactic state, not entry state
nomeata May 25, 2026
4c8e848
refactor(autoTry): drive trigger off `AdmittedGoalInfo` in `reportUns…
nomeata May 25, 2026
b9c0318
refactor(autoTry): drive trigger off message log, drop info-tree push
nomeata May 25, 2026
7a8678a
refactor(autoTry): drop `findLastTacticForGoal`, use message range
nomeata May 25, 2026
08ebb54
refactor(autoTry): use message ref as diagnostic anchor, narrow body …
nomeata May 25, 2026
65ee7cf
test(autoTry): use custom propositions and a single-suggestion generator
nomeata May 25, 2026
0b943ad
test(autoTry): cover `induction ... with | … => …` branches
nomeata May 25, 2026
44ef34c
fix(autoTry): scope the message walk to the current command and dedup
nomeata May 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 1 addition & 27 deletions src/Lean/Elab/BuiltinTerm.lean
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,6 @@ private def getMVarFromUserName (ident : Syntax) : MetaM Expr := do
elabTerm b expectedType?
| _ => throwUnsupportedSyntax

register_builtin_option tactic.tryOnEmptyBy : Bool := {
defValue := false
descr := "when an empty `by` block is encountered interactively, run `try?` to suggest \
a proof (currently disabled by default; may become the default in a future release)"
}

/-- Returns `true` if `stx` is a `by` expression with an empty tactic body
(not a parse error producing `.missing`).
The structure is: `node byTactic [atom "by", node tacticSeq [node tacticSeq1Indented [node null []]]]` -/
Expand All @@ -174,20 +168,7 @@ def isEmptyByBlock (stx : Syntax) : Bool :=
stx[1][0][0].getNumArgs == 0 &&
!stx[1][0][0].isMissing

/-- Returns `true` if all conditions are met for empty `by` to be elaborated as `try?`:
the body is empty, the option is enabled, we are in an interactive (non-combinator) context,
and the `try?` infrastructure (parser `Lean.Parser.Tactic.tryTrace`) is available — the latter
matters when working on the prelude, before `Init.Try` is imported. -/
def shouldElabEmptyByAsTry (stx : Syntax) : TermElabM Bool := do
return isEmptyByBlock stx
&& tactic.tryOnEmptyBy.get (← getOptions)
&& (← read).errToSorry
&& (← getEnv).contains `Lean.Parser.Tactic.tryTrace

/-- Body of the `byTactic` term elaborator: registers a tactic mvar for the body, or
errors when there's no expected type. Shared between `elabByTactic` and
`Lean.Elab.Tactic.Try`'s `elabEmptyByAsTry` so the two paths can't drift. -/
def elabByTacticCore : TermElab := fun stx expectedType? => do
@[builtin_term_elab byTactic] def elabByTactic : TermElab := fun stx expectedType? => do
match expectedType? with
| some expectedType =>
-- `by` switches from an exported to a private context, so we must disallow unassigned
Expand All @@ -198,13 +179,6 @@ def elabByTacticCore : TermElab := fun stx expectedType? => do
tryPostpone
throwError ("invalid 'by' tactic, expected type has not been provided")

@[builtin_term_elab byTactic] def elabByTactic : TermElab := fun stx expectedType? => do
-- When the conditions for `try?` on empty `by` are met, skip this elaborator so a later one
-- (in Lean.Elab.Tactic.Try) can handle it with try?.
if (← shouldElabEmptyByAsTry stx) then
throwUnsupportedSyntax
elabByTacticCore stx expectedType?

@[builtin_term_elab noImplicitLambda] def elabNoImplicitLambda : TermElab := fun stx expectedType? =>
elabTerm stx[1] (mkNoImplicitLambdaAnnotation <$> expectedType?)

Expand Down
1 change: 1 addition & 0 deletions src/Lean/Elab/Tactic.lean
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public import Lean.Elab.Tactic.Impossible
public import Lean.Elab.Tactic.Grind
public import Lean.Elab.Tactic.Monotonicity
public import Lean.Elab.Tactic.Try
public import Lean.Elab.Tactic.AutoTry
public import Lean.Elab.Tactic.AsAuxLemma
public import Lean.Elab.Tactic.TreeTacAttr
public import Lean.Elab.Tactic.ExposeNames
Expand Down
439 changes: 439 additions & 0 deletions src/Lean/Elab/Tactic/AutoTry.lean

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Lean/Elab/Tactic/Basic.lean
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ def focusAndDone (tactic : TacticM α) : TacticM α :=
done
pure a

/-- Close the main goal using the given tactic. If it fails, log the error and `admit` -/
/-- Close the main goal using the given tactic. If it fails, log the error and `admit`. -/
def closeUsingOrAdmit (tac : TacticM Unit) : TacticM Unit := do
/- Important: we must define `closeUsingOrAdmit` before we define
the instance `MonadExcept` for `TacticM` since it backtracks the state including error messages. -/
Expand Down
88 changes: 34 additions & 54 deletions src/Lean/Elab/Tactic/Try.lean
Original file line number Diff line number Diff line change
Expand Up @@ -898,8 +898,9 @@ private def addSuggestions (tk : Syntax) (s : Array Tactic.TryThis.Suggestion) :
else
Tactic.TryThis.addSuggestions tk (s.map fun stx => stx) (origSpan? := (← getRef))

def evalAndSuggest (tk : Syntax) (tac : TSyntax `tactic) (originalMaxHeartbeats : Nat) (config : Try.Config := {}) : TacticM Unit := do
-- Suppress "Try this" messages from intermediate tactic executions
/-- Like `evalAndSuggest`, but returns the suggestion array instead of emitting it. -/
def evalAndCollectSuggestions (tac : TSyntax `tactic) (originalMaxHeartbeats : Nat)
(config : Try.Config := {}) : TacticM (Array Tactic.TryThis.Suggestion) := do
let tac' ← withSuppressedMessages do
try
evalSuggest tac |>.run { terminal := true, root := tac, config, originalMaxHeartbeats }
Expand All @@ -908,8 +909,11 @@ def evalAndSuggest (tk : Syntax) (tac : TSyntax `tactic) (originalMaxHeartbeats
let s := (getSuggestions tac')[*...config.max].toArray
if s.isEmpty then
throwEvalAndSuggestFailed config
else
addSuggestions tk s
return s

def evalAndSuggest (tk : Syntax) (tac : TSyntax `tactic) (originalMaxHeartbeats : Nat) (config : Try.Config := {}) : TacticM Unit := do
let s ← evalAndCollectSuggestions tac originalMaxHeartbeats config
addSuggestions tk s

/-! Helper functions -/

Expand Down Expand Up @@ -1083,27 +1087,20 @@ private def wrapSuggestionWithBy (sugg : Tactic.TryThis.Suggestion) : TacticM Ta
/-- Version of `evalAndSuggest` that wraps tactic suggestions with `by` for term mode. -/
private def evalAndSuggestWithBy (tk : Syntax) (tac : TSyntax `tactic) (originalMaxHeartbeats : Nat)
(config : Try.Config) (footer : MessageData := MessageData.nil) : TacticM Unit := do
-- Suppress "Try this" messages from intermediate tactic executions
let tac' ← withSuppressedMessages do
try
evalSuggest tac |>.run { terminal := true, root := tac, config, originalMaxHeartbeats }
catch _ =>
throwEvalAndSuggestFailed config
let suggestions := (getSuggestions tac')[*...config.max].toArray
if suggestions.isEmpty then
throwEvalAndSuggestFailed config
let suggestions ← evalAndCollectSuggestions tac originalMaxHeartbeats config
let termSuggestions ← suggestions.mapM wrapSuggestionWithBy
if termSuggestions.size == 1 then
Tactic.TryThis.addSuggestion tk termSuggestions[0]! (origSpan? := (← getRef)) (footer := footer)
else
-- Wrap each suggestion with `by `
let termSuggestions ← suggestions.mapM wrapSuggestionWithBy
if termSuggestions.size == 1 then
Tactic.TryThis.addSuggestion tk termSuggestions[0]! (origSpan? := (← getRef)) (footer := footer)
else
Tactic.TryThis.addSuggestions tk termSuggestions (origSpan? := (← getRef)) (footer := footer)
Tactic.TryThis.addSuggestions tk termSuggestions (origSpan? := (← getRef)) (footer := footer)

/-- Core implementation of `try?`: focus, collect info, build tactic, evaluate and suggest.
`tk` is the syntax token where "Try this:" appears. The optional `footer` is appended to the
suggestions message (only when `wrapWithBy := true`). -/
private def elabTryCore (tk : Syntax) (config : Try.Config) (footer : MessageData := MessageData.nil) :
suggestions message (only when `wrapWithBy := true`).

Public so that the `autoTry` linter (`Lean.Elab.Tactic.AutoTry`) can drive `try?` from outside
the normal `try?` syntax entry point. -/
def elabTryCore (tk : Syntax) (config : Try.Config) (footer : MessageData := MessageData.nil) :
TacticM Unit :=
Tactic.focus do withMainContext do
let originalMaxHeartbeats ← getMaxHeartbeats
Expand All @@ -1116,6 +1113,22 @@ private def elabTryCore (tk : Syntax) (config : Try.Config) (footer : MessageDat
else
evalAndSuggest tk stx originalMaxHeartbeats config

/-- Like `elabTryCore`, but returns the suggestion array (as raw tactic syntaxes) instead of
emitting "Try this:" messages. Used by the `autoTry` linter, which formats and emits the
suggestions itself so they can be rendered as append-to-tactic-sequence edits. -/
def collectTryCoreSuggestions (config : Try.Config := {}) :
TacticM (Array (TSyntax `tactic)) :=
Tactic.focus do withMainContext do
let originalMaxHeartbeats ← getMaxHeartbeats
withUnlimitedHeartbeats do
let goal ← getMainGoal
let info ← Try.collect goal config
let stx ← mkTryEvalSuggestStx goal info
let suggs ← evalAndCollectSuggestions stx originalMaxHeartbeats config
return suggs.filterMap fun s => match s.suggestion with
| .tsyntax (kind := `tactic) tac => some ⟨tac⟩
| _ => none

@[builtin_tactic Lean.Parser.Tactic.tryTrace] def evalTryTrace : Tactic := fun stx => do
match stx with
| `(tactic| try?%$tk $config:optConfig) =>
Expand All @@ -1135,37 +1148,4 @@ private def elabTryCore (tk : Syntax) (config : Try.Config) (footer : MessageDat
evalAndSuggest tk tac originalMaxHeartbeats config
| _ => throwUnsupportedSyntax

open Term in
/-- When the `by` body is empty and `tactic.tryOnEmptyBy` is set, run `try?` for its
informational side effect (the "Try this" suggestions) and then delegate to the normal
`by` elaborator so the empty body still produces an unsolved-goals error. The implicit
mode must not change elaboration behavior beyond emitting messages.
Disabled when `errToSorry` is false (nested in a combinator like `first`),
or when `try?` infrastructure is not yet available (e.g. while building the prelude).

We register a *second* `builtin_term_elab` for `byTactic` (rather than folding the
gate-and-dispatch into `elabByTactic` directly) because `Lean.Elab.Tactic.Try` already
imports `Lean.Elab.BuiltinTerm`, so the `try?` infrastructure can't be referenced
from `BuiltinTerm.lean` without breaking the dependency direction. The gate in
`elabByTactic` skips this elaborator (via `throwUnsupportedSyntax`) when the `try?`
path doesn't apply. This could be cleaned up later, e.g. via a registered handler ref
in `BuiltinTerm.lean` populated by `Try.lean`. -/
@[builtin_term_elab byTactic] def elabEmptyByAsTry : TermElab := fun stx expectedType? => do
unless (← shouldElabEmptyByAsTry stx) do
throwUnsupportedSyntax
let some expectedType := expectedType? | do tryPostpone; throwUnsupportedSyntax
-- Run the same body the normal `by` elaborator would.
let mvar ← elabByTacticCore stx expectedType?
let cancelTk ← IO.CancelToken.new
let footer := m!"\n\n(Disable this with `set_option tactic.tryOnEmptyBy false`.)"
let act ← Term.wrapAsyncAsSnapshot (cancelTk? := cancelTk) fun (_ : Unit) => do
let scratch ← mkFreshExprMVar expectedType MetavarKind.syntheticOpaque
try
discard <| Tactic.run scratch.mvarId! <|
withRef stx do elabTryCore stx[0] { wrapWithBy := true } (footer := footer)
catch _ => pure ()
let t ← BaseIO.asTask (act ())
Core.logSnapshotTask { stx? := none, reportingRange := .skip, task := t, cancelTk? := cancelTk }
return mvar

end Lean.Elab.Tactic.Try
Loading
Loading