Zulip Chat Archive

Stream: lean4

Topic: opaque recursion definitions break mergeSort decidability


Tristan Figueroa-Reid (Apr 12 2025 at 22:05):

I decided on Finset.sort which broke in the recent Lean 4.19 update. This is an MRE of the issue:

example : ([1] : List Nat).mergeSort (·  ·) = [1] := by
  unfold List.mergeSort
  decide +kernel

/-
tactic 'decide' failed for proposition
  ([1].mergeSort fun x1 x2 => decide (x1 ≤ x2)) = [1]
since its 'Decidable' instance
  instDecidableEqList ([1].mergeSort fun x1 x2 => decide (x1 ≤ x2)) [1]
did not reduce to 'isTrue' or 'isFalse'.

Reduction got stuck at the 'Decidable' instance
  ([1].mergeSort fun x1 x2 => decide (x1 ≤ x2)).hasDecEq [1]
-/
example : ([1] : List Nat).mergeSort (·  ·) = [1] := by
  decide +kernel

Eric Wieser (Apr 12 2025 at 22:33):

In the past I think the suggestion was to keep these functions defined in a way that is reducible, and then use csimp to link them to their irreducible optimized versions.

Eric Wieser (Apr 12 2025 at 22:36):

I think src#List.mergeSort hasn't changed, but the rules for reducibility have

Tristan Figueroa-Reid (Apr 13 2025 at 00:41):

Eric Wieser said:

In the past I think the suggestion was to keep these functions defined in a way that is reducible, and then use csimp to link them to their irreducible optimized versions.

I do believe that mergeSort already has a irreducible optimized version linked by csimp

Tristan Figueroa-Reid (Apr 13 2025 at 00:44):

I believe the rules for reducibility changed (which broke this well-founded definition of List.mergeSort) changed in #5182.

Tristan Figueroa-Reid (Apr 13 2025 at 00:48):

I don't know much of anything regarding reducibility with csimp, but given that List.mergeSort already has a csimp-linked tail-recursive definition, would it make sense to mark it as semireducible?

Tristan Figueroa-Reid (Apr 13 2025 at 09:27):

Actually, should decide follow csimp marked theorems?

Eric Wieser (Apr 13 2025 at 11:14):

I don't think that would be useful, the runtime-optimal version is less likely to reduce well

Eric Wieser (Apr 13 2025 at 11:15):

And following them in reverse would also not be very useful, because the runtime version should never appear in any theorem statements

Joachim Breitner (Apr 13 2025 at 15:18):

Copying from a discussion with @Bhavik Mehta right now

Oof, is there really no way of making it temporarily semireducible after the fact for decide?

Yes, and after having tiptoed around this issue for a while I believe that reducing proofs in the kernel (as it necessary for well-founded recursion) is just too big of a footgun to be allowed. It may have worked ok in a number of cases so far, but every so often it goes horribly wrong (kernel heartbeat timeout, hard to debug).
Also with the upcoming module system we really want proofs to be irrelevant, so that they can be omitted from interface files, maybe even omitted from downloaded olean etc.
So the conclusion is that well-founded recursion should only be used when you really only care about the classical existence of a function with that specification, and never when you want to get any kind of useful defeqs.

So conversely, if you have a function that you want to have useful defeqs, then don't use well-founded recursion. One way is to use a helper definition that is built to reduce nicely, e.g. using fuel, like I did here: https://github.com/leanprover/lean4/pull/7558

This should be possible for any function that's currently defined by wf recursion with a single Nat measure.

We could even consider having a termination_by fuel … variant that uses that construction internally.

(I don’t know how well this fares when reducing larger inputs, because I assume that although it won’t reduce proofs, the kernel will still construct a large proof for the fuel argument, so it may not scale to large number of recursions either.)

Another alternative is not to use decide, but rewriting, when doing the proof. But I understand that our rewriting tractics (simp, rw) are not great for the use case of “do cbv normalization by rewriting” yet. I did some steps in that direction in https://github.com/leanprover/lean4/pull/5977, but it wasn't quite convincing yet.

I agree that this is all a bit unsatisfactory until we are weaned off reducing wf proofs…

This is orthogonal, but I don't actually understand why reducing defs which use wfrec needs reduction of proofs at all?

Defs using well-founded recursion use WellFounded.fix which is essentially structural recursion on the Acc predicate, and to reduce these definitions by more than one step, the proof of Acc x y needs to be reduce to the Acc.intro constructor. Depending how your well-founded definition is defined, this may involve reducing Nat.le or stuff like that, and can be very expensive, exponential even.

You can kick the can down the road by identifying all affected functions, define them as semireducible, and then right afterwards use attribute [irreducible] foo. This gives you the old behavior and you don’t need to fix all at once.

