feat: empty by runs try? to suggest a proof (#13430)
This PR makes an empty `by` block run `try?` in the background and surface its suggestions, while still producing the usual unsolved-goals diagnostic. The implicit `try?` is informational only — it does not change elaboration behavior beyond emitting messages. Behaviour is controlled by a new option `tactic.tryOnEmptyBy`, disabled by default for now; set it to `true` to opt in. The default may flip in a future release. Behaviour summary, when the option is enabled: * The empty `by` reports unsolved goals immediately, before the (possibly slow) `try?` has finished. * The `try?` work is spawned as an asynchronous snapshot task (`Term.wrapAsyncAsSnapshot` + `Core.logSnapshotTask`), so subsequent elaboration is not blocked and the suggestions arrive when ready. * `try?` is gated on its parser infrastructure being available, so working on the prelude (before `Init.Try` is imported) keeps the regular empty-`by` behaviour. * No effect when the empty `by` appears inside a backtracking combinator (e.g. `first | exact (by) | …`) or when `try?` finds no applicable suggestion. Implementation notes: * `elabEmptyByAsTry` (in `Lean.Elab.Tactic.Try`) is registered as a second `@[builtin_term_elab byTactic]`, alongside the existing `elabByTactic` in `Lean.Elab.BuiltinTerm`. The gate `shouldElabEmptyByAsTry` is checked in both elaborators so the empty-`by` path takes the `try?` route while non-empty `by` follows the regular path. The body shared between them is factored as `elabByTacticCore`. The two-elaborator setup avoids a circular module dependency between `BuiltinTerm.lean` and `Tactic/Try.lean`; an inline comment in `Try.lean` explains this. * A latent bug from #13229 is fixed along the way: `evalSepTactics` returned at the very top for an empty tactic sequence without resolving the `tacSnap` promise that `MutualDef.mkTacTask` sets up for `:= by …` bodies. The dangling promise was harmless in typical use because the cmd's cancellation token would fire shortly after elaboration and drop it, but with a slow async snapshot task in the same command (as the implicit `try?` here) the language-server info-tree walk would block on it and the editor's Messages view would only update once the task finished. Resolved at the early-return in `evalSepTactics`. * The test infrastructure in `Lean.Server.Test.Cancel` gains a label-keyed `testTasksRef` registry plus `mkTestTask` / `wait_for_test_task`. The pre-existing `block_until_cancelled` is reimplemented on top of `mkTestTask` and the redundant `blockUntilCancelledOnce` ref is removed. Tests: * `tests/elab/tryOnEmptyBy.lean`, `tests/elab/try_prelude.lean` — feature behaviour and prelude gating. * `tests/server_interactive/cancellation_empty_by.lean` — verifies that on document re-elaboration `cancelRec` reaches the empty-`by` snapshot's cancel token registered with `Core.logSnapshotTask`. A `[try_suggestion]` generator wires the outer cancel token's `onSet` to resolve a `mkTestTask "T_outer"` promise, and the candidate `wait_for_test_task "T_outer"` waits on it. If `cancelTk? := none` is passed to `Core.logSnapshotTask`, `cancelRec` cannot reach the token, the wait blocks, and the runner times out. If `cancelTk? := none` is also passed to `wrapAsyncAsSnapshot`, no `onSet` resolver is registered, the promise drops without resolution, and `wait_for_test_task` surfaces a `"task dropped"` diagnostic on stderr. * `tests/server_interactive/cancellation_try_plain.lean` — verifies cancellation of plain `try?` (no `=>`) when its `[try_suggestion]` candidate runs synchronously inside `expandUserTactic`, by chaining through `wait_for_cancel_once_async`'s shared promise. Breaking `SnapshotTask.cancelRec` to skip walking children causes a runner timeout. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d98f40cda2
commit
2229b077d6
12 changed files with 382 additions and 68 deletions
|
|
@ -157,7 +157,37 @@ private def getMVarFromUserName (ident : Syntax) : MetaM Expr := do
|
|||
elabTerm b expectedType?
|
||||
| _ => throwUnsupportedSyntax
|
||||
|
||||
@[builtin_term_elab byTactic] def elabByTactic : TermElab := fun stx expectedType? => do
|
||||
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 []]]]` -/
|
||||
def isEmptyByBlock (stx : Syntax) : Bool :=
|
||||
stx.getNumArgs == 2 &&
|
||||
stx[1].getNumArgs >= 1 &&
|
||||
stx[1][0].isOfKind ``Lean.Parser.Tactic.tacticSeq1Indented &&
|
||||
stx[1][0].getNumArgs >= 1 &&
|
||||
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
|
||||
match expectedType? with
|
||||
| some expectedType =>
|
||||
-- `by` switches from an exported to a private context, so we must disallow unassigned
|
||||
|
|
@ -168,6 +198,13 @@ private def getMVarFromUserName (ident : Syntax) : MetaM Expr := 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?)
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@ where
|
|||
-- `stx[0]` is the next tactic step, if any
|
||||
goEven stx := do
|
||||
if stx.getNumArgs == 0 then
|
||||
-- No more tactic steps, but if the parent still passed us a tactic snapshot
|
||||
-- bundle (e.g. for an empty `by` body) we must resolve its promise so that
|
||||
-- consumers walking the snapshot tree (like the language-server info-tree
|
||||
-- lookup) don't block waiting on a promise nothing else will resolve.
|
||||
if let some snap := (← readThe Term.Context).tacSnap? then
|
||||
snap.new.resolve default
|
||||
return
|
||||
let tac := stx[0]
|
||||
/-
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ public import Lean.Elab.Tactic.LibrarySearch
|
|||
public import Lean.Elab.Tactic.Grind.Main
|
||||
public import Lean.Elab.Parallel
|
||||
public meta import Lean.Elab.Command
|
||||
import Lean.Elab.BuiltinTerm
|
||||
import Init.Omega
|
||||
public section
|
||||
namespace Lean.Elab.Tactic
|
||||
|
|
@ -1009,7 +1010,8 @@ private def wrapSuggestionWithBy (sugg : Tactic.TryThis.Suggestion) : TacticM Ta
|
|||
| _ => return sugg
|
||||
|
||||
/-- 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) : TacticM Unit := do
|
||||
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
|
||||
|
|
@ -1023,23 +1025,30 @@ private def evalAndSuggestWithBy (tk : Syntax) (tac : TSyntax `tactic) (original
|
|||
-- Wrap each suggestion with `by `
|
||||
let termSuggestions ← suggestions.mapM wrapSuggestionWithBy
|
||||
if termSuggestions.size == 1 then
|
||||
Tactic.TryThis.addSuggestion tk termSuggestions[0]! (origSpan? := (← getRef))
|
||||
Tactic.TryThis.addSuggestion tk termSuggestions[0]! (origSpan? := (← getRef)) (footer := footer)
|
||||
else
|
||||
Tactic.TryThis.addSuggestions tk termSuggestions (origSpan? := (← getRef))
|
||||
Tactic.TryThis.addSuggestions tk termSuggestions (origSpan? := (← getRef)) (footer := footer)
|
||||
|
||||
@[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
|
||||
/-- 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) :
|
||||
TacticM Unit :=
|
||||
Tactic.focus do withMainContext do
|
||||
let originalMaxHeartbeats ← getMaxHeartbeats
|
||||
withUnlimitedHeartbeats do
|
||||
let goal ← getMainGoal
|
||||
let info ← Try.collect goal config
|
||||
let stx ← mkTryEvalSuggestStx goal info
|
||||
if config.wrapWithBy then
|
||||
evalAndSuggestWithBy tk stx originalMaxHeartbeats config
|
||||
evalAndSuggestWithBy tk stx originalMaxHeartbeats config (footer := footer)
|
||||
else
|
||||
evalAndSuggest tk stx originalMaxHeartbeats config
|
||||
|
||||
@[builtin_tactic Lean.Parser.Tactic.tryTrace] def evalTryTrace : Tactic := fun stx => do
|
||||
match stx with
|
||||
| `(tactic| try?%$tk $config:optConfig) =>
|
||||
elabTryCore tk (← elabTryConfig config)
|
||||
| _ => throwUnsupportedSyntax
|
||||
|
||||
@[builtin_tactic Lean.Parser.Tactic.tryTraceWith] def evalTryTraceWith : Tactic := fun stx => do
|
||||
|
|
@ -1055,4 +1064,37 @@ private def evalAndSuggestWithBy (tk : Syntax) (tac : TSyntax `tactic) (original
|
|||
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
|
||||
|
|
|
|||
|
|
@ -91,14 +91,15 @@ The parameters are:
|
|||
-/
|
||||
def addSuggestion (ref : Syntax) (s : Suggestion) (origSpan? : Option Syntax := none)
|
||||
(header : String := "Try this:") (codeActionPrefix? : Option String := none)
|
||||
(diffGranularity : Hint.DiffGranularity := .none) : CoreM Unit := do
|
||||
(diffGranularity : Hint.DiffGranularity := .none)
|
||||
(footer : MessageData := MessageData.nil) : CoreM Unit := do
|
||||
let hintSuggestion := {
|
||||
span? := origSpan?
|
||||
diffGranularity
|
||||
toTryThisSuggestion := s
|
||||
}
|
||||
let suggs ← Hint.mkSuggestionsMessage #[hintSuggestion] ref codeActionPrefix? (forceList := false)
|
||||
logInfoAt ref m!"{header}{suggs}"
|
||||
logInfoAt ref m!"{header}{suggs}{footer}"
|
||||
|
||||
set_option linter.unusedVariables false in
|
||||
/-- Add a list of "try this" suggestions as a single "try these" suggestion. This has two effects:
|
||||
|
|
@ -134,7 +135,8 @@ def addSuggestions (ref : Syntax) (suggestions : Array Suggestion)
|
|||
(origSpan? : Option Syntax := none) (header : String := "Try these:")
|
||||
(style? : Option SuggestionStyle := none)
|
||||
(codeActionPrefix? : Option String := none)
|
||||
(diffGranularity : Hint.DiffGranularity := .none) : CoreM Unit := do
|
||||
(diffGranularity : Hint.DiffGranularity := .none)
|
||||
(footer : MessageData := MessageData.nil) : CoreM Unit := do
|
||||
if suggestions.isEmpty then throwErrorAt ref "No suggestions available"
|
||||
let hintSuggestions := suggestions.map fun s => {
|
||||
span? := origSpan?
|
||||
|
|
@ -142,7 +144,7 @@ def addSuggestions (ref : Syntax) (suggestions : Array Suggestion)
|
|||
toTryThisSuggestion := s
|
||||
}
|
||||
let suggs ← Hint.mkSuggestionsMessage hintSuggestions ref codeActionPrefix? (forceList := true)
|
||||
logInfoAt ref m!"{header}{suggs}"
|
||||
logInfoAt ref m!"{header}{suggs}{footer}"
|
||||
|
||||
/-! # Tactic-specific widget hooks -/
|
||||
/--
|
||||
|
|
|
|||
|
|
@ -192,39 +192,6 @@ elab_rules : tactic
|
|||
dbg_trace "blocked!"
|
||||
log "blocked"
|
||||
|
||||
/-! ## Helpers for end-to-end testing of cancellation propagation -/
|
||||
|
||||
meta initialize blockUntilCancelledOnce : IO.Ref (Std.HashMap String (Task Unit)) ← IO.mkRef {}
|
||||
|
||||
/--
|
||||
Tactic for testing cancellation propagation. On the first invocation for a given `<label>`,
|
||||
prints `<label>: blocked` to stderr and loops on `Core.checkInterrupted` until the tactic's
|
||||
cancel token fires (at which point the loop throws and `finally` resolves the shared promise).
|
||||
Subsequent invocations (e.g. on re-elaboration) wait on that promise: they return as soon as
|
||||
the first invocation has actually exited the loop, and hang otherwise. So if cancellation
|
||||
propagates correctly, the test completes; if propagation is broken, the second invocation's
|
||||
`IO.wait` blocks forever and the test hangs (timeout = failure).
|
||||
-/
|
||||
scoped syntax "block_until_cancelled" str : tactic
|
||||
elab_rules : tactic
|
||||
| `(tactic| block_until_cancelled $label) => do
|
||||
let lbl := label.getString
|
||||
let prom ← IO.Promise.new
|
||||
let prior ← blockUntilCancelledOnce.modifyGet fun m =>
|
||||
match m[lbl]? with
|
||||
| some t => (some t, m)
|
||||
| none => (none, m.insert lbl prom.result!)
|
||||
if let some t := prior then
|
||||
IO.wait t
|
||||
return
|
||||
IO.eprintln s!"{lbl}: blocked"
|
||||
try
|
||||
while true do
|
||||
Core.checkInterrupted
|
||||
IO.sleep 10
|
||||
finally
|
||||
prom.resolve ()
|
||||
|
||||
meta initialize cmdOnceRef : IO.Ref (Option (Task Unit)) ← IO.mkRef none
|
||||
|
||||
/--
|
||||
|
|
@ -252,3 +219,61 @@ elab_rules : command
|
|||
let t ← BaseIO.asTask (act ())
|
||||
(Core.logSnapshotTask { stx? := none, task := t, cancelTk? := cancelTk })
|
||||
logInfo "blocked"
|
||||
|
||||
/-- Registry of label-keyed `Task (Option Unit)` values for use by `mkTestTask` and
|
||||
`wait_for_test_task`. The stored task is `prom.result?` of the promise returned by
|
||||
`mkTestTask`; the registry itself does not keep that promise alive, so if no other
|
||||
reference exists, the promise drops and the task fires `none`. -/
|
||||
meta initialize testTasksRef : IO.Ref (Std.HashMap String (Task (Option Unit))) ← IO.mkRef {}
|
||||
|
||||
/-- Register a fresh test task under `label`, returning the underlying `IO.Promise`.
|
||||
Returns `none` if a task is already registered under `label`. The caller is responsible
|
||||
for keeping the returned promise alive and arranging its resolution -- typically by
|
||||
capturing it in a `cancelTk.onSet` closure that calls `prom.resolve`. -/
|
||||
meta def mkTestTask (label : String) : BaseIO (Option (IO.Promise Unit)) := do
|
||||
let prom ← IO.Promise.new
|
||||
testTasksRef.modifyGet fun m =>
|
||||
if m.contains label then (none, m) else (some prom, m.insert label prom.result?)
|
||||
|
||||
/-- Block until the test task named `label` fires. Prints a diagnostic to stderr if
|
||||
the underlying promise was dropped without resolution, or if no task is registered for
|
||||
`label`. The diagnostic uses stderr rather than `throwError` so that the failure is
|
||||
visible even when this tactic is evaluated inside `try?` (or any other combinator that
|
||||
swallows tactic errors). -/
|
||||
scoped syntax "wait_for_test_task " str : tactic
|
||||
elab_rules : tactic
|
||||
| `(tactic| wait_for_test_task $label) => do
|
||||
let label := label.getString
|
||||
match (← testTasksRef.get).get? label with
|
||||
| none =>
|
||||
IO.eprintln s!"wait_for_test_task: no task registered for {label}"
|
||||
| some t =>
|
||||
match (← IO.wait t) with
|
||||
| some _ => return
|
||||
| none => IO.eprintln s!"wait_for_test_task: task {label} dropped without resolution"
|
||||
|
||||
/--
|
||||
Tactic for testing cancellation propagation. On the first invocation for a given `<label>`,
|
||||
prints `<label>: blocked` to stderr and loops on `Core.checkInterrupted` until the tactic's
|
||||
cancel token fires (at which point the loop throws and `finally` resolves the shared task).
|
||||
Subsequent invocations (e.g. on re-elaboration) wait on that task: they return as soon as
|
||||
the first invocation has actually exited the loop, and hang otherwise. So if cancellation
|
||||
propagates correctly, the test completes; if propagation is broken, the second invocation's
|
||||
wait blocks forever and the test hangs (timeout = failure).
|
||||
-/
|
||||
scoped syntax "block_until_cancelled" str : tactic
|
||||
elab_rules : tactic
|
||||
| `(tactic| block_until_cancelled $label) => do
|
||||
let lbl := label.getString
|
||||
match (← mkTestTask lbl) with
|
||||
| none =>
|
||||
let some t := (← testTasksRef.get).get? lbl | unreachable!
|
||||
discard <| IO.wait t
|
||||
| some prom =>
|
||||
IO.eprintln s!"{lbl}: blocked"
|
||||
try
|
||||
while true do
|
||||
Core.checkInterrupted
|
||||
IO.sleep 10
|
||||
finally
|
||||
prom.resolve ()
|
||||
|
|
|
|||
|
|
@ -3,27 +3,28 @@
|
|||
-- it is fine to simply remove the `#guard_msgs` and expected output.
|
||||
|
||||
/--
|
||||
info: • [Command] @ ⟨79, 0⟩-⟨79, 40⟩ @ Lean.Elab.Command.elabDeclaration
|
||||
• [Term] Nat : Type @ ⟨79, 15⟩-⟨79, 18⟩ @ Lean.Elab.Term.elabIdent
|
||||
• [Completion-Id] Nat : some Sort.{?_uniq.1} @ ⟨79, 15⟩-⟨79, 18⟩
|
||||
• [Term] Nat : Type @ ⟨79, 15⟩-⟨79, 18⟩
|
||||
• [Term] n (isBinder := true) : Nat @ ⟨79, 11⟩-⟨79, 12⟩
|
||||
• [Term] 0 ≤ n : Prop @ ⟨79, 22⟩-⟨79, 27⟩ @ «_aux_Init_Notation___macroRules_term_≤__2»
|
||||
info: • [Command] @ ⟨80, 0⟩-⟨80, 40⟩ @ Lean.Elab.Command.elabDeclaration
|
||||
• [Term] Nat : Type @ ⟨80, 15⟩-⟨80, 18⟩ @ Lean.Elab.Term.elabIdent
|
||||
• [Completion-Id] Nat : some Sort.{?_uniq.1} @ ⟨80, 15⟩-⟨80, 18⟩
|
||||
• [Term] Nat : Type @ ⟨80, 15⟩-⟨80, 18⟩
|
||||
• [Term] n (isBinder := true) : Nat @ ⟨80, 11⟩-⟨80, 12⟩
|
||||
• [Term] 0 ≤ n : Prop @ ⟨80, 22⟩-⟨80, 27⟩ @ «_aux_Init_Notation___macroRules_term_≤__2»
|
||||
• [MacroExpansion]
|
||||
0 ≤ n
|
||||
===>
|
||||
binrel% LE.le✝ 0 n
|
||||
• [Term] 0 ≤ n : Prop @ ⟨79, 22⟩†-⟨79, 27⟩† @ Lean.Elab.Term.Op.elabBinRel
|
||||
• [Term] 0 ≤ n : Prop @ ⟨79, 22⟩†-⟨79, 27⟩†
|
||||
• [Completion-Id] LE.le✝ : none @ ⟨79, 22⟩†-⟨79, 27⟩†
|
||||
• [Term] 0 : Nat @ ⟨79, 22⟩-⟨79, 23⟩ @ Lean.Elab.Term.elabNumLit
|
||||
• [Term] n : Nat @ ⟨79, 26⟩-⟨79, 27⟩ @ Lean.Elab.Term.elabIdent
|
||||
• [Completion-Id] n : none @ ⟨79, 26⟩-⟨79, 27⟩
|
||||
• [Term] n : Nat @ ⟨79, 26⟩-⟨79, 27⟩
|
||||
• [Term] 0 ≤ n : Prop @ ⟨80, 22⟩†-⟨80, 27⟩† @ Lean.Elab.Term.Op.elabBinRel
|
||||
• [Term] 0 ≤ n : Prop @ ⟨80, 22⟩†-⟨80, 27⟩†
|
||||
• [Completion-Id] LE.le✝ : none @ ⟨80, 22⟩†-⟨80, 27⟩†
|
||||
• [Term] 0 : Nat @ ⟨80, 22⟩-⟨80, 23⟩ @ Lean.Elab.Term.elabNumLit
|
||||
• [Term] n : Nat @ ⟨80, 26⟩-⟨80, 27⟩ @ Lean.Elab.Term.elabIdent
|
||||
• [Completion-Id] n : none @ ⟨80, 26⟩-⟨80, 27⟩
|
||||
• [Term] n : Nat @ ⟨80, 26⟩-⟨80, 27⟩
|
||||
• [CustomInfo(Lean.Elab.Term.AsyncBodyInfo)]
|
||||
• [Term] n (isBinder := true) : Nat @ ⟨79, 11⟩-⟨79, 12⟩
|
||||
• [Term] n (isBinder := true) : Nat @ ⟨80, 11⟩-⟨80, 12⟩
|
||||
• [CustomInfo(Lean.Elab.Term.BodyInfo)]
|
||||
• [Tactic] @ ⟨79, 31⟩-⟨79, 40⟩
|
||||
• [PartialTerm] @ ⟨80, 31⟩-⟨80, 40⟩ @ Lean.Elab.Tactic.Try.elabEmptyByAsTry
|
||||
• [Tactic] @ ⟨80, 31⟩-⟨80, 40⟩
|
||||
(Term.byTactic
|
||||
"by"
|
||||
(Tactic.tacticSeq (Tactic.tacticSeq1Indented [(Tactic.exact? "exact?" (Tactic.optConfig []) [])])))
|
||||
|
|
@ -31,31 +32,31 @@ info: • [Command] @ ⟨79, 0⟩-⟨79, 40⟩ @ Lean.Elab.Command.elabDeclarati
|
|||
n : Nat
|
||||
⊢ 0 ≤ n
|
||||
after no goals
|
||||
• [Tactic] @ ⟨79, 31⟩-⟨79, 33⟩
|
||||
• [Tactic] @ ⟨80, 31⟩-⟨80, 33⟩
|
||||
"by"
|
||||
before ⏎
|
||||
n : Nat
|
||||
⊢ 0 ≤ n
|
||||
after no goals
|
||||
• [Tactic] @ ⟨79, 34⟩-⟨79, 40⟩ @ Lean.Elab.Tactic.evalTacticSeq
|
||||
• [Tactic] @ ⟨80, 34⟩-⟨80, 40⟩ @ Lean.Elab.Tactic.evalTacticSeq
|
||||
(Tactic.tacticSeq (Tactic.tacticSeq1Indented [(Tactic.exact? "exact?" (Tactic.optConfig []) [])]))
|
||||
before ⏎
|
||||
n : Nat
|
||||
⊢ 0 ≤ n
|
||||
after no goals
|
||||
• [Tactic] @ ⟨79, 34⟩-⟨79, 40⟩ @ Lean.Elab.Tactic.evalTacticSeq1Indented
|
||||
• [Tactic] @ ⟨80, 34⟩-⟨80, 40⟩ @ Lean.Elab.Tactic.evalTacticSeq1Indented
|
||||
(Tactic.tacticSeq1Indented [(Tactic.exact? "exact?" (Tactic.optConfig []) [])])
|
||||
before ⏎
|
||||
n : Nat
|
||||
⊢ 0 ≤ n
|
||||
after no goals
|
||||
• [Tactic] @ ⟨79, 34⟩-⟨79, 40⟩ @ Lean.Elab.LibrarySearch.evalExact
|
||||
• [Tactic] @ ⟨80, 34⟩-⟨80, 40⟩ @ Lean.Elab.LibrarySearch.evalExact
|
||||
(Tactic.exact? "exact?" (Tactic.optConfig []) [])
|
||||
before ⏎
|
||||
n : Nat
|
||||
⊢ 0 ≤ n
|
||||
after no goals
|
||||
• [Tactic] @ ⟨79, 34⟩†-⟨79, 40⟩† @ Lean.Elab.Tactic.evalExact
|
||||
• [Tactic] @ ⟨80, 34⟩†-⟨80, 40⟩† @ Lean.Elab.Tactic.evalExact
|
||||
(Tactic.exact "exact" (Term.app `Nat.zero_le [`n]))
|
||||
before ⏎
|
||||
n : Nat
|
||||
|
|
@ -68,8 +69,8 @@ info: • [Command] @ ⟨79, 0⟩-⟨79, 40⟩ @ Lean.Elab.Command.elabDeclarati
|
|||
• [Completion-Id] n : some Nat @ ⟨1, 5⟩†-⟨1, 5⟩†
|
||||
• [Term] n : Nat @ ⟨1, 5⟩†-⟨1, 5⟩†
|
||||
• [CustomInfo(Lean.Meta.Tactic.TryThis.TryThisInfo)]
|
||||
• [Term] t (isBinder := true) : ∀ (n : Nat), 0 ≤ n @ ⟨79, 8⟩-⟨79, 9⟩
|
||||
• [Term] t (isBinder := true) : ∀ (n : Nat), 0 ≤ n @ ⟨79, 8⟩-⟨79, 9⟩
|
||||
• [Term] t (isBinder := true) : ∀ (n : Nat), 0 ≤ n @ ⟨80, 8⟩-⟨80, 9⟩
|
||||
• [Term] t (isBinder := true) : ∀ (n : Nat), 0 ≤ n @ ⟨80, 8⟩-⟨80, 9⟩
|
||||
---
|
||||
info: Try this:
|
||||
[apply] exact Nat.zero_le n
|
||||
|
|
|
|||
68
tests/elab/tryOnEmptyBy.lean
Normal file
68
tests/elab/tryOnEmptyBy.lean
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/-
|
||||
Tests for `tactic.tryOnEmptyBy`: empty `by` blocks run `try?` and suggest proofs.
|
||||
-/
|
||||
|
||||
set_option tactic.tryOnEmptyBy true
|
||||
|
||||
-- Basic: empty by reports unsolved goals first (so the user sees it immediately
|
||||
-- even when `try?` is slow), then `try?` emits its suggestions as a single info
|
||||
-- message with the option-disabling hint at the end.
|
||||
/--
|
||||
error: unsolved goals
|
||||
⊢ True
|
||||
---
|
||||
info: Try these:
|
||||
[apply] by solve_by_elim
|
||||
[apply] by simp
|
||||
[apply] by simp only
|
||||
[apply] by grind
|
||||
[apply] by grind only
|
||||
[apply] by simp_all
|
||||
|
||||
(Disable this with `set_option tactic.tryOnEmptyBy false`.)
|
||||
-/
|
||||
#guard_msgs in
|
||||
example : True := by
|
||||
|
||||
-- Disabled: empty by gives unsolved goals
|
||||
/--
|
||||
error: unsolved goals
|
||||
⊢ True
|
||||
-/
|
||||
#guard_msgs in
|
||||
set_option tactic.tryOnEmptyBy false in
|
||||
example : True := by
|
||||
|
||||
-- Non-empty by is not affected
|
||||
example : True := by
|
||||
trivial
|
||||
|
||||
-- by { } (braces) is not affected
|
||||
example : True := by { trivial }
|
||||
|
||||
-- by { } (empty braces) does not trigger try?
|
||||
/--
|
||||
error: unsolved goals
|
||||
⊢ True
|
||||
-/
|
||||
#guard_msgs in
|
||||
example : True := by { }
|
||||
|
||||
-- Unprovable goal: try? finds no suggestions, so the implicit mode is fully silent
|
||||
-- (no "Try this", no error or warning from try? itself — only the unsolved-goals error).
|
||||
/--
|
||||
error: unsolved goals
|
||||
⊢ False
|
||||
-/
|
||||
#guard_msgs in
|
||||
example : False := by
|
||||
|
||||
-- Nested in a backtracking combinator (`errToSorry = false`): try? must stay silent.
|
||||
-- We only assert the absence of the try? info message; the unsolved-goals error is expected
|
||||
-- because `exact (by)` succeeds at term-elab time (the empty tactic block fails later).
|
||||
/--
|
||||
error: unsolved goals
|
||||
⊢ True
|
||||
-/
|
||||
#guard_msgs in
|
||||
example : True := by first | exact (by) | trivial
|
||||
15
tests/elab/try_prelude.lean
Normal file
15
tests/elab/try_prelude.lean
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
prelude
|
||||
import Init.Notation
|
||||
/-!
|
||||
Tests that while working on the prelude, try?-on-by does not run when not all infrastructure is
|
||||
availbe.
|
||||
-/
|
||||
|
||||
set_option tactic.tryOnEmptyBy true
|
||||
|
||||
/--
|
||||
error: unsolved goals
|
||||
⊢ True
|
||||
-/
|
||||
#guard_msgs in
|
||||
theorem test : True := by
|
||||
68
tests/server_interactive/cancellation_empty_by.lean
Normal file
68
tests/server_interactive/cancellation_empty_by.lean
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import Lean.Server.Test.Cancel
|
||||
import Lean.Elab.Tactic.Try
|
||||
open Lean Lean.Meta Lean.Elab Lean.Elab.Term Lean.Elab.Tactic
|
||||
open Lean.Server.Test.Cancel
|
||||
|
||||
/-!
|
||||
Test cancellation propagation through `elabEmptyByAsTry`. With
|
||||
`tactic.tryOnEmptyBy true` an empty `by` (the proof `:= by` with nothing
|
||||
following) spawns a `wrapAsyncAsSnapshot` task that runs `try?`. The
|
||||
outer snapshot's cancel token (`T_outer`) is registered with
|
||||
`Core.logSnapshotTask`; on re-elaboration `cancelRec` walks the
|
||||
snapshot tree and sets it.
|
||||
|
||||
The `[try_suggestion]` generator runs in MetaM inside the empty-`by`
|
||||
snapshot task, so `(← read).cancelTk?` is `T_outer`. It registers a
|
||||
test task under label `"T_outer"` (via `mkTestTask`) on the first
|
||||
invocation and arranges `T_outer.onSet` to resolve it; the returned
|
||||
candidate `wait_for_test_task "T_outer"` blocks until the task fires.
|
||||
|
||||
`t1`'s `wait_for_cancel_once_async` is used purely to publish the
|
||||
`"blocked"` diagnostic the runner waits for. On re-elaboration
|
||||
`cancelRec` walks both `t1`'s tree and the empty-`by` example's
|
||||
tree; firing `T_outer` resolves the shared task and the snapshot
|
||||
task body completes. The second elaboration's invocation observes
|
||||
the already-resolved task and returns immediately.
|
||||
|
||||
If `elabEmptyByAsTry` passes `cancelTk? := none` to
|
||||
`Core.logSnapshotTask`, `cancelRec` cannot reach `T_outer`. The
|
||||
snapshot task body's wait blocks until `T_outer` is dropped; since
|
||||
the body holds `T_outer` alive (via `wrapAsync`'s ctx) until it
|
||||
returns, `T_outer` does not drop, the wait does not return, and the
|
||||
runner times out. If `elabEmptyByAsTry` additionally fails to plumb
|
||||
`cancelTk` into `wrapAsyncAsSnapshot`, no `onSet` resolver is
|
||||
registered, the promise drops, and `wait_for_test_task` surfaces
|
||||
the explicit "task dropped" diagnostic on stderr.
|
||||
-/
|
||||
|
||||
namespace TestEmptyBy
|
||||
|
||||
opaque UnsolvableProp : Prop
|
||||
|
||||
@[try_suggestion]
|
||||
def tracerSuggestion (_goal : MVarId) (_info : Try.Info) :
|
||||
MetaM (Array (TSyntax `tactic)) := do
|
||||
-- Register the test task on the first call only; later calls reuse the
|
||||
-- existing entry and so observe the same (eventually resolved) task.
|
||||
if let some prom ← mkTestTask "T_outer" then
|
||||
if let some cancelTk := (← readThe Core.Context).cancelTk? then
|
||||
cancelTk.onSet (do prom.resolve ())
|
||||
return #[← `(tactic| wait_for_test_task "T_outer")]
|
||||
|
||||
end TestEmptyBy
|
||||
|
||||
set_option tactic.tryOnEmptyBy true
|
||||
|
||||
example : True := by
|
||||
trivial
|
||||
--^ waitFor: blocked
|
||||
--^ insert: "; skip"
|
||||
--^ sync
|
||||
|
||||
theorem t1 : True := by
|
||||
wait_for_cancel_once_async
|
||||
trivial
|
||||
|
||||
example : TestEmptyBy.UnsolvableProp := by
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
blocked!
|
||||
cancelled!
|
||||
46
tests/server_interactive/cancellation_try_plain.lean
Normal file
46
tests/server_interactive/cancellation_try_plain.lean
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import Lean.Server.Test.Cancel
|
||||
import Lean.Elab.Tactic.Try
|
||||
open Lean Lean.Meta Lean.Elab Lean.Elab.Term Lean.Elab.Tactic
|
||||
open Lean.Server.Test.Cancel
|
||||
|
||||
/-!
|
||||
Test cancellation propagation through plain `try?` (no `=>`, no
|
||||
`wrapAsyncAsSnapshot` wrapping). Plain `try?` calls
|
||||
`mkTryEvalSuggestStx`, which calls `expandUserTactic` for each
|
||||
`[try_suggestion]` candidate. `expandUserTactic` runs the candidate via
|
||||
`Tactic.run + evalTactic` on the *main* elaboration thread. Cancellation
|
||||
must reach the candidate's body promptly via `Core.checkInterrupted`
|
||||
embedded in `evalTactic` and inside the candidate.
|
||||
|
||||
We register a `[try_suggestion]` whose tactic is `wait_for_cancel_once_async`.
|
||||
The first invocation (in the outer theorem body) spawns a snapshot task
|
||||
that polls the cancel token; the second invocation (inside
|
||||
`expandUserTactic`) waits on the first invocation's promise. On
|
||||
re-elaboration the snapshot task observes its cancel token, resolves the
|
||||
shared promise, and the inner `IO.wait` returns. After re-elaboration the
|
||||
chain construction continues, the user attempt-all is built and run, and
|
||||
the further nested invocations wait on the already-resolved promise. If
|
||||
cancellation propagation is broken at any step, the test runner times out.
|
||||
-/
|
||||
|
||||
namespace TestTryPlain
|
||||
|
||||
opaque UnsolvableProp : Prop
|
||||
|
||||
@[try_suggestion]
|
||||
def waitSuggestion (_goal : MVarId) (_info : Try.Info) :
|
||||
MetaM (Array (TSyntax `tactic)) := do
|
||||
let stx ← `(tactic| wait_for_cancel_once_async)
|
||||
return #[stx]
|
||||
|
||||
end TestTryPlain
|
||||
|
||||
example : True := by
|
||||
trivial
|
||||
--^ waitFor: blocked
|
||||
--^ insert: "; skip"
|
||||
--^ sync
|
||||
|
||||
theorem t : TestTryPlain.UnsolvableProp := by
|
||||
wait_for_cancel_once_async
|
||||
try?
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
blocked!
|
||||
cancelled!
|
||||
Loading…
Add table
Reference in a new issue