Zulip Chat Archive
Stream: Type theory
Topic: Iota-reduction and rfl issues
Arthur Adjedj (Nov 11 2023 at 12:36):
Hi, while trying to break Lean, I found some inconsistencies with regards to the reduction of Prop
-recursors. I have come to understand that such recursors simply don't reduce, in order to avoid an undecidable type-checking due to i.e the elimination of Acc
. In practice however, the reduction of such recursors seems inconsistent ? See, in the following code:
def tautext {A B : Prop} (a : A) (b : B)
: A = B := propext (Iff.intro (λ _ => b) (λ _ => a))
def True' : Prop := ∀ A : Prop, A → A
def delta : True' → True' := λ z : True' => z _ id z
def omega : True' := λ _ a => cast (tautext id a) delta
def Omega : True' := delta omega --non-terminating term
def f (h : True ∧ True) : Nat := And.rec (motive := fun _ => Nat) (fun _ _ => 1) h
example : f ⟨.intro,.intro⟩ = 1 := rfl --recursor reduces here
#reduce f (Omega _ ⟨.intro,.intro⟩) --infinite recursion, maxRecDepth error
--despite that, a type mismatch error still appears instead of an infinite loop during defeq
example : f (Omega _ ⟨.intro,.intro⟩) = 1 := rfl
Here,And.rec
reduces, and even leads to an infinite reduction thanks to Omega
. However, when checking for definitional equality, the term doesn't get reduced.
Weirdly enough, if I add an implicit binder to True'
's forall, reduction doesn't trigger on f (@Omega _ ⟨.intro,.intro⟩)
, as one would expect:
def tautext {A B : Prop} (a : A) (b : B)
: A = B := propext (Iff.intro (λ _ => b) (λ _ => a))
def True' : Prop := ∀ {A : Prop}, A → A -- A is now implicit, only change made here
def delta : True' → True' := λ z : True' => z id z
def omega : True' := λ a => cast (tautext id a) delta
def Omega : True' := delta omega --non-terminating term
def f (h : True ∧ True) : Nat := And.rec (motive := fun _ => Nat) (fun _ _ => 1) h
#reduce f (Omega ⟨.intro,.intro⟩) -- And.rec (fun x x => 1) (_ : True ∧ True)
Any idea what's going on here ?
Arthur Adjedj (Nov 11 2023 at 13:30):
Nevermind, it turns out that #reduce
tries to do a full normal form, thus reducing Omega _ ⟨.intro,.intro⟩
even though the recursor doesn't reduce, though i'm still confused as to why f ⟨.intro,.intro⟩ = 1 := rfl
would type-check here.
Mario Carneiro (Nov 12 2023 at 07:26):
I don't think anything other than #reduce
reduces proofs. And.rec f h
reduces to f h.1 h.2
by eta for structures IIRC
Arthur Adjedj (Nov 12 2023 at 08:55):
As discussed here, eta for structures isn't triggered during the reduction of recursors that are in Prop
, so I believe this isn't supposed to type-check, even though it does:
example : f ⟨.intro,.intro⟩ = 1 := rfl --works
Furthermore, I have found what I believe to be an inconsistency between the kernel reduction in the kernel and the elaborator. It's nothing dangerous, but it shows (to me) that reduction with Prop-recursors can have inconsistent behaviours:
import Lean
open Lean Meta Elab Term Command
def whnfKernel (e : Expr) : MetaM Expr := do
let env ← getEnv
let lctx ← getLCtx
match Kernel.whnf env lctx e with
| .ok e => return e
| .error e => throwKernelException e
elab "#whnf" t:term : command => runTermElabM fun _xs => do
let t ← whnf (← elabTermAndSynthesize t none)
logInfo t
elab "#whnf_kernel" t:term : command => runTermElabM fun _xs => do
let t ← whnfKernel (← elabTermAndSynthesize t none)
logInfo t
def tautext {A B : Prop} (a : A) (b : B)
: A = B := propext (Iff.intro (λ _ => b) (λ _ => a))
def True' : Prop := ∀ A : Prop, A → A
def delta : True' → True' := λ z : True' => z _ id z
def omega : True' := λ _ a => cast (tautext id a) delta
def Omega : True' := delta omega --non-terminating term
def f (h : True ∧ True) : Nat := And.rec (motive := fun _ => Nat) (fun _ _ => 1) h
example : f (id ⟨.intro,.intro⟩) = 1 := rfl --recursor reduces here
#whnf f (id ⟨.intro,.intro⟩)
#whnf f (Omega (True ∧ True) ⟨.intro,.intro⟩) --doesn't reduce
#whnf_kernel f (Omega (True ∧ True) ⟨.intro,.intro⟩) --infinite loop
Knowing that the kernel's isDefEq
may be called instead of the elaborator's in certain cases for proofs by reflection, perhaps it would be best to have consistent results between the two.
Arthur Adjedj (Nov 12 2023 at 09:02):
Because of this, conversion is still undecidable, at least in the kernel, and I believe it would be possible to make this leak out in the elaborator thanks to proofs by reflection:
elab "#is_def_eq_kernel" t₁:term "#" t₂:term "#" : command => runTermElabM fun _xs => do
let t₁ ← elabTermAndSynthesize t₁ none
let t₂ ← elabTermAndSynthesize t₂ none
let isDefEq ← isDefEqKernel t₁ t₂
logInfo s!"{isDefEq}"
#is_def_eq_kernel (f (Omega (True ∧ True) ⟨.intro,.intro⟩)) # 1 # --infinite loop
Arthur Adjedj (Nov 12 2023 at 09:06):
Here is Lean looping indefinitely at the elaborator level:
def tautext {A B : Prop} (a : A) (b : B)
: A = B := propext (Iff.intro (λ _ => b) (λ _ => a))
def True' : Prop := ∀ A : Prop, A → A
def delta : True' → True' := λ z : True' => z _ id z
def omega : True' := λ _ a => cast (tautext id a) delta
def Omega : True' := delta omega --non-terminating term
def f (h : True ∧ True) : Nat := And.rec (motive := fun _ => Nat) (fun _ _ => 1) h
example : f (id ⟨.intro,.intro⟩) = 1 := rfl --recursor reduces here
example : (f (Omega _ ⟨.intro,.intro⟩)) = 1 := by rfl --infinite loop
This happens because the rfl
tactic uses the kernel's isDefEq
when checking the conversion of two closed terms with no metavariables (link)
Wojciech Nawrocki (Nov 12 2023 at 19:37):
Here is another one in Lean 3: lean#91
Last updated: Dec 20 2023 at 11:08 UTC