Zulip Chat Archive

Stream: lean4

Topic: Tricky `(kernel) deep recursion detected` error


Alex Keizer (Jun 20 2025 at 12:18):

I've been trying to debug a proof which gives a (kernel) deep recursion detected error. I've included a self-contained example below, however it seems to be resisting further attempt at minimization. I've tried removing several seemingly unrelated complexities from the example, all of which then make the error disappear without a giving me a clear intuition as to why.

Does anyone have a suggestion for what could cause this behaviour?

/-! # Preliminary -/

/--
`PoisonOr` is a simple wrapper around `Option`
-/
structure PoisonOr (α : Type) where
  toOption : Option α

namespace PoisonOr

def value : α  PoisonOr α := (some ·⟩)

instance : Bind PoisonOr where
  bind := fun a f => match a with
    | none    => none
    | some a  => f a

theorem bind_assoc (x : PoisonOr α) (f : α  PoisonOr β) (g : β  PoisonOr γ) :
    x >>= f >>= g = x >>= fun x => f x >>= g := by
  rcases x with (_|_) <;> rfl

end PoisonOr

/-! # MWE -/

def BitVec.umulOverflow' {w : Nat} (x y : BitVec w) : Prop :=
  x.toNat * y.toNat  0
  -- -----^
  -- Making this `+` has no effect, the error remains.
  --
  -- However, replacing `x.toNat` with a constant (even if large), does make the
  --   error disappear
  --
  -- Also note that the rhs was originally `2 ^ w`, but I've replaced it with
  -- `0` to rule out `2 ^ w` being computed. This had no effect on the error.

