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.
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 xand(x : A) → B xare the same dependent function type. Binder conventions carry over:∀ ⦃x y : A⦄, f x = f y → x = yis identical to⦃x y : A⦄ → f x = f y → x = y. Both makexandystrict implicit arguments, inferred when a subsequent explicit argument is supplied.
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 principles enter with rules such as double-negation elimination without further hypotheses.
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 rules also distinguish its computational Set sort
from Prop; Lean’s ordinary small data live in
Type.
Rocq’s Set is a sort; Lean’s mathematical
Set α represents 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
Lean allows arguments to be declared before the colon, leaving their
names in scope for the type that follows. These are two equivalent ways
to write the cons constructor inside the declaration:
| cons {n : Nat} : α → Vec α n → Vec α (n + 1)
| cons : {n : Nat} → α → Vec α n → Vec α (n + 1)
In the first form, the colon after } introduces the
remaining function type; in the second, the whole function type follows
the colon. Both make n implicit, usually inferred by Lean.
The placement is stylistic; the dependency comes from using
n in the subsequent types.
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 a list operation that might fail with this signature. Here the caller must supply a nonempty vector; the 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; an argument of type
Bad → False has a forbidden negative occurrence. Nested
inductive type constructors have additional rules, described in the inductive-type
reference.
If we freely admitted 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 recursively call itself: it
applies the function stored in its argument. For an arbitrary
f : Bad → False, unfolding d and then matching
the constructor gives the open reduction sequence:
d (mk f)
→ (match mk f with | mk g => g (mk g))
→ f (mk f)
The expression is open because f remains a
variable; its behavior determines what happens next. The forbidden
constructor nevertheless lets us package d itself as an
input: mk d : Bad. Specializing to f = d
therefore yields the closed term d (mk d) : False, whose
reduction repeats:
d (mk d) → d (mk d) → ...
Each arrow here abbreviates unfolding and matching. 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 instead also has primitive fixpoint terms with a guardedness discipline in its core presentation. 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.
Off-topic details: 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_byanddecreasing_bylet 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 typeBad → Falseimposes no descent condition. Packagingdwithmklets 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 todcannot 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. Likewise, accepting a Lean definition through well-founded recursion does not establish normalization of every term in Lean’s ambient logic, as the later counterexample will illustrate.
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.
How far GADTs take us
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 _) = xDataKinds 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, _) -> xThe 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.
Transport between equal indices is therefore available in both
languages.
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 aFor 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 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.
At the all-true assignment, all three propositions equal
True. But formulas describe dependence on assignments:
F and G agree everywhere, whereas F and H
disagree at (p,q,r)=(\mathrm{True},\mathrm{False},\mathrm{False}).
The latter are merely equisatisfiable: each has a satisfying
assignment. Equisatisfiability
preserves existence of a model, not agreement under every
assignment.
In Lean, represent these meanings as predicates
F G : Assignment → Prop. Equivalence means
∀ ρ, F ρ ↔︎ G ρ; propext and
funext then give F = G. Equisatisfiability
means (∃ ρ, F ρ) ↔︎ (∃ σ, G σ): propext equates
these existential propositions, not the predicates. The witnesses may
differ. Classically, Prop has two values up to
propositional equality, yet three Boolean inputs permit 2^{2^3}=256 distinct truth functions.
Equality of propositions does not discard dependencies that remain
explicit in a function’s arguments.
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, supports excluded middle. 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. A consistency argument for Lean 3 therefore needs further
work to cover Lean 4’s extensions.
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 three 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. 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 2019
preprint, published in 2020, 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.
The example type-checks in Lean 4.34.1. The companion 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.
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.
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.
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.
Two approaches to dependent types in sets
Yang, Guilloud, and Kunčak’s Are Dependent Types in Set
Theory Feasible? (2026) mechanizes dependent products and cumulative
universes in Lisa using Tarski–Grothendieck set theory. Its
bidirectional type-checking tactic produces proofs of set-membership
judgments for Lisa’s kernel to check. The paper excludes inductives and
recursive definitions and presents the work as a step toward proof
transfer. It does not establish a model of Lean’s complete calculus,
including its impredicative Prop.
Bice’s Dependent Types Simplified (2025) develops two systems by defining set interpretations before choosing sound inference rules. Its canonical universe construction uses H(\kappa_n), sets of hereditary size below selected regular cardinals. For a dependent product \prod_{x:A}B(x), m is the universe level of the domain A, and n is a common universe level for the fibers B(x). The product lies at level \max(m+1,n); its products into the proposition universe are explicitly not impredicative. It offers a route without inaccessible cardinals by changing the universe discipline. Decidability and normalization require further restrictions discussed as future work.
These approaches expose the choices behind “types are sets”: the representation of functions, closure of universes, and treatment of propositions. Lisa provides an implemented interface within set theory; Bice designs calculi around set semantics. Their product rules and scope differ from Lean’s, so each explains part of the design space rather than settling Lean’s metatheory. 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 found while formalizing earlier metatheory, around definitional inversion and the confluence argument. It distinguishes proved results from conjectures and describes an alternative route toward soundness.
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.
This is a relative consistency result: the checker is the object of study, while Lean serves as the metatheory in which the conditional result is proved. The set-theoretic assumptions remain explicit. 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.
The linked critique usefully prompts these questions. Its forecasts about undiscovered contradictions are judgments about risk, beyond what the normalization counterexample establishes. Evaluating them requires the specific rules, models, and implementations examined here.
A first independent Lean proof
Return now to writing a proof. 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
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. No classical axiom is required.
For practice, rewrite the proof as one fun expression.
Prove that injectivity of g ∘ f implies injectivity of
f, using congrArg g. Try the surjectivity
statements too: these exercise existential proofs and the distinction
between witnesses and returned data.
Finally, return to Vec. Prove a property by induction on
its constructors, then change a length expression using
resizeVec. 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.
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.