Zulip Chat Archive

Stream: Type theory

Topic: Equality without axioms


Mirek Olšák (Feb 11 2026 at 14:19):

I was curious about this question (and thought it could be known). Assume, for some definitions a, b, we prove

theorem some_eq : a = b := ...

without assumptions, and even without the use of the standard three axioms (propext, Quot.sound, Classical.choice). Do then a, b must be definitionally equal? If not, what is a simple counterexample?

Kyod (Feb 11 2026 at 14:30):

In vanilla (intensional) Martin-Löf Type Theory (MLTT), a closed proof of propositional equality of two closed terms a and b indeed imply that a and b are definitionally equal (by canonicity).

This fail if you add functional extensionality (for instance in extensional MLTT), takinga := fun n : Nat => n and b := fun n : Nat => 0 + n that are equal by funext + induction but are not convertible.

I would expect Lean without axioms is quite close to intensional MLTT.

Mirek Olšák (Feb 11 2026 at 14:39):

But funext requires Quot.sound in Lean, right?

Mirek Olšák (Feb 11 2026 at 14:40):

I see, so judgemental equality without axioms likely implies definitional, thanks.

Robin Arnez (Feb 11 2026 at 14:40):

I think this statement is a consequence of "every closed term of an inductive type reduces to constructors" where the fact that the proof of some_eq reduces to Eq.refl _ implies that a = b holds definitionally. I'm pretty sure that new statement is true but Acc.rec might get in the way, not sure.

Mirek Olšák (Feb 11 2026 at 14:43):

Which is the "new statement"?

Mirek Olšák (Feb 11 2026 at 14:47):

I was thinking of transfinite constructions but they require building functions, so without functional extensionality, it is hard to get somewhere...

Robin Arnez (Feb 11 2026 at 15:03):

I mean "every closed term of an inductive type reduces to constructors" is probably true without axioms and that implies your statement

Kevin Buzzard (Feb 11 2026 at 18:06):

theorem foo (f g : Unit -> Empty) : f = g := by
  -- rfl -- fails
  cases (f .unit)

#print axioms foo -- 'foo' does not depend on any axioms

Robin Arnez (Feb 11 2026 at 18:08):

That has assumptions though

Kevin Buzzard (Feb 11 2026 at 18:09):