instance : DecidableRel (@BitVec.umulOverflow' w) := by
  unfold BitVec.umulOverflow';
  -- If we `sorry` out this instance, the error disappears
  -- sorry
  infer_instance


/-
The following gives the error:

  (kernel) deep recursion detected

-/
theorem mwe (x' : BitVec 32) :
    (do
      let _z 
        (do
          let y'  PoisonOr.value 0#32
          -- ----- ^^^^^^^^^^^^^^
          -- Replacing the `PoisonOr` wrapper with `Option` (in the whole MWE)
          -- makes the error disappear
          --
          if x'.umulOverflow' (65537#_) then
          -- ----------------- ^^^^^^^
          -- Making this constant smaller, or replacing it with a variable,
          -- makes the error disappear
          --
            PoisonOr.value 0#32
          else
            PoisonOr.value 0#32)
      PoisonOr.value 0) = PoisonOr.value 0 := by
  -- Running `simp only [ite_self]` first makes the error disappear
  --   (but is not applicable in my actual scenario, as the branches of the real
  --    code aren't the same)
  -- Similarly, rewriting the statement to not have the if-then-else also
  -- makes the error disappear.

  rw [PoisonOr.bind_assoc]
  -- ^^ This rewrite is what seems to trigger the recursion, removing it
  --    makes the error disappear.
  sorry

Matthew Ballard (Jun 20 2025 at 12:23):

Can you run it through lean4lean?

Alex Keizer (Jun 20 2025 at 12:51):

How would I do that? I've built lean4lean locally, and am trying to get it to check this MWE using

 lake env $lean4lean --fresh MWE

But lean4lean just complains that it can't find oleans (uncaught exception: Could not find any oleans for: MWE).

Presumably there's no oleans to be found, given that lean fails to build with the aforementioned error?

Matthew Ballard (Jun 20 2025 at 12:57):

Maybe @Mario Carneiro can help? I can try to trouble shoot at an unspecified later time but am surely far less useful.

Alex Keizer (Jun 20 2025 at 13:00):

I also ran a lake build through samply, which gave a stack trace showing a very deep mutual recursion--presumably the one being complained about--involving lean::type_checker::{whnf, reduce_nat}:
image.png

Alex Keizer (Jun 20 2025 at 13:16):

Matthew Ballard said:

Maybe Mario Carneiro can help? I can try to trouble shoot at an unspecified later time but am surely far less useful.

Ah, I managed to get the MWE to build (thus producing oleans) by setting debug.skipKernelTC to suppress the kernel error. Although, lean4lean seems to be giving unrelated errors. That is, it seems to be complaining about my preliminary definitions already existing:

PANIC at _private.Lean.Environment.0.Lean.EnvExtension.getStateImpl Lean.Environment:1297:4: invalid environment extension has been accessed
backtrace:
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(lean_panic+0xf5) [0x7fe0605]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(lean_panic_fn+0x15) [0x7fe07e5]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l___private_Lean_Environment_0__Lean_EnvExtension_getStateUnsafe___rarg+0x16b) [0x14b473b]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l_Lean_ppConstNameWithInfos+0x4f) [0x25850ff]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l_Lean_MessageData_ofConstName___elambda__2+0xaf) [0x1a7c27f]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(lean_apply_2+0xa5f) [0x7fee7ef]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l_Lean_MessageData_formatAux+0x2af) [0x1a7e0af]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l_Lean_MessageData_formatAux+0x172d) [0x1a7f52d]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l_Lean_MessageData_formatAux+0xa97) [0x1a7e897]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l_Lean_MessageData_toString+0x1c) [0x1a82a6c]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l_throwKernelException___rarg+0x978) [0xfa9e78]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l_addDecl___lambda__3+0x36d) [0xfad63d]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l_replayConstant+0x225d) [0xfb573d]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l_Lean_RBNode_forIn_visit___at_replayConstants___spec__1+0x198) [0xfbc788]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l_replayConstants+0x19) [0xfb1ae9]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l_replayConstant+0x374) [0xfb3854]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l_Lean_RBNode_forIn_visit___at_replayConstants___spec__1+0x198) [0xfbc788]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l_Lean_RBNode_forIn_visit___at_replayConstants___spec__1+0xe8) [0xfbc6d8]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l_Lean_RBNode_forIn_visit___at_replayConstants___spec__1+0xe8) [0xfbc6d8]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l_Lean_RBNode_forIn_visit___at_replayConstants___spec__1+0xe8) [0xfbc6d8]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l_Lean_RBNode_forIn_visit___at_replayConstants___spec__1+0xe8) [0xfbc6d8]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l_replay+0x6eb) [0xfc6deb]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l_replayFromImports___lambda__2+0x248) [0xfcce38]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l_replayFromImports___lambda__4+0x15fa) [0xfcefea]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(lean_apply_1+0x11dc) [0x7fecd1c]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(l_EIO_toBaseIO___rarg+0xa) [0x7f0e52a]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(lean_apply_1+0x1219) [0x7fecd59]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean() [0x800a56e]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean(lean_apply_1+0x1219) [0x7fecd59]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean() [0x7fea546]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean() [0x7feb831]
/home/alex/Workspace/PhD/lean4lean/.lake/build/bin/lean4lean() [0x7fde325]
/nix/store/cg9s562sa33k78m63njfn1rw47dp9z0i-glibc-2.40-66/lib/libc.so.6(+0x97e63) [0x7f5ccd297e63]
/nix/store/cg9s562sa33k78m63njfn1rw47dp9z0i-glibc-2.40-66/lib/libc.so.6(+0x11bdbc) [0x7f5ccd31bdbc]
lean4lean found a problem in MWE:
(kernel) constant has already been declared '[Error pretty printing constant: (`Inhabited.default` for `IO.Error`). Falling back to raw printer.]
PoisonOr'

I figured trying out lean4lean with the --fresh flag could be more informative (as in lake env $lean4lean --fresh MWE), but this just complains about uncaught exception: (kernel) invalid form for primitive def Nat.gcd, even if I replace the entire module with a simple

def Foo : String := "foo"