It works sometimes (for example, for technical reasons one unfolding is actually free), and when it works it’s annoyingly useful. But it shot a few legs, so we’d really like to pretend it never worked at all :-)

Mario Carneiro (Apr 13 2025 at 18:53):

If we really actually make Acc reduction not work, maybe we can finally actually fix the core reason why lean has undecidable typechecking and rip this out of the kernel?

Edward van de Meent (Apr 13 2025 at 19:09):

(what is Acc reduction?) i should read :see_no_evil:

Aaron Liu (Apr 14 2025 at 00:57):

Mario Carneiro said:

If we really actually make Acc reduction not work, maybe we can finally actually fix the core reason why lean has undecidable typechecking and rip this out of the kernel?

Why

Joachim Breitner (Apr 14 2025 at 09:58):

Mario Carneiro said:

If we really actually make Acc reduction not work, maybe we can finally actually fix the core reason why lean has undecidable typechecking and rip this out of the kernel?

Just to avoid confusion, what precisely is “this”?

(I can’t answer your question, but maybe we can at least make it an option by eventually removing all uses of reducible wf rec.)

Mario Carneiro (Apr 14 2025 at 11:05):

I'm thinking to change the inductive schema so that types like Acc do not have an iota rule, but possibly give you an iota axiom instead

Mario Carneiro (Apr 14 2025 at 11:06):

which is hopefully enough to prove WellFounded.fix

Joachim Breitner (Apr 14 2025 at 14:38):

While we are at it: What do you think of https://github.com/leanprover/lean4/issues/5234, @Mario Carneiro ?
Summary: When the measure of wf-recursion is just one Nat (not lexicographic, not a different order), use a Nat-specific fix-point operator that is a drop-in replacement for WellFounded.fix, but implemented by using Nat.rec on fuel that’s initialized by the measure.

It seems that this would be simple to implement, would let decide work for many (but not all) wf-rec function, and would avoid most (bot not all) “reduction bombs” because for neutral arguments, the intial fuel is neutral and reduction gets stuck, as it should.

Mario Carneiro (Apr 14 2025 at 14:44):

I don't see why to specialize on Nat here, you can do the same thing for any inductive type

Mario Carneiro (Apr 14 2025 at 14:45):

I'm sad about the general direction, it's making well founded recursion turn into a big bugaboo and discouraging people from using it

Joachim Breitner (Apr 14 2025 at 15:39):

I’m not particularly happy either (besides that this will make proofs even more irrelevant, which is great for the upcoming module system, smaller olean, less recompilation, and that we hopefully get rid of this sometimes hard-to-debug “reduction bombs”)

Mario Carneiro (Apr 14 2025 at 15:39):

I definitely think proofs should be irrelevant but I don't see why that means wf recursion has to suck

Mario Carneiro (Apr 14 2025 at 15:40):

this should never have been implemented by reducing proofs in the first place

Joachim Breitner (Apr 14 2025 at 15:42):

Mario Carneiro said:

I don't see why to specialize on Nat here, you can do the same thing for any inductive type

Can you? Maybe with an inductive type where the < relation that’s picked up by WellFoundedRelation contains the subterm-relation, unless I am missing something. Do you have concrete examples in mind?

Mario Carneiro (Apr 14 2025 at 15:43):

I was thinking you would not use a well founded relation at all, you would just use the subterm relation on an inductive type

Joachim Breitner (Apr 14 2025 at 15:44):

Mario Carneiro said:

this should never have been implemented by reducing proofs in the first place

What are the alternatives? We once discussed a world where Acc.rec reduces always, without looking at theAcc proof, but doesn’t that make the problem of the kernel reducing things ad infinitum even worse?

Mario Carneiro (Apr 14 2025 at 15:46):

you could reduce only closed terms

Mario Carneiro (Apr 14 2025 at 15:49):

I think we should kind of set Acc aside, because we actually care more about WellFounded (they are equivalent in strength) and WellFounded doesn't have the issue of a monotonically increasing proof term during reduction

Mario Carneiro (Apr 14 2025 at 15:49):

I think we're just reinventing fix + match here, we should just check what Rocq does

Joachim Breitner (Apr 14 2025 at 15:52):

Rocq has an in-kernel termination checker (the “guardedness checker”), AFAIR

Mario Carneiro (Apr 14 2025 at 15:53):

yeah but that's sort of separate from this

Mario Carneiro (Apr 14 2025 at 15:53):

the question is more about when reduction happens

Joachim Breitner (Apr 14 2025 at 15:56):

Mario Carneiro said:

I think we should kind of set Acc aside, because we actually care more about WellFounded (they are equivalent in strength) and WellFounded doesn't have the issue of a monotonically increasing proof term during reduction

I’m sorry, but I think you have to spell this out some more for me. Is there another definition of WellFounded that doesn’t go through Acc and allows for recursion somehow?

