Zulip Chat Archive
Stream: mathlib4
Topic: Tactic combinator showing all errors
Patrick Massot (Mar 13 2024 at 17:33):
If you do
import Mathlib.Tactic
example (x y : Nat): x = y := by
first | rfl | aesop
then the error message you get comes from aesop
. If you do
example (x y : Nat): x = y := by
first | aesop | rfl
then you get the error message coming from rfl
. Do we have a combinator that would show both error messages (with a clear indication that it is a list of error messages coming from trying various things, not a single error message)?
Jireh Loreaux (Mar 13 2024 at 19:07):
Could you just copy the first
combinator with logError
instead of throwError
?
Jireh Loreaux (Mar 13 2024 at 19:10):
Ah, nevermind, you need it to accumulate the errors but only throw the list of them if the last tactic doesn't succeed.
Kyle Miller (Mar 13 2024 at 19:29):
Here's a possibility:
section
open Lean Elab Tactic
variable {α : Type}
def orElse' (xref : Syntax) (x : TacticM α) (y : Unit → TacticM α) : TacticM α := do
try withoutRecover x catch e =>
try y () catch e' =>
if (← read).recover then
logErrorAt xref e.toMessageData
throw e'
partial def firstLoop (tacs : Array Syntax) (i : Nat) :=
if i == tacs.size - 1 then
evalTactic (tacs[i]!)[1]
else
let tac := (tacs[i]!)[1]
orElse' tac (evalTactic tac) (fun _ => firstLoop tacs (i+1))
elab "first' " withPosition((ppDedent(ppLine) colGe "| " tacticSeq)+) : tactic => do
let stx ← getRef
let tacs := stx[1].getArgs
if tacs.isEmpty then throwUnsupportedSyntax
firstLoop tacs 0
end
example (x y : Nat): x = y := by
first' | rfl | aesop
Kyle Miller (Mar 13 2024 at 19:30):
Kyle Miller (Mar 13 2024 at 19:30):
I could see first
working like this in general.
Kyle Miller (Mar 13 2024 at 19:30):
Same with <|>
Yaël Dillies (Mar 13 2024 at 19:34):
Yes please! I've been wanting this
Eric Wieser (Mar 13 2024 at 19:50):
Is there a reason that something like docs#List.anyM doesn't work in place of your firstLoop
?
Kyle Miller (Mar 13 2024 at 19:56):
I didn't think about it much — I copied the code from the implementation of first
Kyle Miller (Mar 13 2024 at 19:57):
Do you have any idea how to eliminate xref
from orElse'
? That would help with making it possible to be the main implementation of <|>
Kyle Miller (Mar 13 2024 at 19:57):
Without xref
, the error for x
becomes reported as an error for all of <|>
Jireh Loreaux (Mar 13 2024 at 20:43):
I didn't see a way around the xref
either, and my implementation was similar.
Joachim Breitner (Mar 13 2024 at 22:00):
What happens if this first
is hidden in some other tactic, like decreasing_tactic
? I assume the error messages will just all be shown, but without much context? In that case it might be nicer to be a bit verbose and say “Tactic rlf
failed with: … Tactic simp
failed with: …”? Not sure if it's a problem to pretty print the actual tactic here, I guess it could be too large?
Yaël Dillies (Mar 13 2024 at 22:01):
Surely the decreasing_tactic
should have its own error message anyway, right?
Joachim Breitner (Mar 13 2024 at 22:04):
Yeah, but it essentially says “none of the tactics solved it” also swallowing the error messages.
Although that's actually using macro_rules
not first
to try various tactics. But same problem: you don't get to see the error messages from all but one of them. (Not sure if you really should see all of them in this case.)
Eric Wieser (Mar 13 2024 at 22:54):
Kyle Miller said:
Do you have any idea how to eliminate
xref
fromorElse'
? That would help with making it possible to be the main implementation of<|>
Can't you use e.getRef
instead of xref
?
Thomas Murrills (Mar 13 2024 at 23:04):
There's also simply logException e
(which essentially just wraps logErrorAt e.getRef e.toMessageData
in the non-internal-error case)
Eric Wieser (Mar 13 2024 at 23:14):
I couldn't get firstM
to work without making a primed version:
import Lean
section
open Lean Elab Tactic
variable {α : Type}
instance betterOrElse : OrElse (TacticM α) where orElse x y := do
try withoutRecover x catch e =>
try y () catch e' =>
if (← read).recover then
logException e
throw e'
instance (priority := high) betterAlternative : Alternative TacticM where
orElse x y := (betterOrElse.orElse x y)
failure := failure
/-- Like `firstM`, but without using `f a <|> failure` on `[a]`. -/
@[specialize]
def List.firstM'.{u,v,w} {m : Type u → Type v} [Alternative m] {α : Type w} {β : Type u} (f : α → m β) : List α → m β
| [] => failure
| [a] => f a
| a::as => f a <|> firstM' f as
elab "first' " withPosition((ppDedent(ppLine) colGe "| " tacticSeq)+) : tactic => do
let stx ← getRef
let tacs := stx[1].getArgs
if tacs.isEmpty then throwUnsupportedSyntax
discard <| List.firstM' (fun stx => evalTactic stx[1]) tacs.toList
pure ()
end
example (x y : Nat): x = y := by
first' | rfl | rfl
Eric Wieser (Mar 13 2024 at 23:28):
Hmm, this doesn't work with more than two rfl
s
Thomas Murrills (Mar 14 2024 at 00:00):
On closer inspection, the original doesn't either, I think—the issue is that we throw e'
, which gets caught (when appearing inside y ()
) and reverts our logErrorAt
. This is unless we happen to have throw
n e'
to the top level, in which case we get both the first error and the last error.
Kyle Miller (Mar 14 2024 at 01:18):
Joachim Breitner said:
What happens if this
first
is hidden in some other tactic, likedecreasing_tactic
?
I think there needs to be some protocol for knowing when tactics are "interactive". There's already some of that protocol in the form of the recover
flag, but it doesn't seem to be capturing interactivity — it seems to be an orthogonal concept.
"Interactive" mode might include
- having tactics generate time-consuming diagnostic messages on failure and
- logging errors even if they're being caught (like in the
<|>
andfirst
modifications)
Then tactics that run tactic scripts could go into non-interactive mode to prevent logging incidental errors.
Patrick Massot (Mar 14 2024 at 01:38):
Thanks Kyle! I should have been more specific and say that I’m not actually using first
, I have a single user-facing tactic that tries several things. But at least I know there is nothing already existing, and I will probably be able to reuse some of your code. And I think that first'
could be useful anyway.
Kyle Miller (Mar 14 2024 at 01:42):
No problem — you happened to ask a question in the realm of something I had been thinking about anyway: what sort of protocol do we need or want for tactics in "interactive" mode?
first
is a good example of the phenomenon. Even though you wouldn't see first
in a completed proof like this, it would be helpful to see these messages to help you come up with your completed proof.
Damiano Testa (Mar 14 2024 at 02:18):
By the way, even though you do not see the errors, with the "unused tactic linter", you can do this:
example (x y : Nat): x = y := by
first | rfl | aesop | sorry -- <-- add a sorry, so you know that the goal will be closed
-- and the unused tactic linter reports:
/-
'rfl' tactic does nothing [linter.unusedTactic]
'aesop' tactic does nothing [linter.unusedTactic]
-/
Thomas Murrills (Mar 14 2024 at 02:42):
One of the things that could fix this particular issue would be a "compound error": if we could throw multiple errors at once in a single throw
, we wouldn't have to manage the state carefully.
Likewise, it would be nice if there were a variant of catch
which could catch (and bind) any generated messages—the gap between a control-flow-affecting Exception
and a logged error can sometimes feel uncomfortably large.
(Of course, an interactive mode would still be very useful in general—this isn't any argument against that. :) )
Thomas Murrills (Mar 14 2024 at 03:02):
Though, the arguments of .error
technically have enough structure for us to hack together "compound errors" anyway! So, just for fun, here's an extremely cursed thing you can do. :)
import Lean
section
open Lean Elab Tactic
variable {α : Type}
/-- Abuse `.compose` constructor to get a `MessageData` tree.
Propagate "All failures:" header to the top left; insert newlines between messages. -/
def composeMessages : MessageData → MessageData → MessageData
| .compose (.tagged `comp _) c₁, .compose (.tagged `comp _) c₂
| .compose (.tagged `comp _) c₁, c₂
| c₁, .compose (.tagged `comp _) c₂
| c₁, c₂ => .compose (.tagged `comp m!"All failures:\n") (.compose (.compose c₁ m!"\n\n") c₂)
protected def _root_.Lean.Exception.append : Exception → Exception → Exception
| .error ref msg, .error ref' msg' =>
.error
/- Use the source info from the rightmost node. This is a hack to get
a red squiggle on each individual tactic instead of under the whole span. -/
(Syntax.node (SourceInfo.fromRef ref') `compoundError #[ref, ref'])
(composeMessages msg msg')
| _, _ => panic! "internal errors not yet implemented"
/-- Log each error in a compound error individually, in order. -/
partial def logComposedException {m : Type → Type} [Monad m] [MonadLog m] [AddMessageContext m]
[MonadOptions m] [MonadLiftT IO m] : Exception → m Unit
| .error (Syntax.node _ `compoundError #[ref₁, ref₂])
(.compose (.tagged `comp _) (.compose (.compose msg₁ _) msg₂))
| .error (Syntax.node _ `compoundError #[ref₁, ref₂])
(.compose (.compose msg₁ _) msg₂) => do
logComposedException (.error ref₁ msg₁)
logComposedException (.error ref₂ msg₂)
| e => logException e
def orElse' (x : TacticM α) (y : Unit → TacticM α) : TacticM α := do
try withoutRecover x catch e =>
try y () catch e' =>
let mut e' := e'
if (← read).recover then
e' := e.append e'
logComposedException e'
throw e'
partial def firstLoop (tacs : Array Syntax) (i : Nat) :=
if i == tacs.size - 1 then
evalTactic (tacs[i]!)[1]
else
let tac := (tacs[i]!)[1]
orElse' (evalTactic tac) (fun _ => firstLoop tacs (i+1))
elab "first' " withPosition((ppDedent(ppLine) colGe "| " tacticSeq)+) : tactic => do
let stx ← getRef
let tacs := stx[1].getArgs
if tacs.isEmpty then throwUnsupportedSyntax
firstLoop tacs 0
end
example (x y : Nat) : x = y := by
first' | rfl | rfl | rfl
Thomas Murrills (Mar 14 2024 at 03:11):
The biggest "cheat" here is that we log all three errors, but the error we throw also renders as "All failures: ...", appears as a squiggle only on the last token, and lists all errors. For example, if we used first | simp | dsimp | simp
, the errors would look like:
Foo.lean:223:11
simp made no progress
Foo.lean:223:18
dsimp made no progress
Foo.lean:223:26
simp made no progress
Foo.lean:223:26
All failures:simp made no progress
dsimp made no progress
simp made no progress
Last updated: May 02 2025 at 03:31 UTC