I guess I must be doing something wrong, @Mario Carneiro help/guidance would be much appreciated!

Mario Carneiro (Jun 20 2025 at 13:43):

sorry, lean4lean was broken by one of the recent bumps and I haven't had time to fix it yet; this isn't your fault

Mario Carneiro (Jun 20 2025 at 13:43):

if you can, try using a previous version before the lean update

Alex Keizer (Jun 20 2025 at 13:46):

I figured it'd be something like that, I'll try an old version!

Alex Keizer (Jun 20 2025 at 13:59):

I've tried with commit c0273e3 of lean4lean, which is just before the latest lean bump, where lake env $lean4lean MWE gives no output (which presumably means lean4lean found no problems to report?), but trying --fresh still gives the same error about Nat.gcd.

I also tried 1574d0a, which is just before the lean version bump before that, on which neither lake env $lean4lean MWE nor lake env $lean4lean --fresh MWE give any output.

Mario Carneiro (Jun 20 2025 at 14:01):

I see, I wasn't aware of the Nat.gcd issue. Probably someone changed the definition?

Mario Carneiro (Jun 20 2025 at 14:04):

I think the list of primitives is also out of date

Mario Carneiro (Jun 20 2025 at 14:08):

I think the problem is that well founded definitions no longer reduce on constants, so the kernel override is adding defeqs that are not justified cc: @Joachim Breitner

Mario Carneiro (Jun 20 2025 at 14:09):

I think it needs to be implemented with fuel as Nat.div was

Mario Carneiro (Jun 20 2025 at 14:12):

@Alex Keizer if you want to ignore the issue you can replace line 81-83 of Primitive.lean with return true -- TODO

Alex Keizer (Jun 20 2025 at 14:44):

Mario Carneiro said:

I think the problem is that well founded definitions no longer reduce on constants, so the kernel override is adding defeqs that are not justified cc: Joachim Breitner

Do you mean this is an issue in lean4lean, or that this is the lean issue that causes my deep recursion error? I'm a bit lost.

if you want to ignore the issue you can replace line 81-83 of Primitive.lean with return true -- TODO

This is just talking about the lean4lean issue, right? I'm already satisfied that lean4lean seems to accept the MWE olean, confirming that this looks like a lean core issue

Mario Carneiro (Jun 20 2025 at 14:44):

this is unrelated to the deep recursion error, I think

Alex Keizer (Jun 20 2025 at 14:45):

I've opened an issue on the Github tracker for this problem as well: https://github.com/leanprover/lean4/issues/8898

Mario Carneiro (Jun 20 2025 at 14:45):

this is an issue in the way primitive declarations are being done in lean

Tobias Grosser (Jun 20 2025 at 15:53):

What does this mean?

Tobias Grosser (Jun 20 2025 at 15:53):

Any workaround we can apply?

Jovan Gerbscheid (Jun 20 2025 at 18:48):

The issues seems to be due to the constant 65537, which is being unfolded by the kernel. In general, working with big natural numbers brings the risk that the kernel will try to unfold them.

Rish Vaishnav (Jun 23 2025 at 07:01):

I've discovered that the issue is related to this commit. When I make the corresponding change to Lean4Lean to remove the e.hasFVar check, I get the same issue. The kernel tries to WHNF the second argument to Nat.ble 0 ((BitVec.toNat 32 x) * (BitVec.toNat 32 (BitVec.ofNat 32 65537))) so it can evaluate the Nat.ble application as a primitive op, which it wouldn't have done before because of the variable x. Was this change made because there was a particular case where we would want to evaluate primitive ops with fvars in their args? Or might this change have accidentally found its way into that commit?

Mario Carneiro (Jun 23 2025 at 07:02):

(FYI the unrelated bugs in lean4lean caused by the version bump have now been fixed, it should work on hello world files once more)

Mario Carneiro (Jun 23 2025 at 07:04):

that commit looks pretty sneaky, the second change seems entirely unrelated to the commit message