Mario Carneiro (Apr 14 2025 at 15:57):

I think the definition of WellFounded is an implementation detail, it could be built in axiomatic

Aaron Liu (Apr 14 2025 at 15:58):

Mario Carneiro said:

I think the definition of WellFounded is an implementation detail, it could be built in axiomatic

How?

Mario Carneiro (Apr 14 2025 at 17:10):

That is, WellFounded is a primitive axiomatic constructor and so is WellFounded.fix and it has magic computation rules provided by the kernel. Whether it gets these computation rules because it's built in or because it has a definition that works out that way doesn't really matter for things built on top

Mario Carneiro (Apr 14 2025 at 17:11):

My point is that WellFounded.fix has better definitional reduction rules than Acc.rec does because Acc needs to manipulate a proof term in every step of the recursion

Edward van de Meent (Apr 14 2025 at 17:12):

ok, but then will we be able to create instances for WellFounded?

Mario Carneiro (Apr 14 2025 at 17:12):

sure, it can have a constructor that looks like WellFounded.mk too

Mario Carneiro (Apr 14 2025 at 17:13):

I'm saying to separate the API from the implementation

Edward van de Meent (Apr 14 2025 at 17:14):

i guess that makes sense

Mario Carneiro (Apr 14 2025 at 17:14):

the API of WellFounded includes WellFounded.mk and WellFounded.fix and possibly a defeq that looks like WellFounded.fix_eq, so it's worth talking about what kind of API here and what kind of restrictions on fix_eq give desirable behaviors for things on top

Mario Carneiro (Apr 14 2025 at 17:15):

and if at the end of that it turns out it can be defined in terms of something more basic, great, but if not, it can still be a built in primitive supplied by the kernel

Mario Carneiro (Apr 14 2025 at 17:15):

Acc is already super special and complicates the inductive schema for a case which has essentially only one practical application

Mario Carneiro (Apr 14 2025 at 17:16):

I would definitely trade having a simpler schema in exchange for needing another magic special type like Quot

Edward van de Meent (Apr 14 2025 at 17:16):

does making WellFounded internal/axiomatic let us make sure something like Acc can't be created, though?

Mario Carneiro (Apr 14 2025 at 17:17):

yeah, we would ban types like it and/or make them have crappy defeqs

Aaron Liu (Apr 14 2025 at 17:17):

Edward van de Meent said:

does making WellFounded internal/axiomatic let us make sure something like Acc can't be created, though?

You can do something like saying the subtype of stuff less than x is well-founded

Mario Carneiro (Apr 14 2025 at 17:17):

if it wasn't for Acc -> WellFounded -> every wf rec definition ever, we wouldn't care about Acc at all

Aaron Liu (Apr 14 2025 at 17:19):

Mario Carneiro said:

if it wasn't for Acc -> WellFounded -> every wf rec definition ever, we wouldn't care about Acc at all

We have this one file file#SetTheory/Ordinal/Rank

Mario Carneiro (Apr 14 2025 at 17:20):

Also, keep in mind that "Acc in Type" is one way of neutering Acc by making it act like a regular inductive

Mario Carneiro (Apr 14 2025 at 17:20):

and in the presence of the axiom of choice you can go back and forth with something that has the type of the current Acc

Mario Carneiro (Apr 14 2025 at 17:20):

so I think that rank file would be fine

Edward van de Meent (Apr 14 2025 at 17:24):

what is special about current Acc? as far as i can tell it's really just a normal inductive, so i don't get how you would even begin to ban "inductives like it"

Mario Carneiro (Apr 14 2025 at 17:24):

It's a large eliminating subsingleton type

Edward van de Meent (Apr 14 2025 at 17:24):

it's large eliminating!?

Mario Carneiro (Apr 14 2025 at 17:25):

it's the only large eliminating recursive inductive type in lean

Edward van de Meent (Apr 14 2025 at 17:25):

ah, i missed that bit

Edward van de Meent (Apr 14 2025 at 17:25):

so you don't even get to define something like Acc in lean as-is? that clears it up a bunch

Mario Carneiro (Apr 14 2025 at 17:26):

Equality is also a large eliminating inductive, but it's not recursive

Mario Carneiro (Apr 14 2025 at 17:26):

No, you can define something like Acc

Mario Carneiro (Apr 14 2025 at 17:26):

because it's not actually special cased

Mario Carneiro (Apr 14 2025 at 17:27):

but there is an additional clause in the inductive schema which allows "Acc and things like it" but in practice that means just Acc

Mario Carneiro (Apr 14 2025 at 17:28):

in fact, there is a proof in #leantt that Acc is universal for such types

Mario Carneiro (Apr 14 2025 at 17:29):

Edward van de Meent said:

it's large eliminating!?

