Zulip Chat Archive
Stream: lean4
Topic: About `Nat.all`/`Nat.any`
François G. Dorais (Jul 23 2025 at 19:09):
The following came about in a budding side project for UnicodeBasic that aims to prove/verify relationships between Unicode properties. Since the Unicode standard is basically just a bunch of tables, these properties can only be checked by exhaustion. There are over 1.1 million code points to check so this is not a trivial task!
My plan is that for user-facing code I will replace these exhaustive tests by a custom axiom Unicode.checked that works like sorry. I still need to run the actual checks on a regular basis to justify the claims made by this axiom. This is where Nat.all/Nat.any come into play and I ran into some issues and questions along the way...
1. Reduction
The kernel currently reduces Nat.all/Nat.any. This is fine for Nat.all 17 _, say, but it inevitably leads to deep recursion for Nat.all 1114112 _. In proofs, this makes it impossible to use some tactics that attempt to reduce such hypotheses.
My current workaround is to use Nat.all_eq_finRange_all/Nat.any_eq_finRange_any as early as possible but that doesn't resolve all the deep recursion issues. Locally marking Nat.all/Nat.any as irreducible helps a bit but doesn't fully solve the problem.
Should
Nat.all/Nat.anybe marked irreducible? Perhaps with a simproc to only reduce reasonably small cases? Or is there a better workaround?
2. Definition
The current definition for Nat.all is this:
@[specialize] def all : (n : Nat) → (f : (i : Nat) → i < n → Bool) → Bool
| 0, f => true
| succ n, f => all n (fun i h => f i (by omega)) && f n (by omega)
This is not tail recursive but in the same file there is a tail recursive Nat.allTR which is csimp for Nat.all.
However, this tiny variant is already tail recursive:
@[specialize] def allRev : (n : Nat) → (f : (i : Nat) → i < n → Bool) → Bool
| 0, f => true
| succ n, f => f n (by omega) && allRev n (fun i h => f i (by omega))
The explanation is that while && is propositionally commutative, it is not definitionally commutative. The two definitions are provably equal but not definitionally equal.
Should
Nat.allbe replaced byNat.allRevto avoid the csimp?
(The same holds for Nat.any with && replaced by || and so on...)
3. Compilation
While Nat.all and Nat.allRev from above are provably equal, they compile differently.
For example: Nat.all 4 p compiles essentially the same way as:
(((true && p 0) && p 1) && p 2) && p 3
But Nat.allRev p compiles essentially the same way as:
p 3 && (p 2 && (p 1 && (p 0 && true)))
These are different. If p 0 = true, p 1 = false, p 2 = false, p 3 = true then:
Nat.all 4 pevaluatesp 0andp 1but doesn't evaluatep 2andp 3.Nat.allRev 4 pevaluatesp 3andp 2but doesn't evaluatep 0andp 1.
This is not a serious concern unless p is not really pure. For example, if p is partial and p 3 is undefined but p 0, p 1, p 2 are all defined as above, then Nat.all 4 p is defined but Nat.allRev 4 p is undefined. (Same for Nat.any with && replaced by || and so on...)
Does any code out there rely on the order of evaluation for
Nat.all/Nat.any? A quick check suggests that all code in core doesn't depend on evaluation order forNat.all/Nat.any.
Kim Morrison (Jul 25 2025 at 03:20):
I would be happy to have Nat.allRev and anyRev in addition, but I think the runtime behaviours shouldn't be changed.
Robin Arnez (Jul 25 2025 at 08:07):
What about using Nat.allRev / Nat.anyRev for kernel behavior and then csimping to something with the current runtime behavior?
Kim Morrison (Jul 25 2025 at 08:29):
That seems very confusing. Just let people specify what behaviour they want by calling the relevant function.
Robin Arnez (Jul 25 2025 at 10:29):
Hmm yeah it seems like the kernel can't really handle either of Nat.all / Nat.allRev well for large numbers and the performance is also not significantly different so I guess there is no point in changing anything.
François G. Dorais (Jul 25 2025 at 15:58):
Those are fair assessments: the *Rev versions are just barely more efficient than the *TR versions. It's not really worth adding them without a specific need.
That resolves 2 and 3, but not 1. Is there a smart way to prevent deep recursion with Nat.all/Nat.any?
Robin Arnez (Jul 25 2025 at 17:20):
How quickly do you get into such a situation? Can you provide an #mwe?
François G. Dorais (Jul 25 2025 at 20:19):
Not quite a #mwe but this is a real life example (with no imports required) extracted from batteries#1344:
/-- Returns `true` if `p` returns true for every `Char`. -/
protected def Char.all (p : Char → Bool) : Bool :=
Nat.all 1114112 fun c h =>
if h₁ : c < 55296 then
p <| Char.ofNatAux c <| .inl h₁
else if h₂ : 57343 < c then
p <| Char.ofNatAux c <| .inr ⟨h₂, h⟩
else
true
theorem Char.exists_eq_false_of_all_eq_false (h : Char.all p = false) :
∃ c, p c = false := by
simp only [Char.all, Nat.all_eq_finRange_all, List.all_eq_false] at h
match h with
| ⟨⟨n, hn⟩, ⟨_, h⟩⟩ =>
simp only [Bool.eq_false_iff]
split at h
· refine ⟨ofNatAux n (.inl ?_), h⟩; assumption
· split at h
· refine ⟨ofNatAux n (.inr ⟨?_, hn⟩), h⟩; assumption
· simp at h
This better illustrates my workarounds than the actual problem.
A quick example is on the last line, h: ¬true = true so contradiction was my go-to but it hangs (this is probably an issue with contradiction itself since simp_all does a lot better).
Before finding this working proof, I ran into other deep recursion issues until I realized that Nat.all_eq_finRange_all really helps. I'd have to go back in time to figure out what issues I ran into before then.
Last updated: Dec 20 2025 at 21:32 UTC