Prerequisites
Description
This grind statement fails because of deep recursion:
example (xs : List Int) : xs[0]? = some (-1) := by grind
simp +arith has the same problem:
example (xs : List Int) : xs[0]? = some (-1) := by simp +arith [Option.eq_some_iff_get_eq]
Expected behavior:
At least, I expect grind and simp +arith not to recurse deeply on these examples.
Actual behavior:
deep recursion in both examples
Versions
Lean 4.30.0-nightly-2026-03-05
Target: x86_64-unknown-linux-gnu
Additional Information
An LLM helped me track down this issue. Allow me to provide its analysis that seemed useful (and feel free to ignore!):
The deep recursion occurs during grind's simp preprocessing when simplifying expressions of the form ∃ h : P, body(h) where body contains a proof term using the bound variable h (e.g., ∃ h : 0 < xs.length, xs[0] = -1 where xs[0] is getElem xs 0 h).
The chain of events:
-
exists_prop_congr (@[congr] lemma in Init/PropLemmas.lean:234) fires on the existential. It rewrites the domain 0 < n → 1 ≤ n via Nat.lt_eq, wrapping the bound variable: h becomes Iff.mpr ... h.
-
simpLoop (in Simp/Main.lean:679) sees e != r.expr (the body changed structurally due to Iff.mpr wrapping), so it recursively calls itself via visitPostContinue.
-
On iteration 2+, exists_prop_congr fires again. The domain 1 ≤ n hasn't changed, BUT:
-
The bug: The Lean.Meta.Simp.Arith.Int.simpEq? function (in Simp/Arith/Int/Simp.lean:34-72), called as part of the simpArith post-processing pipeline (only active when arith := true), normalizes the body equation xs[0] = -1. The equation is already normalized, but the function returns some (r, proof) where r == e — it produces a non-trivial proof of (xs[0] = -1) = (xs[0] = -1) instead of returning none.
-
This makes processCongrHypothesis report modified = true (because r.proof?.isSome = true), so exists_prop_congr succeeds and wraps h with another Iff.mpr layer.
-
Each iteration adds one more Iff.mpr wrapper: h → Iff.mpr(..., h) → Iff.mpr(..., Iff.mpr(..., h)) → ..., creating an infinitely growing term that never stabilizes.
Why only +arith / grind?
The simpArith function is hardcoded into simp's post-processing pipeline and only runs when arith := true (default: false). Regular simp never triggers it. Grind sets arith := true in its simp config (Grind/SimpUtil.lean:193). The underlying theorem Int.Linear.norm_eq_var_const is phrased in terms of internal reflection types (Int.Linear.Expr.denote), so adding it as a simp lemma has no effect — it only fires through the reflection-based simpEq? function.
The Fix
Add if r == e then return none to the norm_eq_var_const and norm_eq_var branches of simpEq? to prevent returning a spurious proof when the normalization produces the same expression. This is consistent with the existing check at line 46 for the general case, and with Nat.simpCnstrPos? which already has a correct r != lhs guard.
Impact
Add 👍 to issues you consider important. If others are impacted by this issue, please ask them to add 👍 to it.
Prerequisites
https://github.com/leanprover/lean4/issues
Avoid dependencies to Mathlib or Batteries.
https://live.lean-lang.org/#project=lean-nightly
(You can also use the settings there to switch to “Lean nightly”)
Description
This
grindstatement fails because of deep recursion:simp +arithhas the same problem:Expected behavior:
At least, I expect
grindandsimp +arithnot to recurse deeply on these examples.Actual behavior:
deep recursion in both examples
Versions
Additional Information
An LLM helped me track down this issue. Allow me to provide its analysis that seemed useful (and feel free to ignore!):
The deep recursion occurs during grind's simp preprocessing when simplifying expressions of the form
∃ h : P, body(h)wherebodycontains a proof term using the bound variableh(e.g.,∃ h : 0 < xs.length, xs[0] = -1wherexs[0]isgetElem xs 0 h).The chain of events:
exists_prop_congr(@[congr] lemma inInit/PropLemmas.lean:234) fires on the existential. It rewrites the domain0 < n→1 ≤ nviaNat.lt_eq, wrapping the bound variable:hbecomesIff.mpr ... h.simpLoop(inSimp/Main.lean:679) seese != r.expr(the body changed structurally due toIff.mprwrapping), so it recursively calls itself viavisitPostContinue.On iteration 2+,
exists_prop_congrfires again. The domain1 ≤ nhasn't changed, BUT:The bug: The
Lean.Meta.Simp.Arith.Int.simpEq?function (inSimp/Arith/Int/Simp.lean:34-72), called as part of thesimpArithpost-processing pipeline (only active whenarith := true), normalizes the body equationxs[0] = -1. The equation is already normalized, but the function returnssome (r, proof)wherer == e— it produces a non-trivial proof of(xs[0] = -1) = (xs[0] = -1)instead of returningnone.This makes
processCongrHypothesisreportmodified = true(becauser.proof?.isSome = true), soexists_prop_congrsucceeds and wrapshwith anotherIff.mprlayer.Each iteration adds one more
Iff.mprwrapper:h → Iff.mpr(..., h) → Iff.mpr(..., Iff.mpr(..., h)) → ..., creating an infinitely growing term that never stabilizes.Why only
+arith/grind?The
simpArithfunction is hardcoded into simp's post-processing pipeline and only runs whenarith := true(default:false). Regularsimpnever triggers it. Grind setsarith := truein its simp config (Grind/SimpUtil.lean:193). The underlying theoremInt.Linear.norm_eq_var_constis phrased in terms of internal reflection types (Int.Linear.Expr.denote), so adding it as a simp lemma has no effect — it only fires through the reflection-basedsimpEq?function.The Fix
Add
if r == e then return noneto thenorm_eq_var_constandnorm_eq_varbranches ofsimpEq?to prevent returning a spurious proof when the normalization produces the same expression. This is consistent with the existing check at line 46 for the general case, and withNat.simpCnstrPos?which already has a correctr != lhsguard.Impact
Add 👍 to issues you consider important. If others are impacted by this issue, please ask them to add 👍 to it.