Zulip Chat Archive
Stream: lean4
Topic: Well-founded recursion on Nat
Dan Velleman (Jan 24 2026 at 22:19):
The following used to work:
import Mathlib
@[semireducible]
def mygcd (a b : Nat) : Nat :=
match b with
| 0 => a
| n + 1 =>
have : a % (n + 1) < n + 1 := Nat.mod_lt a (Nat.succ_pos n)
mygcd (n + 1) (a % (n + 1))
termination_by b
example (a n : Nat) : mygcd a (n + 1) = mygcd (n + 1) (a % (n + 1)) := by rfl
Then a change was made so that definitions by well-founded recursion couldn't be marked as semireducible, and it stopped working. Now, in v4.27.0, it is once again possible to mark a definition by well-founded recursion as semireducible if the termination measure has type Nat. So the @[semireducible] in the example above works again. But rfl doesn't work to prove the example, even though it used to. Anyone know why?
Aaron Liu (Jan 24 2026 at 22:21):
See lean4#7965. Since it's not a definitional equality rfl doesn't work.
Dan Velleman (Jan 24 2026 at 22:46):
Well, my question was, why isn't it a definitional equality? In v4.19.0 it was.
I guess this is related to the comment from @Joachim Breitner in lean4#7965 about a couple of mathlib proofs that were broken by this change. I guess for some reason the new fix function makes things that used to be defeq no longer defeq?
Robin Arnez (Jan 24 2026 at 23:25):
Well, it's a compromise. If you compare the following two lines on the earlier and the later version:
example (a n : Nat) : mygcd a (n + 1) = mygcd (n + 1) (a % (n + 1)) := by rfl
example : mygcd 6765 10946 = 1 := by rfl
you'll see that in earlier versions, the first one worked but the second one failed. In newer versions, the first one fails but the second one succeeds pretty quickly, and is still reasonably fast (~a second) for
Absurdly long example
Note: these are the 5000th and 5001st fibonacci number each, so it will actually take 5000 steps to calculate but it has no problem with it!
Robin Arnez (Jan 24 2026 at 23:37):
If you want to know in which sense it's different, it's something along the lines of
mygcd a (n + 1) = something a (n + 1) (n + 1) = something (n + 1) (a % (n + 1)) n
≠ something (n + 1) (a % (n + 1)) (a % (n + 1)) = mygcd (n + 1) (a % (n + 1))
where the last argument to this mystery function is something called "fuel" that decreases by one each iteration instead of by however a % (n + 1) decreases.
Dan Velleman (Jan 24 2026 at 23:45):
Thanks, that helps.
Junyan Xu (Feb 14 2026 at 08:01):
So it seems the number of reduction steps the kernel take doesn't actually depend on the fuel measure (b in termination_by b)? Looking at lean#7965 I see that Nat.fix is defined in terms of go which unfolds to Nat.rec _ _ (fuel + 1) but fuel has 1046 digits in this case, so since the kernel doesn't timeout it can't possibly be reducing Nat.rec, so it somehow reduced mygcd a (n+1) directly to mygcd (n + 1) (a % (n + 1))? Does anyone know how exactly this works (this is not WellFounded.fix but Nat.rec)? Was it already so in Lean 3 (or was there already a corresponding feature in Lean 3)? I guess it wasn't needed because we're freely reducing WellFounded.fix back then.
Robin Arnez (Feb 14 2026 at 08:58):
Well, let's take something like Nat.rec (fun _ ih n => if n < 2 then 0 else ih (n / 2) + 1) fuel n, i.e. Nat.log2. Even if we use an absurdly large number as fuel, say 1000000000, what happens is the following:
Nat.rec (fun _ ih n => if n < 2 then 0 else ih (n / 2) + 1) 1000000000 3reduces to(fun _ ih n => if n < 2 then 0 else ih (n / 2) + 1) 999999999 (Nat.rec (fun _ ih n => if n < 2 then 0 else ih (n / 2) + 1) 999999999) 3- Beta reduction comes into play:
if 3 < 2 then 0 else Nat.rec (fun _ ih n => if n < 2 then 0 else ih (n / 2) + 1) 999999999 (3 / 2) + 1. Now thereccall is buried in theif - Some kind of reducing decidable instances later, the if is reduced to:
Nat.rec (fun _ ih n => if n < 2 then 0 else ih (n / 2) + 1) 999999999 (3 / 2) + 1and then toNat.succ (Nat.rec (fun _ ih n => if n < 2 then 0 else ih (n / 2) + 1) 999999999 (3 / 2)) - Now reduce another time:
Nat.rec (fun _ ih n => if n < 2 then 0 else ih (n / 2) + 1) 999999999 (3 / 2)reduces toif 3 / 2 < 2 then 0 else Nat.rec (fun _ ih n => if n < 2 then 0 else ih (n / 2) + 1) 999999998 (3 / 2 / 2) - Now the condition is false but the
recis in another branch, so we just reduce to0. With theNat.succaround it, we end up withNat.log2 3 = Nat.succ 0 = 1.
So, the kernel doesn't reduce Nat.rec all at once but step by step and the recursive call only happens once reduction reaches it. Thus, how deep Nat.rec is reduced is bounded by the fuel but not necessarily exactly the fuel.
Last updated: Feb 28 2026 at 14:05 UTC