It has to be large eliminating or else one can't define functions by well founded recursion, only prove theorems by well founded induction

Joachim Breitner (Apr 14 2025 at 17:30):

Mario Carneiro said:

Equality is also a large eliminating inductive, but it's not recursive

Any interesting large eliminating inductives besides these two? Or put differently: If we didn’t need Acc, could Eq be the only large eliminating inductive?

Mario Carneiro (Apr 14 2025 at 17:31):

And is also large eliminating

Mario Carneiro (Apr 14 2025 at 17:31):

but it's not interesting

Mario Carneiro (Apr 14 2025 at 17:31):

True and False too

Mario Carneiro (Apr 14 2025 at 17:31):

only recursive large eliminating types are really problematic though

Mario Carneiro (Apr 14 2025 at 17:31):

Eq is semi-problematic :)

Aaron Liu (Apr 14 2025 at 17:32):

False, Eq, and Acc are interesting

Aaron Liu (Apr 14 2025 at 17:34):

With True you can't do anything with its large elimination and with And you can just use the projections

Mario Carneiro (Apr 14 2025 at 17:35):

False's large elimination is quite important but it usually is harmless from a type theory perspective because it has no iota rule (since it has no constructors to have iota rules for)

Aaron Liu (Apr 14 2025 at 17:39):

Mario Carneiro said:

False's large elimination is quite important but it usually is harmless from a type theory perspective because it has no iota rule (since it has no constructors to have iota rules for)

What does iota rule do (why is not having one harmless)?

Mario Carneiro (Apr 14 2025 at 17:40):

T.rec F (T.constructor x) = F x

Mario Carneiro (Apr 14 2025 at 17:41):

you have one of those for every constructor of an inductive type, so for False there are no rules

Aaron Liu (Apr 14 2025 at 17:43):

Is having these "harmful" then?

Mario Carneiro (Apr 14 2025 at 17:43):

it's what makes the theory interesting

Mario Carneiro (Apr 14 2025 at 17:44):

where "interesting" may go as far as "soundness is an open question"

Mario Carneiro (Apr 14 2025 at 17:44):

but it's not like we don't want to be able to make definitions by recursion on Nat

Mario Carneiro (Apr 14 2025 at 18:05):

Here's a proof (using AC) that it is possible to construct WellFounded.fix using a small-eliminating Acc:

import Mathlib.Logic.Unique

inductive SmallAcc {α} (r : α  α  Prop) : α  Prop
  | mk x : Unit  ( y, r y x  SmallAcc r y)  SmallAcc r x

def WellFounded' {α} (r : α  α  Prop) :=  x, SmallAcc r x

inductive FixOut {α} {C : α  Sort _} {r : α  α  Prop}
    (F : (x : α)  ((y : α)  r y x  C y)  C x) :  x, C x  Prop
  | mk x f : ( y h, FixOut F y (f y h))  FixOut F x (F x f)

theorem SmallAcc.fix_exists {α} {C : α  Sort _} {r : α  α  Prop}
    (F : (x : α)  ((y : α)  r y x  C y)  C x) (x : α) (hwf : SmallAcc r x) :
     y : C x, FixOut F x y :=
  hwf.rec fun _ _ _ IH =>
    F _ fun y h => (IH y h).choose, ⟨_, _, fun y h => (IH y h).choose_spec⟩⟩