Oh apologies, I must have not understood the question :-(

Mirek Olšák (Feb 11 2026 at 18:22):

It is an interesting example anyway. It shows that opaque can introduce such examples too

opaque a : Unit := ()
opaque b : Unit := ()

example : a = b := by
  cases a
  cases b
  rfl

#print axioms some_eq -- 'some_eq' does not depend on any axioms

Aaron Liu (Feb 11 2026 at 18:23):

This one is also just rfl

Aaron Liu (Feb 11 2026 at 18:23):

because of unit-eta

Mirek Olšák (Feb 11 2026 at 18:24):

Doesn't work for me...

example : a = b := rfl -- Type mismatch
example : a = b := by rfl' -- Tactic `rfl` failed

Mirek Olšák (Feb 11 2026 at 18:25):

Although decide works...

Aaron Liu (Feb 11 2026 at 18:29):

opaque a : Unit := ()
opaque b : Unit := ()

example : a = b := Eq.refl ()

Robin Arnez (Feb 11 2026 at 18:30):

This is very strange

example (a b : Unit) : a = b := rfl

works but

opaque a : Unit := ()
opaque b : Unit := ()

example : a = b := rfl

doesn't and

example : a = b := Eq.refl ()

does?!

Robin Arnez (Feb 11 2026 at 18:30):

This seems like a bug

Robin Arnez (Feb 11 2026 at 18:32):

At least the kernel has no problem

import Lean

opaque a : Unit := ()
opaque b : Unit := ()

open Lean Elab Tactic
theorem th : a = b := by
  run_tac
    ( getMainGoal).assign (mkApp2 (.const ``rfl [1]) (mkConst ``Unit) (mkConst `a))

Mirek Olšák (Feb 11 2026 at 22:09):

By the way, I realized there is a cleaner way of using opaque to break definitional equality

structure EqPair where
  a : Nat
  b : Nat
  eq : a = b

opaque eqPair : EqPair := 0, 0, rfl

def a := eqPair.a
def b := eqPair.b

theorem some_eq : a = b := eqPair.eq -- not definitionally equal
#print axioms some_eq

it feels a bit cheap though... What was the idea with Acc.rec?

Aaron Liu (Feb 11 2026 at 22:20):

Lean's definitional equality algorithm doesn't respect substitution:

theorem foo : True  True := trivial, trivial

/--
error: Type mismatch
  Eq.refl 0
has type
  0 = 0
but is expected to have type
  And.rec (fun x x_1 => 0) foo = 0
-/
#guard_msgs in
example : foo.rec (fun _ _ => 0) = 0 :=
  Eq.refl 0

example : foo.rec (fun _ _ => 0) = 0 :=
  congrArg (fun p => (p.rec (fun _ _ => 0) : Nat)) (proof_irrel foo trivial, trivial)

Aaron Liu (Feb 11 2026 at 22:25):

see also lean4#3213

Mirek Olšák (Feb 11 2026 at 22:37):

This one seems kernel defeq.

theorem foo : True  True := trivial, trivial
example : foo.rec (fun _ _ => 0) = 0 := by rfl'

Robin Arnez (Feb 11 2026 at 22:47):

... because the kernel has no trouble unfolding the theorem. If you use opaque, then the kernel fails too:

import Lean

opaque foo : True  True := trivial, trivial
example : foo.rec (fun _ _ => false) = false := by
  run_tac
    ( Lean.Elab.Tactic.getMainGoal).assign Lean.reflBoolFalse

James E Hanson (Feb 11 2026 at 23:05):

Mirek Olšák said:

What was the idea with Acc.rec?

Something like this?

def f (n : Nat) (p : Acc (· < ·) n) : Bool :=
  match p with
  | Acc.intro n h => match n with
                      | 0          => true
                      | Nat.succ m => f m (h m Nat.le.refl)

def inv' {α : Sort u} {R : α  α  Prop} (x : α) (h : Acc R x) (y : α) : R y x  Acc R y :=
  Acc.inv h

opaque p : Acc (· < ·) 1 := Nat.lt_wfRel.2.1 _

theorem eq_0 : f 1 p = f 1 (Acc.intro 1 (inv' 1 p)) := rfl
theorem eq_1 : f 1 (Acc.intro 1 (inv' 1 p)) = f 0 (inv' 1 p 0 Nat.le.refl) := rfl

theorem eq_2 : f 1 p = f 0 (inv' 1 p 0 Nat.le.refl) := Eq.trans eq_0 eq_1

/-- info: 'eq_2' does not depend on any axioms -/
#guard_msgs in
#print axioms eq_2

/--
error: Type mismatch
  rfl
has type
  ?m.14 = ?m.14
but is expected to have type
  f 1 p = f 0 ⋯
-/
#guard_msgs in
#check (rfl : f 1 p = f 0 (inv' 1 p 0 Nat.le.refl))

This is based on one of the two examples of the failure of transitivity of definitional equality in Mario's thesis. Mario helped me adapt it to Lean 4 a while ago. I feel like I was able to clean it up further at some point, but I can't find it right now.

This does need p to be opaque.

James E Hanson (Feb 11 2026 at 23:38):

Ah okay here's an example with no opaques:

def f (n : Nat) (p : Acc (· < ·) n) : Bool :=
  match p with
  | Acc.intro n h => match n with
                      | 0          => true
                      | Nat.succ m => f m (h m Nat.le.refl)

def inv' {α : Sort u} {R : α  α  Prop} (x : α) (h : Acc R x) (y : α) : R y x  Acc R y :=
  Acc.inv h

theorem eq_0 : (fun p  f 1 p) = (fun p  f 1 (Acc.intro 1 (inv' 1 p))) := rfl
theorem eq_1 : (fun p  f 1 (Acc.intro 1 (inv' 1 p))) = (fun p  f 0 (inv' 1 p 0 Nat.le.refl)) := rfl

theorem eq_2 : (fun p  f 1 p) = (fun p  f 0 (inv' 1 p 0 Nat.le.refl)) := Eq.trans eq_0 eq_1

/-- info: 'eq_2' does not depend on any axioms -/
#guard_msgs in
#print axioms eq_2

/--
error: Type mismatch
  rfl
has type
  ?m.24 = ?m.24
but is expected to have type
  (fun p => f 1) = ?m.19
-/
#guard_msgs in
#check (rfl : (fun p  f 1) = (fun p  f 0 (inv' 1 p 0 Nat.le.refl)))

Kevin Buzzard (Feb 11 2026 at 23:40):

(There's a typo f 1 -> f 1 pin the last line I think but I suspect that this doesn't change your point)

Arthur Adjedj (Feb 11 2026 at 23:40):

Robin Arnez said:

... because the kernel has no trouble unfolding the theorem. If you use opaque, then the kernel fails too:

import Lean

opaque foo : True  True := trivial, trivial
example : foo.rec (fun _ _ => false) = false := by
  run_tac
    ( Lean.Elab.Tactic.getMainGoal).assign Lean.reflBoolFalse

This is an instance of lean4#3213. As said previously, the fact that propositionally equal terms in an empty context are definitionally equal should hold in Lean’s meta-theory (as in, with a judgemental definitional equality, not the existing algorithm implemented), but in practice that’s not really the case for a few reasons as pointed out here, namely that the kernel has a few completeness issues making some "obvious" examples fail, that defeq is undecidable in general, and that lean adds a lot of axioms to the context in practice.

Kevin Buzzard (Feb 11 2026 at 23:43):

So James' example is exposing the difference between "definitionally equal in Lean's metatheory" and "provable by rfl".

James E Hanson (Feb 11 2026 at 23:51):

Here's a simpler example:

def X : Prop := Quot (fun _ _  True : True  True  Prop)

def a : X := Quot.mk _ trivial

def g : X  Nat := Quot.lift (fun _  0) (fun _ _ _  rfl)

theorem eq_0 : (fun x  g x) = (fun _  g a) := rfl

theorem eq_1 : (fun (_ : X)  g a) = (fun _  0) := rfl

theorem eq_2 : (fun x  g x) = (fun _  0) := Eq.trans eq_0 eq_1

/-- info: 'eq_2' does not depend on any axioms -/
#guard_msgs in
#print axioms eq_2

/--
error: Type mismatch
  rfl
has type
  ?m.9 = ?m.9
but is expected to have type
  (fun x => g x) = fun x => 0
-/
#guard_msgs in
#check (rfl : (fun x  g x) = (fun _  0))

Arthur Adjedj (Feb 12 2026 at 00:16):

Kevin Buzzard said:

So James' example is exposing the difference between "definitionally equal in Lean's metatheory" and "provable by rfl".

It looks like it, yes. Namely, since Acc can be eliminated to Type, its recursor can get stuck in places where another proof of accessibility (that would be defeq to the other thanks to proof-irrel) wouldn't, producing relevant terms that "get stuck" in places they shouldn't. Since Acc is not a structure, lean can't simply eta-expand the term in such cases to makes the recursor progress nonetheless. In systems without definitional proof-irrelevance like Rocq, eq_0 wouldn't work:

Require Import Coq.Arith.Le.

Fixpoint f (n : nat) (p : Acc (fun n1 n2 => n1 < n2) n) : bool :=
  match p with
    | Acc_intro _ h => match n as m return (n = m -> bool) with
      | O => fun _ => true
      | S m => fun h2 =>  f m (h m ltac:(rewrite h2; apply le_refl))
      end eq_refl
  end.

Fail Definition eq_0 : (fun p => f 1 p) = (fun p => f 1 (Acc_intro 1 (@Acc_inv _ _ 1 p))) := eq_refl.

Definition eq_1 : (fun p => f 1 (Acc_intro 1 (@Acc_inv _ _ 1 p))) = (fun p => f 0 (@Acc_inv _ _ 1 p 0 (le_refl _))) := eq_refl.

Arthur Adjedj (Feb 12 2026 at 00:31):

James E Hanson said:

Here's a simpler example:

def X : Prop := Quot (fun _ _  True : True  True  Prop)

def a : X := Quot.mk _ trivial

def g : X  Nat := Quot.lift (fun _  0) (fun _ _ _  rfl)

theorem eq_0 : (fun x  g x) = (fun _  g a) := rfl

theorem eq_1 : (fun (_ : X)  g a) = (fun _  0) := rfl

theorem eq_2 : (fun x  g x) = (fun _  0) := Eq.trans eq_0 eq_1

/-- info: 'eq_2' does not depend on any axioms -/
#guard_msgs in
#print axioms eq_2

/--
error: Type mismatch
  rfl
has type
  ?m.9 = ?m.9
but is expected to have type
  (fun x => g x) = fun x => 0
-/
#guard_msgs in
#check (rfl : (fun x  g x) = (fun _  0))

This is caused by the fact that you're allowed to quotient over Props, I don't see why it is like this, but I feel it shouldn't.

James E Hanson (Feb 12 2026 at 01:53):

Kevin Buzzard said:

So James' example is exposing the difference between "definitionally equal in Lean's metatheory" and "provable by rfl".

Lean's source code doesn't seem to agree with you about what the phrase 'definitional equality' means:

import Qq

open Qq

def X : Prop := Quot (fun _ _  True : True  True  Prop)

def g : X  Nat := Quot.lift (fun _  0) (fun _ _ _  rfl)

/-- info: false -/
#guard_msgs in
#eval Lean.Meta.isDefEq q(fun x  g x) q(fun _  0 : X  Nat)

François G. Dorais (Feb 12 2026 at 02:44):

Could this quirk be about the old way of making IO work? It used to be that IO.RealWorld was just Unit. This was (correctly) changed in lean4#9631 but there may be leftover workarounds that are not useful anymore.

James E Hanson (Feb 12 2026 at 06:34):

François G. Dorais said:

Could this quirk be about the old way of making IO work? It used to be that IO.RealWorld was just Unit. This was (correctly) changed in lean4#9631 but there may be leftover workarounds that are not useful anymore.

Which quirk? The fact that those terms are not definitionally equal?

Miyahara Kō (Feb 12 2026 at 10:29):

Not Meta.isDefEq but Kernel.isDefEq:

import Qq

open Qq

def X : Prop := Quot (fun _ _  True : True  True  Prop)

def g : X  Nat := Quot.lift (fun _  0) (fun _ _ _  rfl)

/- true -/
#eval do
  let a := Lean.Kernel.isDefEq ( Lean.getEnv) Lean.LocalContext.empty q(fun x  g x) q(fun _  0 : X  Nat)
  return a.toBool

Robin Arnez (Feb 12 2026 at 10:39):

Well, the kernel not producing an error doesn't mean that it thinks that they are defeq:

import Qq

open Qq

def X : Prop := Quot (fun _ _  True : True  True  Prop)

def g : X  Nat := Quot.lift (fun _  0) (fun _ _ _  rfl)

/-- info: false -/
#guard_msgs in
#eval do
  let a := Lean.Kernel.isDefEq ( Lean.getEnv) Lean.LocalContext.empty q(fun x  g x) q(fun _  0 : X  Nat)
  match a with
  | .error e => Lean.throwKernelException e
  | .ok b => return b

Miyahara Kō (Feb 12 2026 at 10:40):

Oh, sorry:disappointed:.


Last updated: Feb 28 2026 at 14:05 UTC