Mario Carneiro (Jun 23 2025 at 07:04):

it could be an accidental change?

Mario Carneiro (Jun 23 2025 at 07:05):

oh, the actual change is lean4#7386

Rish Vaishnav (Jun 23 2025 at 07:07):

Ah, so we need to also check that the variable isn't a let variable, perhaps? Rather than removing the check entirely

Mario Carneiro (Jun 23 2025 at 07:07):

well anything other than that particular check won't be O(1)

Rish Vaishnav (Jun 23 2025 at 07:12):

Oh, right, so to keep things O(1) we'll need to add a new entry to Expr.Data? Something like Expr.Data.hasLetVar?

Mario Carneiro (Jun 23 2025 at 07:13):

I don't think that can be computed in Expr.Data, since it depends on external information

Rish Vaishnav (Jun 23 2025 at 07:15):

Oh darn, yeah it depends on the context, tricky indeed...

Rish Vaishnav (Jun 23 2025 at 07:51):

Could this be a reasonable compromise?

def _root_.Lean.Expr.hasNonLetFVar (lctx : LocalContext) (e : Expr) : Bool := Id.run do
  let mut e := e
  for decl in lctx do
    if let .ldecl .. := decl then
      e := e.abstract #[decl.toExpr]
  return e.hasFVar

def reduceNat (e : Expr) : RecM (Option Expr) := do
  if e.hasNonLetFVar ( readThe Context).lctx then
    return none
  ...

The iteration for decl in lctx do is obviously not ideal, but we could perhaps reduce that cost by separately tracking a list of let-bound free variables in the typechecking context and only iterating over those instead.

Rish Vaishnav (Jun 23 2025 at 08:20):

^ if we do only iterate over let-bound fvars, this actually recovers O(1) in the common case (when there are no let-bound fvars in the context)

Mario Carneiro (Jun 23 2025 at 08:21):

I don't think it's practical, because this is being run on every subterm all the time - we don't even know yet whether it's a nat expression

Mario Carneiro (Jun 23 2025 at 08:22):

we may as well just step forward a bit and see if the head is Nat.add or whatever

Mario Carneiro (Jun 23 2025 at 08:26):

but maybe taking a step back, I'm not yet satisfied with the explanation of the issue. Why is this even triggering a big nat computation? It's just a bind reassociation which isn't unfolding anything

Joachim Breitner (Jun 23 2025 at 08:28):

Mario Carneiro schrieb:

but maybe taking a step back, I'm not yet satisfied with the explanation of the issue. Why is this even triggering a big nat computation? It's just a bind reassociation which isn't unfolding anything

It may be enough to be defeq checking, due to