noncomputable def WellFounded'.fix {α} {C : α  Sort _} {r : α  α  Prop}
    (hwf : WellFounded' r) (F : (x : α)  ((y : α)  r y x  C y)  C x) (x : α) : C x :=
  (SmallAcc.fix_exists F x (hwf x)).choose

theorem WellFounded'.fix_eq {α} {C : α  Sort _} {r : α  α  Prop}
    (hwf : WellFounded' r) (F :  x, ( y, r y x  C y)  C x) (x : α) :
    fix hwf F x = F x (fun y _ => fix hwf F y) := by
  suffices  y y', FixOut F x y  FixOut F x y'  y = y' from
    this _ _ (SmallAcc.fix_exists F x (hwf x)).choose_spec
      ⟨_, _, fun y _ => (SmallAcc.fix_exists F y (hwf y)).choose_spec
  intro y y' H1 H2
  induction H1 with
  | _ x f h IH =>
    let ⟨_, f', h' := H2
    congr; ext; exact IH _ _ _ (h' ..)

Aaron Liu (Apr 14 2025 at 18:07):

No computation makes it slightly disappointing

Mario Carneiro (Apr 14 2025 at 18:08):

the point is that if one is willing to give up computation then we don't need this kind of inductive at all

Mario Carneiro (Apr 14 2025 at 18:08):

plus the compiler can of course support computation of this function anyway

Mario Carneiro (Apr 14 2025 at 18:09):

I don't think it's possible to get a version that computes in the kernel without kernel magic (either Acc-like types or primitive WellFounded.fix)

Aaron Liu (Apr 14 2025 at 18:12):

What would be the consequences of using a type-valued Acc?

Mario Carneiro (Apr 14 2025 at 18:19):

You would be able to define fix easily, but WellFounded would no longer be a Prop, and also the proof of well foundedness would not be erased

Mario Carneiro (Apr 14 2025 at 18:20):

If you make WellFounded a prop using Nonempty then you need the axiom of choice to define fix and you end up with something similar to the above

Mario Carneiro (Apr 14 2025 at 18:21):

It's possible you could make it work if we had erasure annotations and used them on well foundedness arguments

Mario Carneiro (Apr 14 2025 at 18:22):

which TBH sounds less scary than the alternatives that involve kernel magic

Edward van de Meent (Apr 14 2025 at 18:24):

so if i understand correctly, you're proposing to get rid of this rule:
image.png
and make WellFounded a built-in exception?

Edward van de Meent (Apr 14 2025 at 18:26):

(the image is from #leantt , i added the box)

Mario Carneiro (Apr 14 2025 at 18:26):

WellFounded isn't exactly an exception to this rule, since it's not an inductive type of this kind at all (it's a structure wrapping Acc)

Edward van de Meent (Apr 14 2025 at 18:26):

ah, that's fair

Joachim Breitner (Apr 14 2025 at 18:31):

Mario Carneiro said:

the proof of well foundedness would not be erased

Does that matter? At least if you refer to erasure in compiled code, because the compiler works on the recursive PreDefinition and doesn’t even see the kernel-facing construction we are discussing here. And the kernel doesn’t erase proofs, does it?

Mario Carneiro (Apr 14 2025 at 18:32):

Ah, I suppose that's true

Mario Carneiro (Apr 14 2025 at 18:33):

hm... let's try it and see what breaks?

Joachim Breitner (Apr 14 2025 at 18:36):

Making Acc a Type would allow simplifying the type theory, but not help with the other issues (slow reduction of Acc terms, for example), would it?

Edward van de Meent (Apr 14 2025 at 18:37):

it might force users to write better-reducing "proofs" though

Edward van de Meent (Apr 14 2025 at 18:37):

i.e. not use tactics

Mario Carneiro (Apr 14 2025 at 18:38):

I think it would mean that defeq would work reliably

Mario Carneiro (Apr 14 2025 at 18:39):

well foundedness proofs would have a similar status to Decidable proofs, you would want to avoid using rw except in the proof arguments

Joachim Breitner (Apr 14 2025 at 18:40):

I see – are saying we need WF proofs to be more carefully crafted, and that’s more likely to happen in Type

Mario Carneiro (Apr 14 2025 at 18:40):

Note that a (Type-valued) proof of well foundedness still contains a lot of proofs in it because the r argument is still a prop

Mario Carneiro (Apr 14 2025 at 18:41):

and unlike "reducing proofs", we can clearly separate out the spine of the WF proof from the side proofs showing individual relations hold

Joachim Breitner (Apr 14 2025 at 18:41):

Right, that’s my worry still

Mario Carneiro (Apr 14 2025 at 18:42):

you would not need to do anything special to tell lean to reduce the WF argument but not the side proofs

Mario Carneiro (Apr 14 2025 at 18:46):

I think it makes a difference in this setting whether you are doing well founded recursion over a relation or its transitive closure. Doing recursion on the transitive closure should be more efficient

Mario Carneiro (Apr 14 2025 at 18:53):

I'm trying it out now, and there is an issue: Acc in Type is a subsingleton but not definitionally so (because we don't have eta for "recursive structures" and that sounds scary), which means that fix_eq is not a defeq because it uses two different derivations for the well foundedness proof on each side of the equality

Aaron Liu (Apr 14 2025 at 18:54):

What if we add a rule that elements of a structurally subsingleton type are definitional equal?

Aaron Liu (Apr 14 2025 at 18:55):

Structural eta already does a lot of this, recursive types like Acc are sort of an exception now

Mario Carneiro (Apr 14 2025 at 18:55):

it could work

Mario Carneiro (Apr 14 2025 at 18:56):

I think it reintroduces the issue that makes Acc bad though

Mario Carneiro (Apr 14 2025 at 18:57):

you can use definitional equality to swap out constructors in an inconsistent context and so typing is undecidable

Mario Carneiro (Apr 14 2025 at 18:58):

I guess that's the whole point, WellFounded.fix_eq (which ignores its proof argument) can't be a defeq without implicating undecidability of typechecking

Mario Carneiro (Apr 14 2025 at 18:59):

note that Acc.fixF works just fine

Edward van de Meent (Apr 14 2025 at 19:01):

docs#Acc.fixF :looking:

Edward van de Meent (Apr 14 2025 at 19:02):

:man_shrugging:

Mario Carneiro (Apr 14 2025 at 19:02):

docs#WellFounded.fixF

Mario Carneiro (Apr 14 2025 at 19:07):

But actually, I think it doesn't matter that fix_eq isn't a defeq. It's already not a defeq. The question is whether it computes on closed terms, and I believe it should

Bhavik Mehta (May 01 2025 at 14:43):

So conversely, if you have a function that you want to have useful defeqs, then don't use well-founded recursion

I have made this change for Nat.xgcd here: #24514

Aaron Liu (May 01 2025 at 16:11):

Should we make everything come with fuel?

Bhavik Mehta (May 01 2025 at 16:53):

That's my conclusion from Joachim's message

Antoine Chambert-Loir (Jun 05 2025 at 19:01):

Do you have a link to how fuel should be used, or is it enough to guess from what @Bhavik Mehta did in #24514?

François G. Dorais (Jun 05 2025 at 19:09):

Fuel only works if you have an explicit upper bound on the number of recursion steps needed, so it is not a fully general replacement.

Eric Wieser (Jun 05 2025 at 19:20):

For mergeSort, this is easy enough to prove as n^2, right?

François G. Dorais (Jun 05 2025 at 19:37):

Yes, the problematic cases are much more complex.

Antoine Chambert-Loir (Jun 05 2025 at 20:14):

and for the list of prime factors which interests me, it's at most Nat.log 2 n, so that's ok for me too…

Bhavik Mehta (Jun 05 2025 at 22:22):

I had adapted it from Joachim's change to Nat.div in core, so that example could also be helpful.

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

There is an almost fully general version, if you allow fuel to be an element of any inductive type, since at that point it basically becomes a version of Acc in type

Junyan Xu (Oct 03 2025 at 16:32):

In #30169 I redefine xgcd using a "reducible" version of Nat.strongRec, which uses Nat.rec instead of Acc.rec/WellFounded.fix. I changed the mathlib version, but maybe it's better to change batteries version and deduplicate the mathlib version. The changes for xgcd are pretty clean, without the need of private lemmas, though I do need some private def/lemma to prove my strongRec' reduces as expected. You could nest strongRec' with other recursors to work with lexicographic orders involving Nat.
My original motivation is to make Nat.binaryRec reducible (for exponentiation by doubling), but I realized strongRec' still requires linear (not logarithmic) complexity to evaluate a closed term. So this is a case where using fuel is really useful. But I wonder whether the kernel compute the fuel (Nat.log2 in this case) in logarithmic time? It should be the case if it uses GMP.

Bhavik Mehta (Oct 03 2025 at 16:40):

How does it compare to #24514, which also makes xgcd reducible?

Junyan Xu (Oct 03 2025 at 17:17):

The changes for xgcd are pretty clean, without the need of private lemmas

Bhavik Mehta (Oct 03 2025 at 17:42):

I was specifically thinking about the speed of reduction here, rather than the number of private lemmas.

Junyan Xu (Oct 03 2025 at 18:35):

I think our definitions (and lean#7558) boil down to the same thing, so I'm not expect much difference in performance, though I also don't know how to test.
Unexpectedly my definition is breaking norm_num (reporting Stack overflow detected. Aborting.), and the following tests

theorem ex61 : Nat.gcd (553105253 * 776531401) (553105253 * 920419823) = 553105253 := by norm_num1
theorem ex62 : Nat.gcd (2^1000 - 1) (2^1001 - 1) = 1 := by norm_num1
theorem ex62' : Nat.gcd (2^1001 - 1) (2^1000 - 1) = 1 := by norm_num1
theorem ex63 : Nat.gcd (2^500 - 1) (2^510 - 1) = 2^10 - 1 := by norm_num1
theorem ex64 : Int.gcd (1 - 2^500) (2^510 - 1) = 2^10 - 1 := by norm_num1
theorem ex64' : Int.gcd (1 - 2^500) (2^510 - 1) + 1 = 2^10 := by norm_num1

needs to be replaced by

set_option exponentiation.threshold 1001
theorem ex62 : Nat.gcd (2^1000 - 1) (2^1001 - 1) = 1 := rfl
theorem ex62' : Nat.gcd (2^1001 - 1) (2^1000 - 1) = 1 := rfl
theorem ex63 : Nat.gcd (2^500 - 1) (2^510 - 1) = 2^10 - 1 := rfl
set_option maxRecDepth 4000
theorem ex64 : Int.gcd (1 - 2^500) (2^510 - 1) = 2^10 - 1 := rfl
theorem ex64' : Int.gcd (1 - 2^500) (2^510 - 1) + 1 = 2^10 := rfl

Aaron Liu (Oct 03 2025 at 18:39):

That sounds like a thing that happens in complied code

Aaron Liu (Oct 03 2025 at 18:40):

We can just fix that with a csimp lemma

Mario Carneiro (Oct 03 2025 at 18:41):

when norm_num breaks like that that usually means there is a type error in the proof forcing something bad to be unfolded

Aaron Liu (Oct 03 2025 at 18:41):

These are big numbers

Mario Carneiro (Oct 03 2025 at 18:42):

yes, but norm_num should still generate good proofs

Aaron Liu (Oct 03 2025 at 18:42):

It was a stack overflow

Mario Carneiro (Oct 03 2025 at 18:43):

in fact it should probably just be deferring to kernel checking in this case because it's Nat.gcd

Aaron Liu (Oct 03 2025 at 18:43):

Sounds like a problem with the generation not with the proof

Mario Carneiro (Oct 03 2025 at 18:44):

the definition of xgcd should not be unfolded by norm_num, if it is that's a bug

Aaron Liu (Oct 03 2025 at 18:45):

What's the code that does norm num gcd

Mario Carneiro (Oct 03 2025 at 18:45):

unfortunately norm_num has the problem that many of the subproofs are actually true by rfl, it's just not a good idea to prove them that way

Mario Carneiro (Oct 03 2025 at 18:45):

so when something goes wrong it doesn't break, it just gets really slow or overflows

Aaron Liu (Oct 03 2025 at 18:46):

Oh that might be a kernel problem now that I think about it

Mario Carneiro (Oct 03 2025 at 18:46):

It's possible that norm_num is still doing gcd "manually", not sure it has been updated for all the new kernel accelerated functions

Mario Carneiro (Oct 03 2025 at 18:47):

it seems to be so

Mario Carneiro (Oct 03 2025 at 18:48):

I think that only helps for gcd itself though, if you want the other parts of xgcd you still need to use the old code, I don't think there is some clever algebra that will give you the coefficients using only gcd

Aaron Liu (Oct 03 2025 at 18:48):

docs#Tactic.NormNum.proveNatGCD

Aaron Liu (Oct 03 2025 at 18:49):

now I'm back to thinking that this is not a kernel problem

Aaron Liu (Oct 03 2025 at 18:50):

it should probably be fixed if you write a @[csimp] lemma for Nat.xgcdAux

Mario Carneiro (Oct 03 2025 at 18:50):

the code does call Nat.xgcdAux in meta code

Mario Carneiro (Oct 03 2025 at 18:51):

so if that function is written badly then it will overflow in #eval

Aaron Liu (Oct 03 2025 at 18:51):

yes I claim that that's what's stack overflowing

Mario Carneiro (Oct 03 2025 at 18:52):

I think the call to strongRec builds up a huge stack of closures which overflows when it is called

Mario Carneiro (Oct 03 2025 at 18:54):

I think strongRec' is the one that needs a csimp lemma

Aaron Liu (Oct 03 2025 at 18:55):

oh yeah that works too

Junyan Xu (Oct 03 2025 at 19:45):

Thanks! I'm csimping strongRec' to strongRec in #30181. strongRec is irreducible so it's not gonna overflow, right?

Aaron Liu (Oct 03 2025 at 19:51):

strongRec being irreducible is actually irrelevant

Junyan Xu (Oct 03 2025 at 21:23):

#30181 passed CI and !bench reported a speedup, while #30169 is still stuck at "test mathlib" after 2h 46m passed.
image.png

The speedup is probably spurious, as #30144 (benched against the same base commit 3da1779) reports an almost identical speedup despite having diffs disjoint from #30181.

Aaron Liu (Oct 03 2025 at 21:24):

hmmm

Aaron Liu (Oct 03 2025 at 21:25):

maybe still do that @[csimp] lemma for Nat.xgcdAux and just make it do the original implementation

Aaron Liu (Oct 03 2025 at 21:25):

or make strongRec be @[specialize] maybe that will work

Junyan Xu (Oct 03 2025 at 21:27):

#30181 works and is awaiting review.
I've closed #30169, not deleting the branch yet for forensic purposes. (maybe the CI run and/or the machine has crashed.)

Junyan Xu (Oct 04 2025 at 09:45):

In #30144 I changed Nat.binaryRec to use Nat.rec with docs#Nat.log2 as fuel. But this breaks the following test

def Nat.popcountTR (n : Nat) : Nat :=
  n.binaryRec (·) (fun b _ f x  f (x + b.toNat)) 0

/-- info: 1 -/
#guard_msgs in
#eval Nat.popcountTR (2 ^ 20240)

with error

info: stderr:
libc++abi: terminating due to uncaught exception of type lean::throwable: deep recursion was detected at 'interpreter' (potential solution: increase stack space in your system)
interpreter stacktrace:
#1 Nat.binaryRec._at_.Nat.popcountTR.spec_0._lam_2._boxed
#2 Nat.binaryRec._at_.Nat.popcountTR.spec_0._lam_0
#3 Nat.binaryRec._at_.Nat.popcountTR.spec_0._lam_0._boxed
#4 Nat.binaryRec._at_.Nat.popcountTR.spec_0._lam_2
#5 Nat.binaryRec._at_.Nat.popcountTR.spec_0._lam_2._boxed
#6 Nat.binaryRec._at_.Nat.popcountTR.spec_0._lam_0
#7 Nat.binaryRec._at_.Nat.popcountTR.spec_0._lam_0._boxed
#8 Nat.binaryRec._at_.Nat.popcountTR.spec_0._lam_2
...
#8306 Nat.binaryRec._at_.Nat.popcountTR.spec_0._lam_0
#8307 Nat.binaryRec._at_.Nat.popcountTR.spec_0._lam_0._boxed
#8308 Nat.binaryRec._at_.Nat.popcountTR.spec_0._lam_2
#8309 Nat.binaryRec._at_.Nat.popcountTR.spec_0._lam_2._boxed
#8310 Nat.binaryRec._at_.Nat.popcountTR.spec_0
#8311 Nat.popcountTR
#8312 _eval._closed_1
#8313 _eval._closed_2
#8314 _eval._closed_3
#8315 _eval
error: Lean exited with code 134

Anyone knows how to fix?

Aaron Liu (Oct 04 2025 at 10:46):

csimp it

Junyan Xu (Oct 04 2025 at 11:18):

#eval also uses csimp? Yes indeed ...
I also tried

set_option exponentiation.threshold 20240
set_option maxRecDepth 15838
theorem x : Nat.popcountTR (2 ^ 20240) = 1 := rfl

which triggers "maximum recursion depth has been reached"; if I change maxRecDepth to 15839 then "Stack overflow detected. Aborting." Is stack size adjustable?

Junyan Xu (Oct 04 2025 at 11:20):

It seems that csimp only supports constants now, so I need to keep the original binaryRec and csimp my new version to it?

Junyan Xu (Oct 04 2025 at 11:27):

It turns out

set_option exponentiation.threshold 20240
set_option maxRecDepth 15842
theorem x : Nat.log2 (2 ^ 20240) = 20240 := rfl

is sufficient to overflow the stack. #eval Nat.log2 (2 ^ 20240) is okay though.

Junyan Xu (Oct 04 2025 at 12:03):

This is interesting: with the current state of #30144, #eval Nat.popcountTR (2 ^ 2075) is okay while #eval Nat.popcountTR (2 ^ 2076) crashes Lean. But if I change binaryRecAux from abbrev to def, then I need to go up to #eval Nat.popcountTR (2 ^ 4186) to crash Lean.

Aaron Liu (Oct 04 2025 at 12:22):

probably inlining?

Aaron Liu (Oct 04 2025 at 12:22):

abbrev makes it reducible and inline

Junyan Xu (Oct 04 2025 at 12:22):

Following the PR #17897 by @Yuyang Zhao that introduced the test, I tried to add @[specialize] to def binaryRecAux, but this decreases the crash threshold from 4186 to 4152. Any idea what's going on?

Junyan Xu (Oct 04 2025 at 12:23):

Adding inline or specialize to abbrev binaryRecAux has no effect.

Aaron Liu (Oct 04 2025 at 12:23):

I have no idea why

Junyan Xu (Oct 04 2025 at 12:24):

Adding inline to def binaryRecAux decreases the crash threshold to 2076, same as abbrev.

Mario Carneiro (Oct 04 2025 at 19:17):

this indicates that it's not tail recursing either way

Junyan Xu (Oct 04 2025 at 22:05):

Thanks for the inputs, problem solved by using specialize on both binaryRecAux and binaryRec AND using the equation compiler instead of Nat.rec for binaryRecAux!

Junyan Xu (Oct 04 2025 at 22:58):

What would happen if we remove @[irreducible] from docs#Rat.add /sub/mul/inv? They're in lean4 repo so harder to test. The above two PRs enable kernel reduction of elliptic curve nsmul over fields of prime order, and I'd like the same over the rationals.

Bhavik Mehta (Oct 04 2025 at 23:43):

Junyan Xu said:

The above two PRs enable kernel reduction of elliptic curve nsmul over fields of prime order, and I'd like the same over the rationals.

curiously enough, this was my motivation for my PR too :)

Junyan Xu (Oct 04 2025 at 23:47):

Good to know! I only started thinking about this after the post #general > Problems with elliptic curves @ 💬.

Bhavik Mehta (Oct 04 2025 at 23:53):

You might like to discuss with @Andrew Yang, who has more thoughts and more code on this :)


Last updated: Dec 20 2025 at 21:32 UTC