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


Last updated: May 02 2025 at 03:31 UTC