/** \remark t_n, s_n are updated. */
lbool type_checker::lazy_delta_reduction(expr & t_n, expr & s_n) {
    while (true) {
        lbool r = is_def_eq_offset(t_n, s_n);
        if (r != l_undef) return r;

        if (!has_fvar(t_n) && !has_fvar(s_n)) {
            if (auto t_v = reduce_nat(t_n)) {
                return to_lbool(is_def_eq_core(*t_v, s_n));
            } else if (auto s_v = reduce_nat(s_n)) {
                return to_lbool(is_def_eq_core(t_n, *s_v));
            }

(related investigation at https://github.com/leanprover/lean4/issues/6152#issuecomment-2490917863)

Mario Carneiro (Jun 23 2025 at 08:31):

hm, needs more lazy

Gavin Zhao (Jul 30 2025 at 18:04):

Possibly similar problem: #lean4 > strange deep recursion error when using `rw` with `match`

Devon Tuma (Jul 30 2025 at 19:34):

I think they are essentially the same problem. I see the same reduce_nat involvement when debugging this. I would have thought that using Fin would be enough indirection to prevent this but maybe the expose in Fin.ofNat makes it still problematic in the same way? I'm trying to work around it by defining constants rather than using explicit numbers but it feels cumbersome.

Devon Tuma (Jul 30 2025 at 19:35):

The fact that that example relies on explicit match cases vs. a single default case probably comes down to some short circuiting that the compiler does when reducing a match?

Alex Keizer (Jul 31 2025 at 13:41):

So the way we worked around this issue is by introducing an opaque definition which is prop-eq to the identity function, but by virtue of being opaque blocks any reduction:

/-- Definition of `hide` and `hide_eq` -/
opaque hide_def : { hide : α  α // hide = id } := id, rfl

/--
`hide` is an opaque version of the identity function.
By making it opaque, we prevent the kernel from trying to reduce the argument.
-/
def hide : α  α := hide_def.1

/-- A proof that `hide` is the identity. Note that this is deliberately *not*
a def-eq, since we made `hide` opaque. -/
theorem hide_eq {a : α} : hide a = a := by
  rw [hide, hide_def.2]; rfl

And then we wrote a simproc which rewrites problematic constants to hide them behind this opaque defintion:

/-- Symmetric version of `hide_eq`. -/
theorem eq_hide {a : α} : a = hide a := by
  symm; exact hide_eq

def simpHide (e : Expr) : Meta.SimpM Meta.Simp.Step := do
  let ctx  Simp.getContext
  if let some parent := ctx.parent? then
    if parent.isAppOf ``hide then
      trace[LeanMLIR.Elab] "{Lean.crossEmoji}: parent ({parent}) is an application of `hide`"
      return .continue

  let expr  Meta.mkAppM ``hide #[e]
  let proof  Meta.mkAppOptM ``eq_hide #[none, e]
  return .done {
    expr := expr
    proof? := some proof
  }

protected partial def isConstant (e : Expr) : Bool :=
  match_expr e with
  | Neg.neg _α _self x => HideConstants.isConstant x
  | OfNat.ofNat _α x _self => HideConstants.isConstant x
  | _ => e.isRawNatLit
open HideConstants (isConstant)


simproc BitVec.hideOfIntConstants (BitVec.ofInt _ _) := fun e => do
  let_expr BitVec.ofInt _w x := e | return .continue
  withTraceNode `LeanMLIR.Elab (fun _ => pure m!"Hiding: {e}") <| do
    if !isConstant x then
      trace[Meta.Tactic.simp] "{Lean.crossEmoji}: {x} is not a constant"
      return .continue
    simpHide e

simproc LLVM.hideConst? (LLVM.const? _ _) := fun e => do
  let_expr LLVM.const? _w x := e | return .continue
  withTraceNode `LeanMLIR.Elab (fun _ => pure m!"Hiding: {e}") <| do
    if !isConstant x then
      trace[Meta.Tactic.simp] "{Lean.crossEmoji}: {x} is not a constant"
      return .continue
    simpHide e

macro "hide_constants" : tactic => `(tactic|
  simp -failIfUnchanged -memoize only [BitVec.hideOfIntConstants, LLVM.hideConst?]
  -- --------------------^^^^^^^
  -- NOTE: we have to disable the memoize option, since the simprocs
  -- inspect the parent expressions, which are disregarded by the cache
)

macro "unhide_constants" : tactic => `(tactic|
  all_goals simp -failIfUnchanged only [hide_eq]
)

Hopefully this workaround works in your situation also

Aaron Liu (Jul 31 2025 at 14:22):

There's irreducible_def from file#Tactic/IrreducibleDef

Devon Tuma (Aug 01 2025 at 20:03):

Thanks! The hide_def stuff does seem to solve my use case very well. These definitions made it very easy to tailor the simproc and isConstant definitions to our specific use case too

Devon Tuma (Aug 01 2025 at 20:11):

I've just had to add a few other cases so far for e.g. "variable * constant" that come up for us. But this was a very natural extension to make, and with multiple macros you can have different tiers of hiding things for use cases

Devon Tuma (Aug 04 2025 at 20:55):

I have found now though that in a couple of our cases hide_constants itself can cause the deep recusion error (seemingly when the constants involved are especially large). See for example:

namespace HideConstants

open Lean

/-- Definition of `hide` and `hide_eq` -/
opaque hide_def : { hide : α  α // hide = id } := id, rfl

/--
`hide` is an opaque version of the identity function.
By making it opaque, we prevent the kernel from trying to reduce the argument.
-/
def hide : α  α := hide_def.1

/-- A proof that `hide` is the identity. Note that this is deliberately *not*
a def-eq, since we made `hide` opaque. -/
theorem hide_eq {a : α} : hide a = a := by
  rw [hide, hide_def.2]; rfl

/-- Symmetric version of `hide_eq`. -/
theorem eq_hide {a : α} : a = hide a := by
  symm; exact hide_eq

def simpHide (e : Expr) : Meta.SimpM Meta.Simp.Step := do
  let ctx  Meta.Simp.getContext
  if let some parent := ctx.parent? then
    if parent.isAppOf ``hide then
      trace[LeanMLIR.Elab] "{Lean.crossEmoji}: parent ({parent}) is an application of `hide`"
      return .continue
  let expr  Meta.mkAppM ``hide #[e]
  let proof  Meta.mkAppOptM ``eq_hide #[none, e]
  return .done {
    expr := expr
    proof? := some proof
  }

protected partial def isConstant (e : Expr) : Bool :=
  match_expr e with
  | Neg.neg _α _self x => HideConstants.isConstant x
  | OfNat.ofNat _α x _self => HideConstants.isConstant x
  | Fin.val _α _x => true -- Also try to hide Fin values
  | _ => e.isRawNatLit
open HideConstants (isConstant)

simproc_decl BitVec.hideOfNatConstants (BitVec.ofNat _ _) := fun e => do
  let_expr BitVec.ofNat _w x := e | return .continue
  withTraceNode `LeanMLIR.Elab (fun _ => pure m!"Hiding: {e}") <| do
    if !isConstant x then
      trace[Meta.Tactic.simp] "{Lean.crossEmoji}: {x} is not a constant"
      return .continue
    simpHide e

macro "hide_constants" : tactic => `(tactic|
  simp -failIfUnchanged -memoize only [BitVec.hideOfNatConstants]
)

macro "unhide_constants" : tactic => `(tactic|
  all_goals simp -failIfUnchanged only [hide_eq]
)

example {n : Fin _} (s : Unit) :
    EStateM.run (do
      let _ 
        (match
          (match ((BitVec.ofNat 1 (n * 100000 : Fin 1000000)))[0]'Nat.one_pos with
          | true => true
          | false => true) with
        | true => pure ()
        | false => pure () : EStateM Unit Unit Unit)
      return ()) s = EStateM.Result.ok () s := by
  hide_constants
  stop -- deep recursion is already caused by just calling `hide_constants`
  rw [EStateM.run_bind]
  unhide_constants
  cases (BitVec.ofNat 1 (n * _).val)[0]
  · simp only [EStateM.run_pure]
  · simp only [EStateM.run_pure]

end HideConstants

Although maybe this is also a problem with having the Fin.val case in general without checking things are full constants. There are at least fewer cases where this bug seems to happen.

Devon Tuma (Aug 05 2025 at 00:08):

Actually here's a very simple example I also came across as well. Seems that using n <<< 16 is over n * 2^16 is pretty necessary:

import Mathlib

example (n : ) :
    (match
      (match BitVec.ofNat 64 (n * 2^16) with
      | 0#64 => some false
      | _ => some true) with
    | none => ()
    | some _ => ()) =
    if 1 + 1 = 2 then () else () := by
  rw [one_add_one_eq_two] -- causes deep recursion error

Last updated: Dec 20 2025 at 21:32 UTC