-
Notifications
You must be signed in to change notification settings - Fork 546
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: kernel-irreducible well-founded recursion #5192
Comments
Ok, import Mathlib
open Nat
def minFacAux1 (n : ℕ) (fuel : ℕ) (k : ℕ) (_ : 2 * fuel > sqrt n + 2 - k) : ℕ :=
match fuel with
| 0 => by exfalso; omega
| fuel+1 =>
if h : n < k * k then n
else
if k ∣ n then k
else
minFacAux1 n fuel (k + 2) (by
push_neg at h
have : sqrt n ≥ k := by exact le_sqrt.mpr h
omega
)
def minFacAux2 (n : ℕ) : ℕ → ℕ
| k =>
if n < k * k then n
else
if k ∣ n then k
else
minFacAux2 n (k + 2)
termination_by k => sqrt n + 2 - k
decreasing_by simp_wf; apply minFac_lemma n k; assumption
/--
info: 449
---
info: time: 15ms
-/
#guard_msgs in
#time
#reduce minFacAux1 449 449 3 (by norm_num)
/--
info: 449
---
info: time: 1882ms
-/
#guard_msgs in
#time
#reduce minFacAux2 449 3 So maybe the conclusion should be that one simply should not use well-founded recursion when one wants kernel reduction. |
I'd really rather we fix well founded recursion to not be so broken rather than pile on more hacks. |
Well, it’s not broken, but it’s just too slow when you try to reduce it. So it seems unless we can make it reduce as efficient as it looks, we should avoid reducing it? What are our options at making well-founded recursion reduce efficiently? If we are willing to give up termination I guess we can just not reduce |
We shouldn't be reducing proofs in the first place. This is the source of all of the problems. It's possible to reduce the recursor without reducing the proof, because it can only unfold to one thing. This is described in my thesis. But I think we want something a bit more specialized to well founded recursion in this case, so that the kernel can actually just run the function which is what users want anyway. Never running the function is not a solution, it's causing more problems because it means that lean can't evaluate all expressions in its language so people have to just avoid a key feature of lean entirely when using kernel reduction. That's not a sustainable path forward. Re: "If we are willing to give up termination": We already gave up termination a long time ago. Lean has been known to have theoretical termination issues for a while, and we also have concrete examples of code that fails to terminate in practice. I think you don't even need Acc to do it, reducing proofs is enough to create the counterexample. |
If we can get away with not reducing these proofs, I would be happy! But I don't see not reducing certain definition as bleak.
I expect we'll have more variants of Isabelle-style “here is a specification of a function, construct it under hood for me and prove it satisfies these equations” function definitions. I'm in particular thinking of tail-recursive and monadic functions, or more general fix points in partial order. Maybe others that just prove the existence of a certain function. I don't expect these will have any useful DefEqs, but aren't they still useful features in a theorem prover? |
The trouble is that lean currently leans hard on defeq as a method for evaluating arbitrary functions, in |
I'd say we have the same mechanism as Isabelle: rewriting with Proof by reflection is something I, too, want to be well-supported by Lean, and plan to work on soon. But as long as reduction of well-founded recursive functions is horribly slow I'd rather prevent it from happening (so that users know that it isn't something that's supported) then having unhappy users due to slow reductions. If we ever do get reduction of fix without having to reduce the proof objects we can change that again. |
Here might be a way to have our cake and eat it too. The majority of functions defined by well-founded recursion have a def Nat.fix {motive : α → Sort u} (h : α → Nat) (F : (x : α) → ((y : α) → h y < h x → motive y) → motive x) : (x : α) → motive x :=
let rec go : ∀ (fuel : Nat) (x : α), (h x < fuel) → motive x := Nat.rec
(fun _ hfuel => (not_succ_le_zero _ hfuel).elim)
(fun _ ih x hfuel => F x (fun y hy => ih y (Nat.lt_of_lt_of_le hy (Nat.le_of_lt_add_one hfuel))))
fun x => go (h x + 1) x (Nat.lt_add_one _)
theorem Nat.fix.go_fuel_congr {motive : α → Sort u} (h : α → Nat) (F : (x : α) → ((y : α) → h y < h x → motive y) → motive x)
(fuel1 fuel2 : Nat) (x : α) (hfuel1 : h x < fuel1) (hfuel2 : h x < fuel2) :
Nat.fix.go h F fuel1 x hfuel1 = Nat.fix.go h F fuel2 x hfuel2 := by
induction fuel1 generalizing fuel2 x hfuel2 with
| zero => contradiction
| succ fuel1 IH => cases fuel2 with
| zero => contradiction
| succ fuel2 => show F .. = F ..; congr; ext y hy; apply IH
theorem Nat.fix_eq {motive : α → Sort u} (h : α → Nat) (F : (x : α) → ((y : α) → h y < h x → motive y) → motive x)
(x : α) : Nat.fix h F x = F x (fun y _ => Nat.fix h F y) := by
show F .. = F ..; congr; ext y hy
apply Nat.fix.go_fuel_congr It’s about bedtime here, so I didn’t measure the reduction speed, but I expect its similarly good as with One worry is that the kernel, even though it won’t have to reduce proofs, it will build large unevaluated proof terms in each step and that this will kill performance. |
And my expectation was proven wrong, it’s about as bad as the well-founded definition: def minFacAux3 (n : ℕ) : ℕ → ℕ :=
Nat.fix (fun k => sqrt n + 2 - k) fun k ih =>
if h : n < k * k then n
else
if k ∣ n then k
else
ih (k + 2) (by simp_wf; apply minFac_lemma n k; assumption)
/--
info: 449
---
info: time: 1914ms
-/
#guard_msgs in
#time
#reduce minFacAux3 449 3 Maybe
This is strange. I wonder if I made a silly mistake somewhere. Yes, I did! The expensive thing isn’t /--
info: 21
---
info: time: 1880ms
-/
#guard_msgs in
#time
#reduce Nat.sqrt 449 Above I accidentially initialized
then it is just as slow. So much for barking up the wrong tree. Now I wonder if I can speed up the definition by well-founded recursion if I avoid def minFacAux4 (n : ℕ) : ℕ → ℕ
| k =>
if h : n < k * k then n
else
if k ∣ n then k
else
minFacAux4 n (k + 2)
termination_by k => 2*n + 1 - k
decreasing_by
simp_wf
push_neg at h
have : k ≤ n := k.le_mul_self.trans h
omega But it’s not faster either. I am confused. I’ll just continue using this as a note-taking post… It seems that elab "#kernel_reduce" t:term : command => Lean.Elab.Command.runTermElabM fun _ => do
let e ← Lean.Elab.Term.elabTermAndSynthesize t none
let e' ← Lean.ofExceptKernelException <| Lean.Kernel.whnf (← Lean.getEnv) (← Lean.getLCtx) e
Lean.logInfo m!"{e'}" I am slowly making sense of this.
Note that having So my simple benchmarking shows this
Why is the last one faster than the manual fuel version? I suspected the Looking at def sqrt1 (n : Nat) : Nat :=
if n ≤ 1 then n else
iter n (n / 2)
where
/-- Auxiliary for `sqrt`. If `guess` is greater than the integer square root of `n`,
returns the integer square root of `n`. -/
iter (n guess : Nat) : Nat :=
let next := (guess + n / guess) / 2
if _h : next < guess then
iter n next
else
guess
termination_by guess with one using def sqrt2 (n : Nat) : Nat :=
if n ≤ 1 then n else
iter (n / 2)
where
/-- Auxiliary for `sqrt`. If `guess` is greater than the integer square root of `n`,
returns the integer square root of `n`. -/
iter : Nat → Nat := Nat.fix (fun guess => guess) (fun guess ih =>
let next := (guess + n / guess) / 2
if _h : next < guess then
ih next (by decreasing_tactic)
else
guess
) then calculating the square root of 441 speeds up from This improvement seems to just too good to leave on the table! |
Another user bitten by accidentially reducing a function defined with well-founded recursion: This increases my confidence that at least with the current status quo, we should block reduction of wf-rec functions, at least until when and if we can evaluate them efficiently. |
It seems actually likely that the Lean kernel will at some point do this kind of fast reduction of Also, it’s not unlikely that eventually the kernel will have more fine-grained transparency controls, like the elaborator now. The timeline for both isn’t clear, though, and until then I am still in favor of removing the footgun of kernel-reduction of |
In issue #5321 someone again stumbled over this. But I noticed that since #3772 (merged two weeks ago) There might be corner cases where the elaborator manages to type-check the terms without reducing the wf term, but then the kernel tries to, but I’d like to see evidence of that before jumping into action. Hence lowering the priority of this to |
A long standing issue is that the kernel will happily try to reduce functions defined by well-founded recursion (#2171), and then this often fails with
(kernel) deep recursion detected
.This leads to highly unpredictable behavior:
It would be good to fix this. In #4061 we made functions defined by well-founded recursion irreducible by default, but this only affects the elaborator, not the kernel.
It might thus be desirable to prevent the kernel from unfolding these functions as well.
This can be achieved without changes to the kernel by inserting an
opaque
identity function into the definition ofWellFounded.fix
, as shown in #5182. This fixes the above issue.One downside is that
unseal f in
doesn’t help anymore to make the function reduce after all. In #5182 and its mathlib adaption branch I could replace many rfl-proofs that neededunseal f
by suitable proofs by rewriting (by simp [f]
or variants). But two applications remain in mathlib where kernel-reduction of well-founded definitions are intentional and hard to replace:Nat.minFacAux finds the smallest factor of a number, and needs to be kernel reducible so that small prime numbers can be found.
Arguably it could be rewritten to use structural recursion (or have a fuel argument), but it’s not a trivial change.
The function
in Mathlib/CategoryTheory/Filtered/Small.lean also seems really need well-founded recursion and kernel reduction.
Here is one possible way forward:
WellFounded.fix
andWellFounded.reducibleFix
. The former is as in feat:WellFounded.fix
to be kernel-irreducible #5182 (no kernel reduction), the latter as it is now (kernel can reduce theAcc
argument).WellFounded.fix
and are thus irreducible.@[semireducible]
, then it is compiled usingWellFounded.reducibleFix
This way, special cases like the two above continue working, while in general well-founded definitions do not reduce, as desired.
Alternatives
Impact
Add 👍 to issues you consider important. If others benefit from the changes in this proposal being added, please ask them to add 👍 to it.
The text was updated successfully, but these errors were encountered: