Dependent Types from Rocq to Lean 4

A mathematical refresher on constructions, computation, and proof

Suppose you want to state that every finite-dimensional vector space has a basis. On paper, the dimension, the vector space, the basis vectors, and the proof that they form a basis coexist comfortably in one sentence. A formal language must explain their dependencies. A basis with n vectors is a different kind of object from a basis with m vectors; the assertion that these vectors span refers to the particular vectors; and a construction of a basis differs from an assertion that one exists.

Dependent type theory gives these dependencies a uniform notation. Types may depend on values. Proofs inhabit types expressing propositions. Functions can return both mathematical objects and evidence about them. The interesting work begins when we ask which distinctions the system preserves, which it identifies automatically, and which require a theorem.

This article offers a route back into the subject for someone who once knew Coq, now called Rocq, and a route into Lean for a mathematician new to dependent types. The common foundation is the Calculus of Constructions and its extensions with inductive types. Lean and Rocq make different choices within that family; understanding those choices explains much of Lean’s behavior. Programmers who know GADTs from Haskell or OCaml get a section of their own, which other readers can skip.

Milo Moses’s A critical look at Lean4 supplies the occasion. It raises questions about normalization, consistency, and the reliability of implementations. We will return to those after developing enough of the machinery to distinguish them. The mathematical exposition comes first: what a proof term is, how computation enters a type, and what happens when equality itself becomes an object of reasoning.

Families of types and dependent functions

A familiar function has a fixed codomain: f:A\to B. A dependent function has a codomain that varies with its input:

f:\prod_{x:A}B(x).

Here B assigns a type B(x) to each x:A. To give f is to give an element f(x):B(x) uniformly for each x. Geometrically, think of a family of fibers over A and a section choosing a point in each fiber. This analogy concerns the dependency alone; continuity or smoothness would be additional conditions.

For example, let B(n) be the type of tuples of length n. A function that produces a zero tuple for every length has type \prod_{n:\mathbb N}B(n). Its result type records the length it was asked for. A function returning a list and a separate promise about its length carries similar information in a different representation.

Lean writes a dependent function type as (x : A) → B x. Application is juxtaposition, and fun x => t is function abstraction. Curly braces mark arguments that Lean should usually infer. These are the basic conventions of its dependent type language.

Here is dependent composition. The result of g depends on the value that f produces:

universe u v w

def dependentCompose {A : Type u} {B : Type v}
    {C : B → Type w} (f : A → B) (g : (b : B) → C b) :
    (a : A) → C (f a) :=
  fun a => g (f a)

Read the signature before the body. C is a family over B; g selects an element in each fiber; composing with f produces a section of the family pulled back to A. The body is ordinary composition. The type records the dependency that ordinary function notation suppresses.

Typing is expressed by a judgment, conventionally

\Gamma\vdash t:T.

It says that, under the declarations collected in the context \Gamma, the term t has type T. A context might contain A : Type, x : A, and P : A → Prop. The order matters: a declaration can depend on earlier declarations.

The colon here belongs to the language of judgments. By contrast, a proposition x ∈ S is itself a type whose inhabitants are proofs. Thus “this term has the wrong type” and “this mathematical proposition is false” are different claims.

Three rules explain dependent functions. Suppressing universe side conditions for the moment, they have the following shapes:

\frac{\Gamma\vdash A\ \mathrm{type}\qquad\Gamma,x:A\vdash B(x)\ \mathrm{type}} {\Gamma\vdash\prod_{x:A}B(x)\ \mathrm{type}}

\frac{\Gamma,x:A\vdash t:B(x)} {\Gamma\vdash(\lambda x:A.\,t):\prod_{x:A}B(x)}

\frac{\Gamma\vdash f:\prod_{x:A}B(x)\qquad\Gamma\vdash a:A} {\Gamma\vdash f\,a:B(a)}.

The first forms the function type, the second constructs a function, and the third applies it. The substitution in the final result type is the dependent part: applying g to f a produces a term of C (f a), exactly as in the example. The accompanying computation rule is beta reduction, (\lambda x.\,t)\,a\longrightarrow t[a/x], with substitution avoiding accidental capture of variables.

Why implication and universal quantification are function types

A proof of P\to Q transforms any proof of P into a proof of Q: it is a function. A proof of \forall x:A,\,P(x) supplies a proof of P(x) for every x: it is a dependent function. This is the propositions-as-types correspondence.

Syntax note. Lean’s ∀ (x : A), B x and (x : A) → B x are the same dependent function type.

The proof of an elementary implication is therefore a small program:

theorem composeImplications (P Q R : Prop)
    (hPQ : P → Q) (hQR : Q → R) : P → R :=
  fun hP => hQR (hPQ hP)

The same proof can be constructed interactively:

theorem composeImplicationsBy (P Q R : Prop)
    (hPQ : P → Q) (hQR : Q → R) : P → R := by
  intro hP
  apply hQR
  exact hPQ hP

After intro hP, the context contains a proof of P and the goal is R. Applying hQR reduces that goal to Q; the final line supplies its proof. The kernel checks the resulting proof term. See Lean’s propositions and proofs tutorial.

Other logical connectives have corresponding constructors and elimination rules:

Mathematical statement Evidence to construct it How to use the evidence
P\land Q A proof of each conjunct Project either proof
P\lor Q A chosen disjunct and its proof Reason by cases
\exists x:A,\,P(x) A witness and a proof about it Reason about an arbitrary such witness
\bot No constructor Derive any proposition from it

Negation is \neg P := P\to\bot: assume P and derive a contradiction. Classical logic adds principles such as double-negation elimination, \neg\neg P\to P, which the rules above do not provide.

Rocq’s forall x : A, P x becomes ∀ x : A, P x; fun x => t, intros, apply, and exact remain recognizable. The treatment of proofs and equality changes more substantially.

The Calculus of Constructions and its descendants

The Calculus of Constructions, developed by Thierry Coquand and Gérard Huet, brought together dependent types and powerful polymorphism in one typed lambda calculus. Their 1988 paper proves strong normalization: every reduction sequence from a well-typed term terminates. Extensions to the calculus require their own metatheoretic arguments.

For a returning type theorist, the lambda-cube description is a useful memory aid. Begin with simply typed functions. Add terms parameterized by types, type constructors parameterized by types, and types parameterized by terms. CoC combines all three forms of dependency. In the usual two-sort presentation there is a lower sort * and an upper sort \square, with *:\square. Dependent products permit all four combinations of domain and codomain sorts. There is no axiom \square:\square.

For the mathematician, the payoff is uniformity. The same substitution operation instantiates a polynomial at a number, a construction at a vector space, or a theorem at a hypothesis. Types and terms share a syntax, while typing rules prevent arbitrary self-reference.

Pure CoC does not begin with a primitive natural-number type and the induction principle used in ordinary mathematics. Polymorphism permits Church encodings: an encoded natural number can describe iteration. But obtaining the desired dependent induction and computational behavior motivates a richer system with primitive inductive definitions. The early history of Coq traces this development, including the work of Christine Paulin-Mohring.

Rocq’s core language is a version of the Calculus of Inductive Constructions, or CIC. It includes a hierarchy of universes and rules for inductive families, recursive definitions, and coinductive constructions. Its typing rules specify a much more substantial language than the original CoC.

Lean belongs to the same broad family, with its own universe discipline, definitional proof irrelevance, primitive quotients, and rules governing inductives. These choices affect both everyday proofs and the metatheory; we will examine each in turn.

Universes and the special role of propositions

Since types appear in expressions, they need types of their own. Lean uses a hierarchy:

\mathrm{Sort}\ u:\mathrm{Sort}\ (u+1).

Prop abbreviates Sort 0; Type u abbreviates Sort (u + 1); bare Type means Type 0. Consequently, Nat : Type and Type : Type 1. Universe variables such as u are level parameters, distinct from ordinary natural-number variables inside a program.

The levels constrain self-reference. In the usual expressive dependent calculi, simply imposing a universal Type : Type permits paradoxes. A hierarchy lets us quantify over a collection of types without placing that collection inside itself.

Lean’s universe rules can be compressed into one formula. If A:\mathrm{Sort}\ u and B(x):\mathrm{Sort}\ v, then

\left(\prod_{x:A}B(x)\right):\mathrm{Sort}(\operatorname{imax}(u,v)),

\operatorname{imax}(u,v)=\begin{cases}0&v=0,\\\max(u,v)&v>0.\end{cases}

Thus a proposition may quantify over objects in arbitrarily high fixed universes and remain a proposition. This is the impredicativity of Prop. For example, ∀ P : Prop, P → P is itself a proposition, although its quantifier ranges over all propositions, including that one.

Data universes are predicative. A type of functions ranging over all small types generally lives higher than those small types. Impredicativity and Type : Type are different design choices; the former does not by itself imply the latter.

One practical trap for a Rocq user is cumulativity. Rocq permits smaller universes to embed into larger ones through its typing discipline. Lean does not automatically regard a type in Type u as an inhabitant of Type (u + 1). Universe polymorphism often removes the need for lifting, but explicit constructions such as ULift are available when needed.

Rocq’s Set is a sort for small computational data, which Lean places in Type; Lean’s Set α is the type of predicates on α. The shared spelling conceals different roles.

Induction is dependent recursion

Natural numbers are specified by a zero constructor and a successor constructor. Their dependent eliminator has the mathematical shape

\begin{aligned} \operatorname{rec}_{\mathbb N}&: \prod_{C:\mathbb N\to\mathrm{Sort}\ u} C(0)\to{}\\ &\quad\left(\prod_{n:\mathbb N}C(n)\to C(n+1)\right)\to{}\\ &\quad\prod_{n:\mathbb N}C(n). \end{aligned}

The family C is called the motive. If C(n) is a proposition, this is induction. If it is a data type, this is a recursive construction. If it varies with n, the recursion constructs different types of results at different stages. The same principle accounts for all three readings. Lean’s induction and recursion chapter shows how recursive definitions are expressed through recursors.

An indexed inductive family makes the dependency visible:

inductive Vec (α : Type u) : Nat → Type u where
  | nil : Vec α 0
  | cons {n : Nat} : α → Vec α n → Vec α (n + 1)

def Vec.head {α : Type u} {n : Nat} : Vec α (n + 1) → α
  | .cons x _ => x

Arguments declared before a constructor’s colon stay in scope for the type that follows. The cons line could equally be written

| cons : {n : Nat} → α → Vec α n → Vec α (n + 1)

Either way, n is an implicit argument that Lean usually infers. The dependency comes from using n in the later types, not from where it is declared.

The element type α is a parameter: it stays fixed through the declaration. The length is an index: the constructors return different instances of the family. In head, the empty constructor cannot inhabit the input type, so there is no empty case to handle.

Compare List.head?, which must return an Option α because its input may be empty. Vec.head instead requires a nonempty vector, so its result needs no optional wrapper. The mathematical precondition is expressed in the domain of the function.

Inductive declarations require restrictions. In particular, recursive occurrences must meet strict positivity conditions. To understand the terminology, consider an occurrence of a type X in an expression built from function types. Start with positive polarity: entering an arrow’s domain flips the sign, while entering its codomain preserves it. Intuitively, A → X produces an X, whereas X → A consumes one. Assuming A and B contain no X:

Expression Position of X Strictly positive?
X Positive Yes
A → X Positive Yes
X → A Negative No
(X → A) → B Positive: two sign flips No

Strict positivity is stronger than a positive final sign: in these arrow expressions, the path to X must never enter a function’s domain. The check applies inside each constructor argument’s type, not across the outer arrows that make the constructor a function. Thus Vec α n is an allowed recursive argument to cons, while a recursive argument of type X → False would be forbidden. Nested inductive type constructors have additional rules, described in the inductive-type reference.

If we freely admitted a type Bad with a constructor mk : (Bad → False) → Bad, its apparent elimination rule would let us define:

d : Bad → False
d (mk f) := f (mk f)

The definition of d does not call itself: it applies the function stored in its argument. But the constructor lets us store d itself, as mk d : Bad. Applying d to it gives a closed term of type False. Unfolding d and matching the constructor returns the same term:

d (mk d) → d (mk d) → ...

Self-application emerges from the constructor and its elimination rule despite the absence of explicit recursion. The closed term’s type False is the contradiction; the loop also exposes the computational failure. The positivity check blocks the declaration of Bad before either becomes available.

Lean’s kernel checks inductive declarations and their recursors. Surface recursive definitions are compiled into applications of those principles, including well-founded recursion when needed. Rocq’s core language instead has primitive fixpoint terms, checked by a guardedness discipline; its recursors are defined from them. The two systems therefore organize the checking of recursion differently. Rocq’s inductive and recursive rules are the appropriate comparison, rather than the superficial similarity of pattern-matching syntax.

Aside: termination through decreasing measures

A standard termination argument assigns each recursive call a measure in a well-founded order. For a natural-number measure \mu, a call from input x to input y must satisfy \mu(y)<\mu(x). Recursing on a list’s tail decreases its length; more elaborate algorithms may decrease a lexicographically ordered tuple. There can be no infinite descending chain in the chosen order. Lean’s termination_by and decreasing_by let us specify such a measure and prove the required decreases, elaborating the definition into well-founded recursion.

Sized types carry descent information in types themselves. Schematically, write \mathrm{List}_{\leq n}(A) for lists of length at most n. A helper with type \mathrm{List}_{\leq n}(A)\to\mathrm{List}_{\leq n}(A) promises not to exceed that bound. Applied to the tail of a list of length n+1, its result is still small enough for a recursive call, even if it is not a literal subtree of the original input. Size information can cross function boundaries instead of being recovered by inspecting each helper’s implementation. Actual sized calculi have additional rules, and may use ordinal bounds; Abel’s MiniAgda paper develops this approach.

An ordinary recursive-call check has no self-call to inspect in d. It calls a stored function, whose unrestricted type Bad → False imposes no descent condition. Packaging d with mk lets that call return to the same expression. Any rank strictly decreasing along this complete reduction cycle would require \mu(t)<\mu(t). A termination-tracking system must therefore reject or restrict some part of the construction; merely attaching a measure to d cannot validate the unchanged rules.

Strict positivity restricts the datatypes and eliminators we may introduce; decreasing measures justify recursive uses of the admitted principles. Sized types can connect these concerns, but a size index alone proves neither termination nor consistency. Well-founded recursion even brings a difficulty of its own. Lean’s accessibility predicate Acc lives in Prop, and together with proof irrelevance it makes definitional equality undecidable; the section on normalization returns to this.

Computation and the two forms of equality

Dependent types make computation part of type checking. To see why, define an addition that explicitly recurses on its first argument:

def addLeft : Nat → Nat → Nat
  | 0, m => m
  | n + 1, m => Nat.succ (addLeft n m)

example (n : Nat) : addLeft 0 n = n := rfl

example asks Lean to check an unnamed theorem. The argument (n : Nat) is arbitrary; the proposition to prove follows :, and its proof follows :=.

rfl stands for reflexivity. In := rfl, it is a proof term with its argument inferred. The equality constructor Eq.refl n proves n = n and also works here. Writing := by rfl instead invokes the tactic of that name to construct the proof. Both use the same reflexivity principle; see Lean’s equality reference and tactic tutorial.

Reflexivity works even though the two sides look different: the defining equation for addLeft makes addLeft 0 n compute to n, even though n is unknown. The kernel’s conversion relation recognizes the two sides as definitionally equal, so a proof of n = n also proves the displayed equation. No induction lemma is needed.

Now consider the other order:

theorem addLeft_zero (n : Nat) : addLeft n 0 = n := by
  induction n with
  | zero => rfl
  | succ n ih =>
      change Nat.succ (addLeft n 0) = Nat.succ n
      exact congrArg Nat.succ ih

An unknown n does not reveal whether the first argument of addLeft is zero or a successor. Computation alone cannot finish. Induction does: the successor case lifts the induction hypothesis through the function Nat.succ.

Two relations are at work. Definitional equality belongs to the machinery of typing and conversion; it lets the checker treat suitable expressions interchangeably. Propositional equality, written a = b, is a type inside the language, and a proof inhabiting it can be passed to other functions.

The latter is an inductive family with reflexivity as its constructor. Its elimination principle says that equal things can be substituted in dependent families. This is the formal justification for transporting an object from B(a) to B(b) using a proof of a=b. Lean’s quantifiers and equality chapter develops this substitution principle.

def transport {A : Type u} (B : A → Type v)
    {a b : A} (h : a = b) (x : B a) : B b :=
  h ▸ x

def resizeVec {α : Type u} {n m : Nat}
    (h : n = m) (xs : Vec α n) : Vec α m :=
  transport (Vec α) h xs

Nothing about the vector’s entries changes. The equality proof justifies changing the index appearing in its type. This is why arithmetic identities can matter while manipulating a vector: a mathematically equal length may still require explicit transport if the two length expressions are not definitionally interchangeable.

Conversion is often explained as reducing both expressions to the same normal form. Lean’s type-system reference includes beta reduction of function applications, delta unfolding of definitions, iota reduction of recursors, and zeta substitution of let-bound values. It also includes function and structure eta rules, proof irrelevance, and quotient computation. The kernel implements an algorithmic conversion test; this is distinct from the declarative relation specified by the rules. The later normalization example will show why reducing every term to a normal form cannot provide a general implementation.

For writing proofs, a useful discipline is to ask what makes an equality available. Does it follow by unfolding and computation? Is it an induction theorem? Does it use extensionality? Each answer suggests a different Lean move: rfl or change, induction or rewriting, or an explicit extensionality principle.

A first independent Lean proof

We can now write a proof without a guide. The composite of two injective functions is injective; an explicit definition keeps every step visible:

def IsInjective {A : Type u} {B : Type v} (f : A → B) : Prop :=
  ∀ ⦃x y : A⦄, f x = f y → x = y

theorem injectiveComp {A : Type u} {B : Type v} {C : Type w}
    (f : A → B) (g : B → C)
    (hf : IsInjective f) (hg : IsInjective g) :
    IsInjective (fun x => g (f x)) := by
  intro x y h
  apply hf
  apply hg
  exact h

#print axioms injectiveComp

The double braces in ⦃x y : A⦄ make x and y strict implicit arguments. Lean infers them, but only once a later explicit argument, here the equality, is supplied. So hf on its own still denotes the whole quantified statement.

intro x y h introduces and names three local assumptions. Unfolding IsInjective exposes two universally quantified variables followed by an implication. After intro, the new assumptions and remaining goal are:

x y : A
h : g (f x) = g (f y)
⊢ x = y

Mathematically: let x, y be arbitrary and assume h. We could write intro a b hab and later exact hab. The remaining tactics work backward: apply hf asks for f x = f y, then apply hg asks for g (f x) = g (f y), supplied by exact h. The final command, #print axioms, lists the axioms a theorem depends on; for injectiveComp it reports none.

For practice, rewrite the proof as one fun expression. Prove that injectivity of g ∘ f implies injectivity of f, using congrArg g. Then try surjectivity: a composite of surjections is surjective, and if g ∘ f is surjective, so is g. Both statements end in an existential, so their proofs must supply witnesses.

Finally, return to Vec. Define Vec.toList and prove by induction that its result has length n. Then use resizeVec with Nat.zero_add n to turn a Vec α (0 + n) into a Vec α n. This brings together the three skills on which the rest depends: reading a dependent signature, understanding the motive of induction, and recognizing when equality requires transport.

How far GADTs take us

Readers who do not program in Haskell or OCaml can skip this section.

Much of the vector example is available in Haskell and OCaml through generalized algebraic datatypes, or GADTs. Their constructors can specify different result indices, and matching a constructor introduces type equations within the branch. This captures an important part of indexed inductive families. It does not by itself supply arbitrary dependence on ordinary terms.

Indexed vectors in Haskell and OCaml

In Haskell, enable GADTs, DataKinds, and KindSignatures:

data Nat = Z | S Nat

data Vec (n :: Nat) a where
  VNil  :: Vec 'Z a
  VCons :: a -> Vec n a -> Vec ('S n) a

headVec :: Vec ('S n) a -> a
headVec (VCons x _) = x

DataKinds promotes Nat to a kind of indices, with promoted constructors 'Z and 'S. The GADT declaration then constrains which indices each vector constructor produces. These are separate features: promotion supplies the index language; GADT matching supplies refinement. The GHC documentation explains the latter through typed expression trees, another useful indexed family.

OCaml can express the same nonempty-vector guarantee using separate types for zero and successor:

type z = Z
type 'n s = S of 'n

type (_, _) vec =
  | VNil : (z, 'a) vec
  | VCons : 'a * ('n, 'a) vec -> ('n s, 'a) vec

let head_vec : type n a. (n s, a) vec -> a = function
  | VCons (x, _) -> x

The type n a. annotation introduces locally abstract types for checking the body. Here z and 'n s serve as indices; no values of those types need to be stored in a vector. Both languages rule out the empty constructor at this input type. OCaml’s GADT tutorial also develops existential indices and equality witnesses; Haskell provides the latter as :~:. Matching such a witness transports a value between equal indices, as Lean’s transport does.

Connecting an index to an ordinary value

Lean can express replication with the signature (n : Nat) → α → Vec α n: the ordinary argument occurs in the result type. Haskell’s conventional encoding uses a singleton connecting a type-level index to its runtime representative:

data SNat (n :: Nat) where
  SZ :: SNat 'Z
  SS :: SNat n -> SNat ('S n)

replicateVec :: SNat n -> a -> Vec n a
replicateVec SZ     _ = VNil
replicateVec (SS n) x = VCons x (replicateVec n x)

data SomeVec a where
  SomeVec :: SNat n -> Vec n a -> SomeVec a

For each finite natural-number index, the singleton has exactly one fully defined constructor-shaped value. Matching it reveals the index’s structure. The existential package SomeVec lets a runtime computation return a vector of an initially unknown length. Unpacking exposes a fresh index shared by its singleton and vector. GADTs do not require every length to be a numeral known at compile time.

OCaml supports this singleton-and-package technique too. Its cost is maintaining the connection between an ordinary representation and an index. Libraries such as Haskell’s singletons automate parts of that work; full dependent types express the connection directly.

Computing with indices or carrying a relation

Haskell adds type families for computation in types. With TypeFamilies, concatenation can use a separate type-level addition:

type family Add (n :: Nat) (m :: Nat) :: Nat where
  Add 'Z     m = m
  Add ('S n) m = 'S (Add n m)

append :: Vec n a -> Vec m a -> Vec (Add n m) a
append VNil ys = ys
append (VCons x xs) ys = VCons x (append xs ys)

The equations for Add reduce as the vector match reveals n. Lean can instead use an ordinary addition function inside the result type. This difference concerns the language of indices, beyond the GADT feature itself. See GHC’s type-family rules.

An OCaml encoding can replace that function with its graph: a relation carrying evidence that three indices satisfy addition.

type (_, _, _) plus =
  | PlusZ : (z, 'm, 'm) plus
  | PlusS : ('n, 'm, 'k) plus -> ('n s, 'm, 'k s) plus

let rec append : type n m k a.
    (n, m, k) plus -> (n, a) vec -> (m, a) vec -> (k, a) vec =
  fun sum xs ys ->
    match sum, xs with
    | PlusZ, VNil -> ys
    | PlusS smaller, VCons (x, rest) ->
        VCons (x, append smaller rest ys)

PlusZ witnesses 0+m=m; PlusS lifts evidence for n+m=k to evidence for (n+1)+m=k+1. Both versions enforce the result length. The OCaml interface carries an explicit witness instead of asking the checker to evaluate a type family. Witnesses can also be computed and packaged with results.

Haskell consequently offers more direct machinery for computing with indices. Higher-kinded type variables and kind polymorphism also make families easier to abstract over. OCaml uses module functors or encodings for corresponding abstractions over type constructors; Yallop and White explain that tradeoff.

What general dependency adds

The deeper step is allowing a type to mention the actual data being processed. Vector induction in Lean admits a motive of the form

M:\prod_{n:\mathbb N}\operatorname{Vec}(A,n)\to\mathrm{Prop}.

The proposition can depend on the entire vector and its entries, not only its length. A GADT match exposes constructor and index equations, but does not automatically make the matched value available as an argument to a type-level predicate. More dependencies can be encoded by representing more data in indices. General dependent products and elimination remove the need to construct that representation separately for each domain.

Finally, expressive specifications are not yet a logic. Haskell accepts a definition fake :: p with body fake = fake, where p can be any ordinary type; OCaml likewise permits computations that diverge or raise exceptions at any desired result type. Neither computation produces an equality witness, for example. Lean’s later normalization counterexample will inhabit an already true proposition, without granting arbitrary proofs by unrestricted recursion. The rules controlling proofs matter independently of how richly a datatype can be indexed.

Proof irrelevance and what can be extracted from existence

Lean identifies all proofs of a fixed proposition definitionally:

example (P : Prop) (h₁ h₂ : P) : h₁ = h₂ := rfl

This says that once the proposition P is fixed, the identity of its proof is irrelevant to conversion. The reference on propositions marks this as a property of the type system itself.

A visible benefit is array access with a proof that the index is in bounds. Imagine an operation get xs i h, where h : i < xs.size. Two parts of a program may establish the same bound differently, supplying proofs h₁ and h₂. We want get xs i h₁ and get xs i h₂ to agree without reconciling those proofs. Lean’s array notation makes this concrete:

example {α : Type u} (xs : Array α) (h₁ h₂ : 0 < xs.size) :
    xs[0]'h₁ = xs[0]'h₂ := rfl

The apostrophe supplies the bounds proof explicitly. In ordinary xs[0] notation, Lean attempts to construct that proof automatically, so two accesses can look identical while carrying different evidence. Proof irrelevance makes these differing proof arguments interchangeable by conversion. A proof-relevant system might instead establish independence from them with a separate lemma.

Ordinary Rocq Prop does not have this general definitional identification. One can assume propositional proof irrelevance in Rocq, but an axiom proving h₁ = h₂ does not make the checker identify the terms directly. Modern Rocq also has SProp, a separate sort with definitional proof irrelevance and its own restrictions.

The distinction between a proof and data becomes concrete with existential quantification. Both of the following involve a witness, but they support different operations:

def WitnessData (α : Type u) (P : α → Prop) :=
  {x : α // P x}

theorem forgetWitness {α : Type u} {P : α → Prop}
    (w : WitnessData α P) : ∃ x, P x :=
  ⟨w.val, w.property⟩

noncomputable def chooseWitness {α : Type u} {P : α → Prop}
    (h : ∃ x, P x) : WitnessData α P :=
  ⟨Classical.choose h, Classical.choose_spec h⟩

In ∃ x, P x, the comma separates the bound variable x from the proposition P x; it does not construct a pair. Lean infers x : α from P : α → Prop. The fully annotated form is ∃ x : α, P x, which expands to Exists (fun x : α => P x). The angle brackets in forgetWitness supply the witness w.val and the proof w.property that it satisfies P.

The subtype {x : α // P x} is data: we can project its stored witness. The existential type ∃ x, P x belongs to Prop, so its inhabitants are proofs and obey Lean’s restrictions on eliminating propositions into data. In typing notation, (∃ x, P x) : Prop, while h : (∃ x, P x) says that h proves that proposition. The ordinary eliminator for existence lets us reason about a witness to establish another proposition, but does not generally expose the witness as computational data. The reverse direction above uses classical choice and is marked noncomputable.

Why impose this restriction? Suppose one existential proof used witness 0 and another used witness 1. Proof irrelevance identifies the proofs of that same existential proposition. A computational projection that distinguished them would make the identification incompatible with computation. Classical.choose instead selects an unspecified witness satisfying the predicate; it need not return the witness used to construct the proof. For a fixed predicate, changing the existential proof cannot change that selection.

More generally, \sum_{x:A}B(x), a dependent pair or sigma type, packages an object with data whose type depends on it. When the second component is a proposition, a subtype is often the convenient Lean representation. When both components carry data, use a sigma type. An existential proposition forgets the computational distinction between witnesses.

There are controlled exceptions to the prohibition on eliminating propositions into data. Empty propositions and suitable single-constructor propositions have stronger elimination rules; equality supports transport. Thus “nothing can ever move from Prop to Type” is too strong. The restriction concerns which distinctions a recursor is allowed to observe. Rocq also restricts elimination from Prop, even though ordinary Prop there lacks Lean’s general definitional proof irrelevance.

Extensional mathematics and primitive quotients

Mathematicians normally identify functions that agree at every argument. In Lean this is a theorem, funext, used as follows:

example {A : Type u} {B : Type v} (f g : A → B)
    (h : ∀ x, f x = g x) : f = g :=
  funext h

Pointwise equality need not be definitional equality of the functions. The theorem gives a propositional equality, which permits substitution without making their implementations convertible.

For reasoning about programs, distinguish a function from an implementation with a particular execution history. This distinction already matters without function extensionality: β-reduction identifies (fun y => y) x with x, although an operational semantics that counts β-steps distinguishes their evaluation costs. An equality that identifies these expressions cannot simultaneously preserve that step count. Ordinary function types do not expose source syntax or execution traces for inspection.

Function extensionality also identifies sorting functions that agree on every input despite different running times. Given e : f = g, any observer must satisfy observe f = observe g, by congrArg observe e; even a postulated cost observer cannot consistently distinguish them. To study costs, represent algorithms as syntax with an evaluator that counts steps, or instrument them to return a result and a cost, with type List Nat → List Nat × Nat. Equal sorted outputs need not imply equal result-cost pairs. Dependent types support these richer models; the chosen representation determines which distinctions survive.

Rocq’s standard library supplies function extensionality as an axiom, but omitting it does not restore the operational distinctions already lost through definitional equality.

Extensionality also affects computation involving equality proofs: transports using funext can block kernel reduction while compiled execution erases them. Lean’s Axioms and Computation illustrates this difference between evaluation routes.

Similarly, propositional extensionality, propext, turns an equivalence P ↔︎ Q into an equality P = Q.

In particular, any two proved propositions are equal. Given proofs a : A and b : B, the functions fun _ => b and fun _ => a prove the two implications: each ignores its input and returns the available proof of its conclusion.

Does this erase the distinctions between formulas? Consider three Boolean variables and the formulas

F=p\land q\land r,\qquad G=r\land p\land q,\qquad H=p\lor q\lor r.

A formula with free variables is not a proposition but a predicate on assignments, F : Assignment → Prop. F and G agree at every assignment, so propext and funext together give F = G. F and H disagree at (p,q,r)=(\mathrm{True},\mathrm{False},\mathrm{False}). They are merely equisatisfiable: each has a satisfying assignment. Then propext equates the propositions ∃ ρ, F ρ and ∃ ρ, H ρ, but not the predicates. Classically, Prop has two values up to propositional equality, yet three Boolean inputs permit 2^{2^3}=256 distinct truth functions.

Representing subsets by membership predicates, these principles turn pointwise equivalence into equality:

def SubsetOf (A : Type u) := A → Prop

theorem subsetExt {A : Type u} (S T : SubsetOf A)
    (h : ∀ x, S x ↔ T x) : S = T :=
  funext (fun x => propext (h x))

#print axioms subsetExt

The last command exposes the theorem’s axiom dependencies: propext and Quot.sound. Lean derives function extensionality from quotients, as we will see below.

Lean’s primitive Quot identifies representatives related by r propositionally. To define a function on the quotient, supply a function f on representatives and a proof h that it respects r. The computation rule is

\operatorname{Quot.lift}\ f\ h\ (\operatorname{Quot.mk}\ r\ a)\equiv f(a).

For example, quotient natural numbers by parity:

def SameParity (a b : Nat) : Prop := a % 2 = b % 2

def parityValue : Quot SameParity → Nat :=
  Quot.lift (fun n => n % 2) (fun _ _ h => h)

example : parityValue (Quot.mk SameParity 5) = 1 := rfl

example : Quot.mk SameParity 1 = Quot.mk SameParity 3 :=
  Quot.sound (by rfl)

Quotient soundness supplies the last equality without making its representatives definitionally identical. The library wrapper Quotient packages an equivalence relation with its laws; see the quotient reference.

Standard Rocq CIC has no corresponding primitive quotient. A common approach uses setoids: carriers with equivalence relations, and functions proved to respect them. Other quotient constructions require further assumptions or particular encodings.

From quotients to function extensionality

Let F=A\to B, and quotient it by pointwise equality:

f\sim g\quad:\!\iff\quad\forall x:A,\;f(x)=g(x),\qquad Q=F/{\sim}.

Given h:\forall x,\;f(x)=g(x), Quot.sound h supplies [f]=[g] in Q. How do we obtain equality back in F?

Fix an input x. Evaluation f\mapsto f(x) respects the relation: the required proof is simply h(x). Hence Quot.lift defines \operatorname{eval}_x:Q\to B with \operatorname{eval}_x([f])\equiv f(x). Assemble these evaluations into

\operatorname{rebuild}(q):=\lambda x.\operatorname{eval}_x(q).

The crucial calculation is definitional:

\operatorname{rebuild}([f]) \equiv\lambda x.\operatorname{eval}_x([f]) \equiv\lambda x.f(x) \equiv f.

The final step uses function η-equality: Lean identifies fun x => f x with f. This rule concerns the expansion of one function; it does not assume that arbitrary pointwise-equal functions are equal.

Apply ordinary congruence to [f]=[g]. It gives \operatorname{rebuild}([f])=\operatorname{rebuild}([g]), whose endpoints are definitionally f and g. Here is the derivation, generalized to dependent functions:

def Pointwise {A : Sort u} {B : A → Sort v}
    (f g : (x : A) → B x) : Prop :=
  ∀ x, f x = g x

def rebuild {A : Sort u} {B : A → Sort v}
    (q : Quot (@Pointwise A B)) : (x : A) → B x :=
  fun x =>
    Quot.lift (fun f => f x) (fun _ _ h => h x) q

theorem funextFromQuot {A : Sort u} {B : A → Sort v}
    (f g : (x : A) → B x)
    (h : ∀ x, f x = g x) : f = g :=
  congrArg rebuild (Quot.sound h)

#print axioms funextFromQuot

The reported dependency is only Quot.sound; quotient reduction and η-equality are built-in rules. This follows Lean’s implementation of funext.

The placement of the lambda avoids circularity. Lifting fun f => f directly would require proving f\sim g\Rightarrow f=g, our goal. Instead, we fix x, lift evaluation using h x, and then abstract over x. No representative is selected by choice: evaluation reconstructs a function, and quotient computation plus η makes reconstruction a left inverse of the quotient map.

Classical principles and conversion

Classical.choice, together with the extensional principles, proves excluded middle; Lean’s Classical.em follows Diaconescu’s argument. Local classical commands enable associated instances without modifying the kernel’s rules. Use #print axioms to inspect a theorem’s actual dependencies.

Lean therefore combines an intensional account of computation with strong propositional extensionality principles and definitional proof irrelevance. It does not implement equality reflection, the rule that every proved equality becomes a conversion rule. Nor is it a homotopy type theory: equality proofs live in its proof-irrelevant Prop, and the resulting uniqueness of equality proofs is incompatible with the general higher structure sought in univalent foundations.

What evolved from early Lean to Lean 4

Lean began in 2013; Lean 3 was released in 2017; Lean 4 development began in 2018, with version 4.0 released in September 2023. The official history describes the progression from a prover centered on C++ infrastructure to a system implemented largely in Lean itself.

There was also a foundational branch that is easy to forget. Lean 2 supported a separate HoTT mode with proof irrelevance disabled and univalence postulated. Sebastian Ullrich’s account of Lean’s development records that experiment. Modern Lean’s mainstream mathematical foundation follows the proof-irrelevant branch; its extensionality and quotient features should be understood in that setting.

Lean 3 already allowed tactics and other extensions to be written in Lean. Lean 4 substantially expanded this architecture through an extensible parser, hygienic macros, elaboration machinery, and a compiler intended to make Lean a useful programming language. The Lean 4 system paper, by de Moura and Ullrich, explains these engineering choices. They change how the prover can be extended and how efficiently proof search can be implemented.

The logical changes also matter. Carneiro’s 2024 Lean4Lean paper identifies kernel support for nested inductive types and eta for structures as changes that substantially affect the metatheory. Structure eta identifies a structure with the result of rebuilding it from its fields, just as function eta identifies fun x => f x with f. Such rules remove bookkeeping from proofs, but their interaction with other conversion rules needs justification.

A nested inductive type places recursion underneath another type constructor. For example, a rose tree contains a list of subtrees:

inductive Rose (α : Type u) where
  | node : α → List (Rose α) → Rose α

Here the recursive occurrence sits underneath List. Generating and checking the elimination machinery is more involved than for a binary tree. This raises soundness questions about the generated recursors even without invoking non-terminating reduction. Carneiro’s 2019 thesis proved Lean 3’s type theory consistent relative to ZFC with n inaccessible cardinals for every finite n. Lean 4’s extensions need further work beyond that argument.

The layers matter when learning the language. A numeral is surface syntax. Notation, implicit arguments, coercions, and type-class search help turn an expression into a fully specified term. Tactics solve elaboration goals by constructing terms. The kernel checks the resulting declarations. The compiler provides a further route from executable definitions to running programs. Lean’s elaboration and compilation reference separates these stages.

This explains why “I know the logic” and “I can use Lean fluently” are different achievements. A mathematician may understand a theorem perfectly while needing help with instance search or with the library’s preferred representation. A former Coq user may recognize the proof term while misreading a coercion or assuming the wrong transparency behavior.

A compact comparison helps locate those surprises:

Feature Rocq Lean 4
Shared ancestry CIC family CIC family with distinct extensions
Ordinary Prop Impredicative; no general definitional proof irrelevance Impredicative and definitionally proof irrelevant
Data universes Cumulative hierarchy Noncumulative hierarchy; universe polymorphism and lifts
General quotients Usually setoids or further constructions Primitive quotient support
Recursive definitions Core fixpoints subject to guardedness rules Surface recursion translated to recursors
Mathematical function extensionality An additional principle in ordinary CIC Derived using primitive quotients

Rocq also supports eta conversion for functions and primitive records. Its conversion rules specify these features beyond the broad comparison in the table.

What the normalization result actually says

We can now separate four metatheoretic properties. Strong normalization says every reduction sequence from a well-typed term terminates. Canonicity, in a specified fragment and with specified axioms, says closed terms of data types compute to appropriate canonical values, such as numerals. Consistency says there is no closed proof of False under the designated assumptions. Decidability of type checking says an algorithm settles every typing judgment, including every question of definitional equality. These properties interact, but they are not synonyms.

Normalization and an analysis of normal forms can establish consistency: if every closed proof normalizes, and no normal form can inhabit False, then no such proof exists. Losing that method does not exhibit a proof of False. A semantic model offers another route: interpret types as suitable objects and False as empty, then prove that derivable judgments preserve the interpretation.

Abel and Coquand’s 2020 paper exhibits non-normalizing proof terms. Their closed example uses propositional extensionality as well as impredicativity and proof-irrelevant equality. It does not require a recursive definition or an assumption of False. We can reconstruct the mechanism in Lean 4, starting with the computational rule that makes it possible.

A cast that does not wait for its proof

Transport along reflexivity should do nothing: cast rfl x reduces to x. Lean also permits the following behavior for any proof h : A = A:

example (A : Type) (h : A = A) (x : A) : cast h x = x := rfl

#reduce (fun (h : Nat = Nat) => cast h (37 : Nat))
-- fun h => 37

The variable h cannot be unfolded to reveal a reflexivity constructor. Nevertheless, the cast disappears. Definitional proof irrelevance identifies h with reflexivity at the fixed proposition A = A, and Lean’s equality recursor supports the corresponding computation. The operational rule we need is

\operatorname{cast}_{A,A}(h,x)\longrightarrow x.

The crucial condition is that the endpoints coincide definitionally. A cast between arbitrary propositionally equal types need not disappear. Also, merely assuming a theorem of propositional proof irrelevance would not automatically install this computational rule. This is about conversion and reduction, not just a theorem saying that two proofs are equal.

Impredicativity makes a controlled self-application typable

Define a proposition T asserting that every proposition implies itself, and let D abbreviate T\to T:

T:=\prod_{P:\mathrm{Prop}}(P\to P),\qquad D:=T\to T.

Both are propositions. Let i:D be the identity on proofs of T. For a variable z:T, inspect this application one argument at a time:

\begin{aligned} z\,D &: D\to D,\\ z\,D\,i &: D,\\ z\,D\,i &: T\to T,\\ z\,D\,i\,z &: T. \end{aligned}

Thus we can define d:D by d(z):=z\,D\,i\,z. This resembles self-application, but every application has a legitimate type. We instantiate z at a proposition which itself mentions the type of z.

Impredicativity does specific work here: quantifying over Prop leaves T inside Prop, so D is an allowed argument to z. Try the same construction with the predicative type T_u:=\prod_{A:\mathrm{Type}\ u}(A\to A). Now T_u lives in Type (u + 1), and so does T_u\to T_u. That function type is too large to pass as the Type u argument of a term of T_u. The attempted self-application climbs out of its permitted universe.

Propositional extensionality closes the loop

Recall that proofs a : A and b : B give A ↔︎ B by returning the available proof in each direction. Applying propext yields e(a,b):A=B.

Use this to construct w:T. Given any proposition P and proof p:P, we have proofs i:D and p:P, hence e(i,p):D=P. Cast d:D along this equality:

w(P,p):=\operatorname{cast}_{D,P}(e(i,p),d):P.

This has exactly the type required for w:T. Finally set L:=d(w):T. All the definitions together are small enough to inspect:

namespace ReductionLoop
set_option linter.defProp false

def T : Prop := ∀ P : Prop, P → P
def ident : T → T := fun z => z
def diagonal (z : T) : T := z (T → T) ident z

def trueEq {A B : Prop} (a : A) (b : B) : A = B :=
  propext ⟨fun _ => b, fun _ => a⟩

def rolled : T := fun P p =>
  cast (trueEq (A := T → T) (B := P) ident p) diagonal

def loop : T := diagonal rolled

#print axioms loop
-- [propext]
end ReductionLoop

Here ident, diagonal, rolled, and loop are i,d,w,L. These are ordinary nonrecursive definitions. The linter option allows us to keep their proof bodies as transparent defs rather than following the usual stylistic preference for theorem.

Now perform the reductions. Unfolding diagonal exposes an application of rolled; unfolding that application exposes a cast with matching endpoints:

\begin{aligned} L=d(w) &\longrightarrow w\,D\,i\,w\\ &\longrightarrow \bigl(\operatorname{cast}_{D,D}(e(i,i),d)\bigr)\,w\\ &\longrightarrow d(w). \end{aligned}

The cast exposes d because its endpoints agree, without evaluating the propext proof to reflexivity. This returns us to the starting expression; otherwise the computation would remain stuck at the cast.

Strong normalization requires every reduction sequence to terminate; weak normalization requires at least one sequence reaching a normal form. The cycle alone refutes the former. A looping subterm can sometimes be discarded: (\lambda t:T.\;0)(d\,w) reduces to 0 by contracting the outer application. For d\,w itself, Abel and Coquand establish that there is not even a weak head normal form: reduction cannot expose a finished outer shape. Thus this term is not weakly normalizing either; choosing another reduction order does not rescue it.

This identifies three distinct contributions: impredicativity permits the instantiation at D; propositional extensionality supplies the casts between inhabited propositions; and proof-irrelevant cast reduction removes the cast when the loop returns to D. The claim is about this combination of rules, not every system that has some notion of proof irrelevance. Rocq keeps a related combination behind an option: its manual warns that the Definitional UIP flag for SProp, off by default, breaks termination of reduction in the presence of an impredicative sort, and cites this example.

The example type-checks in Lean 4.34.1. The accompanying Lean file checks that #reduce (proofs := true) ReductionLoop.loop reaches a bounded recursion limit. This diagnostic corroborates the behavior; the reduction argument establishes divergence.

There is no contradiction: T already has the proof fun P p => p. Proof irrelevance identifies loop with it, but does not supply a computational reduction to it. A checker can recognize proofs of the same proposition without reducing them, so the example does not force every type check to diverge.

Undecidable conversion and the kernel’s approximation

Lean’s checker faces a second limitation, independent of the loop. Carneiro’s thesis shows that definitional equality under Lean’s rules is undecidable. The source is the accessibility predicate behind well-founded recursion, which Lean’s core library defines as follows:

inductive Acc {α : Sort u} (r : α → α → Prop) : α → Prop where
  | intro (x : α) (h : (y : α) → r y x → Acc r y) : Acc r x

Acc is a proposition, yet its recursor may eliminate into data. It computes when the proof is built by intro. Any proof a : Acc r x yields, by projection, the argument that intro would need. Proof irrelevance then makes a definitionally equal to intro applied to those projections. That rebuilt proof lets the recursor take one more step, and the rebuilding can be repeated indefinitely.

Carneiro uses this to encode an unbounded search. Fix a decidable predicate P on natural numbers, and take the relation >, which is not well-founded. Define a function by the recursor that tests P(n) and continues to n+1 while it holds, returning false at the first failure. Assume a proof a : Acc (· > ·) 0 as a hypothesis. No such proof exists, but definitional equality must still be decided in that context. The function’s result at 0 is then definitionally equal to false exactly when P fails somewhere. Halting of a Turing machine has this form, so no algorithm decides definitional equality.

The kernel’s conversion check is therefore a decidable approximation. It does not search for rebuilt proofs, so it can fail to confirm equalities that the rules permit. Carneiro draws two further consequences. The algorithmic relation is not transitive: it confirms a rebuilding step and the reduction that follows it, but not the equation that skips the rebuilt proof. And typing based on that relation fails subject reduction: reducing a well-typed term can produce one the checker rejects at the same type.

Neither result yields a proof of False. Both concern completeness: the kernel may reject terms the declared rules accept, but should not accept terms they reject. In practice Lean avoids depending on such reductions; definitions by well-founded recursion are irreducible by default, and their defining equations are used as theorems.

There is another, milder way computation can fail to produce an answer: a term can become stuck at an opaque axiom. This is different from an infinite reduction sequence. Classical choice can specify a mathematical object without an executable construction. noncomputable records that situation; it does not mean “unproved,” and it does not grant arbitrary recursive proofs. Similarly, Lean’s programming facilities for partial or unsafe code have restrictions separating them from ordinary logical definitions.

Denotational semantics and the meaning of a dependent type

A reduction describes how an expression changes during computation. A denotation assigns the expression a mathematical meaning. These are different kinds of account: a function can denote a perfectly ordinary mathematical object even when a particular reduction strategy fails to produce a normal form for its expression.

Start with a data fragment containing dependent functions. An environment \rho assigns mathematical values to the variables in a context. We want a typing judgment to imply membership:

\Gamma\vdash t:A \quad\Longrightarrow\quad \llbracket t\rrbracket_\rho\in\llbracket A\rrbracket_\rho \quad(\rho\models\Gamma).

Interpret a type as a set, a family as a set-valued assignment, and a dependent function as a graph selecting one element from each fiber:

\llbracket\prod_{x:A}B(x)\rrbracket_\rho =\prod_{a\in\llbracket A\rrbracket_\rho} \llbracket B(x)\rrbracket_{\rho[x\mapsto a]}.

For a finite example, take A=\{0,1\}, B(0)=\{a,b\}, and B(1)=\{c\}. The dependent product contains exactly two graphs:

\{(0,a),(1,c)\},\qquad\{(0,b),(1,c)\}.

A lambda expression specifying the first choice denotes the first graph. Applying it to 1 denotes c. Beta reduction preserves that meaning because substituting an argument in a body agrees with evaluating the body in an environment assigning that argument to the bound variable. That statement is the semantic substitution lemma behind the familiar computation rule.

To interpret universes, we need collections of sets with the closure properties demanded by the typing rules. Merely saying “let a universe contain all sets” does not produce a set. Which closure conditions we choose, and whether product formation remains in the same universe, determine the required set-theoretic assumptions. Werner’s “Sets in Types, Types in Sets” (1997) settled the strength needed for Rocq’s calculus. ZFC with n inaccessible cardinals proves the consistency of CIC with n+1 universes, and CIC with n+2 universes proves the consistency of ZFC with n inaccessibles. Carneiro’s thesis adapts this model to Lean.

Why propositions make the model less immediate

A tempting interpretation uses just two proposition meanings: 0=\varnothing for false and 1=\{\star\} for true. It explains proof irrelevance by giving each true proposition exactly one proof value. But combining it with ordinary function graphs needs care.

For example, the ordinary dependent product \prod_{x:A}1 is a singleton containing the constant graph

g_A=\{(a,\star)\mid a\in A\}.

Its sole element is g_A, not necessarily \star. Changing A changes the graph. Thus the products of true propositions over different domains do not automatically become the same designated truth value 1. To keep quantification impredicative and proofs irrelevant, a model must reconcile these representations with abstraction, application, and substitution. Saying “all proofs are a point” does not itself do that work.

Miquel and Werner analyze this difficulty in “The not so simple proof-irrelevant model of CC”. Carneiro’s thesis handles it by proving unique typing first: each term has one type up to definitional equality. The model can then tell which products land in Prop and interpret them differently. Barras found a shortcut. Aczel’s encoding represents a function f as the set of pairs (x,y) with y\in f(x). Taking \star=\varnothing makes the constant function to \star the empty set, which is \star itself, whatever its domain.

This is also where the normalization example becomes illuminating. In a successful proof-irrelevant model of its rules, loop and the elementary proof of T would have the same denotation. Every step around the loop would preserve that denotation. There is no contradiction in

t\longrightarrow t'\longrightarrow t, \qquad\llbracket t\rrbracket=\llbracket t'\rrbracket.

Soundness requires preservation of meaning. Termination requires an additional argument ruling out infinite computation; equality of meanings supplies no decreasing measure. A semantic consistency proof can therefore succeed even when reduction fails to normalize.

A mathematician’s alternative: semantics first

Tristan Bice’s Dependent Types Simplified (2025) reverses the usual order of work. Writing as “a mathematician trained in classical logic,” Bice observes that a typical paper on dependent types opens with pages of inference rules and explains their meaning later, if at all. His two systems start from set interpretations and admit only inference rules that are sound for them.

An interpretation assigns sets to constants and variables, and nothing else affects it. Bice contrasts this with Miquel–Werner and Carneiro, whose interpretations also depend on the typing context and split the language into propositional and non-propositional parts. In his view, the split gives up the main attraction of propositions as types: one uniform treatment of types and propositions.

The price is impredicativity. Bice’s universes are the sets H(\kappa_n) of hereditary size below chosen regular cardinals; the lowest contains the propositions \varnothing and \{\varnothing\}. A dependent product whose domain lives at level m and whose fibers live at level n lands at level \max(m+1,n). Bice notes that m+1 cannot be lowered to 0, even when the fibers are propositions. Identifying all singletons would force it, but would forget each function’s domain, which is the g_A problem above. Barras’s shortcut is, in effect, that identification. It suffices for a soundness proof, but Bice wants the semantics alone to decide which terms are well-formed, and a forgotten domain makes that impossible.

Bice simulates impredicativity instead. His first system defines universal quantification as a doubly negated product, \neg\neg\prod. Since \neg X := X\to\bot denotes \{\varnothing\} when X is empty and \varnothing otherwise, the truncated product always denotes one of the two propositions. The truncation bears on our loop. A proof of \neg\neg\prod_{P}(P\to P) is not a function that accepts a proposition, so the instantiation at D cannot be written. His second system specifies quantifiers by typing statements about constants.

Specifications are how the second system introduces mathematical structure. A specification is a set of typing statements about fresh constants, judged by whether every canonical model interprets them as intended. Equality is a worked example: a reflexivity constant and a substitution constant force every canonical model to make the equality type nonempty exactly on equal pairs. These two constants mirror Eq.refl and the substitution principle behind Lean’s transport.

Large cardinals become optional. With inaccessible cardinals, each universe satisfies ZFC, as in Carneiro’s model. Without them, each still satisfies ZFC minus the power-set axiom.

Computation enters as reduction statements added as axioms. Bice states the trade-off plainly: the more reduction statements a system admits, the more likely type checking becomes undecidable. His future-work section places Lean in this frame, citing Carneiro. Lean’s computation rules correspond to an intermediate option, between pure type systems, which reduce only by β, and extensional systems, which admit arbitrary reductions and give up decidability. Normalization would require restricting attention to suitable contexts. Bice leaves it open, together with completeness of the inference rules relative to the semantics.

Bice’s systems are not Lean, and he does not claim that they model it. They isolate the choice that makes Lean’s semantics delicate: an impredicative Prop, which the loop above also exploits. To connect semantics to a proof assistant’s reliability, we also need an argument about the checker that accepts its terms.

Consistency proofs and the reliability of a checker

Mario Carneiro’s February 2025 HoTTEST presentation reports a gap in the thesis, found while formalizing it. The affected theorems are unique typing and definitional inversion, which rest on a confluence argument; the talk now lists them as conjectures. The thesis’s soundness proof used unique typing, though the thesis itself notes that Barras’s encoding avoids that dependency. The talk mentions an alternative route to soundness, while some kernel optimizations still depend on the conjectured theorems.

As of October 13, 2026, the con-leche project constructs a set-theoretic interpretation and proves that every environment accepted by its verified checker has a model. It assumes set theory with an ω-chain of Grothendieck universes; the interpretation of accepted declarations is then proved to exist. Since False denotes the empty set, no accepted theorem can inhabit it. The checker deliberately differs from the official kernel, including its handling of inductives, so the result covers con-leche’s accepted declarations without automatically covering everything the official kernel accepts.

The result is relative: the accepted declarations are consistent if set theory with those universes is. The proof is itself a Lean development, so Lean also serves as the metatheory, checked by the official kernel. Connecting that theorem to an executable checker also requires accounting for its parsing, runtime, and input handling.

This leads to four separate questions about an individual formalization:

Question What would answer it
Does the formal statement express the intended mathematics? Inspect definitions, hypotheses, quantifiers, and representations
Does the proof depend only on acceptable assumptions? Inspect its transitive axiom dependencies
Does the checker implement its stated rules correctly? Implementation analysis, verification, and independent rechecking
Are those rules consistent under the chosen foundations? A metatheoretic argument for the precise formal system

Success at one level does not discharge the others. A correct kernel can check an unintended theorem. A plausible mathematical theory can have a defective implementation. A theorem about one checker may omit parts of the runtime used to execute it.

De Moura’s August 24, 2026 postmortem documents kernel and runtime soundness bugs and reports fixes in Lean 4.33.1. It explains why adversarial developments need validation beyond an ordinary build, including statement comparison and external checking. These exploits concern the behavior of the affected implementations.

#print axioms answers the dependency question. Statement inspection and implementation validation answer others. A successful build provides evidence about a particular checking run; assessing its trust assumptions also requires knowing how tactics, native computation, and imported artifacts enter that run.

Moses’s critique makes claims of several kinds, and these distinctions sort them. Its normalization claim is correct; the loop above reconstructs it. What it costs is one route to consistency, not evidence of inconsistency. Its consistency claim also stands as written: no public proof covers Lean 4 as the official kernel implements it. Carneiro’s thesis covers Lean 3, and its proof now has a reported gap; con-leche covers a different checker. Its strongest evidence concerns implementations: kernel bugs, nested inductives, and validation tools run without isolation. That evidence belongs to the third row of the table, and the postmortem confirms it. Its warning about mistranslated statements belongs to the first row. Its forecast that current Lean accepts some proof of False is a judgment about implementation risk, which the foundational results here neither support nor refute.

The opening basis theorem now has a more precise shape. It can assert that a natural number and a family of vectors satisfying the basis properties exist, package them as data with proofs attached, or define a particular basis using classical choice. Each formulation makes a different promise about computation and observation. Reading Lean means seeing those promises in the type.

The accompanying Lean file contains the Lean examples in order. The GADT examples are collected in Haskell and OCaml files. The Lean examples were checked with Lean 4.34.1, using its bundled library and no Mathlib dependency. For continued study, use Theorem Proving in Lean 4 alongside the Rocq reference manual: read the common construction first, then compare the rules that differ.