computo ergo sum日本語
The whole guide

Start here

  1. What Lean is
  2. Getting started
  3. Handing a finite check to decide
  4. Counting with Finset
  5. An upper bound from a single injection
  6. Series and inequalities
  7. Having Lean check a certificate
  8. It passed — but does it say what you meant?
  9. How to read a statement
  10. What is realistically too heavy for Lean
  11. Common pitfalls
  12. Glossary
  13. How Lean works

How Lean works — where it came from, and how checking works

The idea of having a machine check a proof is about half a century older than Lean, and Lean is a recent member of that lineage. Here we look, in order, at where the idea came from, how Lean itself is built, and what actually happens while a check runs. The last section is written so that you can follow it without ever having seen Lean.

The order of this page
  1. Where the idea of machine-checked proof came from
  2. Where Lean came from
  3. How Lean is put together
  4. How checking works — at a beginner's pace
  5. References and sources

01

Where the idea of machine-checked proof came from

The idea is the confluence of two threads that started far apart.

The first thread: a proof can be written as an object a machine can handle. The mechanism that assigns types to functions and the mechanism that proves an implication "if A then B" have the same shape, a correspondence known as the Curry–Howard correspondence. One half of the name comes from Curry's 1934 paper "Functionality in Combinatory Logic". With this correspondence, a proof is no longer something you read and find convincing; it is a concrete piece of writing, a term of the right type.

The second thread: the part that checks should be small. In 1972, Milner implemented a proof checker called LCF at Stanford. In the LCF tradition, the tools that assemble a proof (tactics) can be written freely in a metalanguage, while whether the finished product is accepted is decided by a small core alone. No matter how many tools are added, the part you have to trust does not grow.

The first serious meeting of the two threads was de Bruijn's Automath. Designed as a language for writing down mathematical proofs and having a machine check them, it was reported in 1970. It was the first full-scale attempt to put mathematical proofs through machine checking. In the 1970s, Jutting checked Landau's Grundlagen der Analysis in this language.

The design principle that emerged from this is what later came to be called the de Bruijn criterion: keep the proof not as an intermediate state of the check but as a self-contained term, so that a small, independent checker can read it again. In the classic comparison table of proof assistants, this stands as its own item, "small proof kernel (has proof objects)". Lean's official documentation says the same thing: "a proof term is sufficient evidence of the truth of a theorem, and it can be subjected to independent verification".

On the type-theory side, Martin-Löf's intuitionistic type theory (1975) set out to rebuild the foundations of mathematics on top of types, and building on it, Coquand presented the first version of the Calculus of Constructions in 1985. The first implementation was begun in 1984 by Huet and Coquand, and its core was a type checker called the Constructive Engine. In 1989, Coquand and Paulin added inductive definitions, giving the Calculus of Inductive Constructions. This is the logic of Coq, and it is also the foundation of Lean's logic.

The large theorems actually checked in this lineage are mixed into the timeline below.

YearEvent
1934Curry, "Functionality in Combinatory Logic". One half of the name Curry–Howard
1970de Bruijn reports Automath. The first full-scale attempt to put proofs through machine checking
1972Milner implements LCF at Stanford ("Logic for Computable Functions: Description of a Machine Implementation")
1970sJutting checks Landau's Grundlagen der Analysis in Automath
1975Martin-Löf, "An Intuitionistic Theory of Types: Predicative Part"
1984Huet and Coquand begin implementing the Calculus of Constructions. The core is a type checker called the Constructive Engine
1985Coquand presents the first version of the Calculus of Constructions
1988Coquand and Huet publish the Calculus of Constructions paper
1989Coquand and Paulin add inductive definitions, giving the Calculus of Inductive Constructions
c. 2005A proof of the four colour theorem is reported checked in Coq
2013The paper on checking the Feit–Thompson theorem (the odd order theorem) in Coq appears. 6 years of joint work
2017The paper on checking the proof of the Kepler conjecture in HOL Light and Isabelle appears (Flyspeck)

Lean sits at the continuation of this table. Its underlying type theory is of the same family as Coq's, and its design principle inherits the "small core" of the LCF tradition.


02

Where Lean came from

Lean was started in 2013 by de Moura at Microsoft Research. The first commit is from July of that year. Since then, both the notation and the implementation have been replaced several times over.

The first public release was 0.1 (2014). Version 0.2 had a homotopy type theory mode alongside the standard logic, and is frozen in a separate repository under the name Lean 2. Lean 3 (January 2017) brought a wider user base, and in July of the same year the mathematics library mathlib was created. Development of Lean 4 began in April 2018, and the official release came in September 2023.

One of the distinguishing features of Lean 4 is that it is rewritten in Lean itself. The parser, the elaborator, and the tactics are all written in Lean, and users can extend them in Lean as well. Parts that in earlier versions could only be added by writing C++ can now be written in the same language as the library.

mathlib, for its part, grew as the work of a community rather than of an individual. Its design is recorded in a 2019 paper, and it has now passed a million lines. The port from Lean 3 to Lean 4 was completed in July 2023.

In that same July 2023, the way Lean itself is developed changed. de Moura and Ullrich founded Lean FRO (a Focused Research Organization), and Lean became a tool developed under a non-profit organization.

Two formalizations in Lean stand as landmarks of scale. The Liquid Tensor Experiment was posed in December 2020 as a challenge, "can this theorem be formalized?"; the proof of the first target was announced on 28 May 2021, and the whole was completed on 14 July 2022. For the polynomial Freiman–Ruzsa conjecture, a formalization project was launched right after the proof paper appeared in November 2023, and its first stage is complete.

DateEvent
2013-07First commit to the Lean repository
2014-06Lean 0.1 released
2015-01First university course using Lean (Carnegie Mellon University)
2015-08System description of Lean appears at CADE-25
2017-01Lean 3.0 released
2017-07mathlib (for Lean 3) created
2018-04Development of Lean 4 begins
2019-10Paper describing the design of mathlib appears
2020-12Liquid Tensor Experiment posed as a challenge
2021-05mathlib4 repository created
2022-07Liquid Tensor Experiment completed
2023-07Lean FRO founded. Port of mathlib to Lean 4 completed
2023-09Lean 4.0 officially released
2023-11Formalization project for the polynomial Freiman–Ruzsa conjecture begins
2023-12Formalization project for Fermat's Last Theorem begins
2025-01Contributions to mathlib4 pass 20,000

03

How Lean is put together

Between the string a user types and its acceptance as a theorem, Lean passes through the following stages.

StageWhat it does
parserTurns the sequence of characters into a syntax tree. Since users can add new notation, the type of syntax trees is very general
macro expansionReplaces syntactic sugar, there for ease of writing, with more basic syntax
elaboratorTurns user-facing syntax into terms of the core type theory. Fills in omitted arguments, searches for type-class instances, and runs the tactics inside by
kernelChecks that the term produced by the elaborator obeys the rules of the type theory
compilerTurns elaborated Lean code into something that can be executed

What does the work here is that the core type theory is far simpler than the language users write in. In the words of the official documentation, "this core theory is much simpler, which allows the trusted kernel to be kept very small". However clever the elaborator becomes, what the kernel reads is still the simple language.

The compiler is not in the line of verification. It is a separate track for producing runnable programs and has no bearing on whether a theorem is correct. The exception is native_decide: only then does a result produced by the compiler enter the logic as an axiom (01).

For the core type theory, we note just the names.

Dependent types

A mechanism by which a type is determined by a value. Like "the type of lists of length n": the type is fixed only after n is received. It is this mechanism that lets propositions be written as types.

Inductive types

A mechanism for making a type by declaring "everything that can be built in these ways". For the natural numbers: "0, and the successor of something". From this declaration, the principles of case analysis and induction are generated mechanically.

Universes

Since types are themselves terms, a "type of types" is needed. Splitting this into levels gives the universes, each with a level (a natural number). Every universe is an element of the one above it, and the types in a given universe can quantify, propositions aside, only over types in smaller universes. So "the type of all types" cannot be made naively.

Definitional equality

The relation "comes to the same thing when computed". Applying a function to an argument (β), replacing a defined name with its body (δ), advancing a case analysis on an inductive type (ι), replacing a name bound by let with its value (ζ). In addition: reduction of quotient types, η-equivalence for functions and for single-constructor types, and proof irrelevance (any two proofs of the same proposition are equal). When the kernel says "both sides are the same", this is the relation it means.

One distinction, to close. mathlib is not part of the kernel. It is a library, that is, a collection of definitions and theorems written in Lean, and in status it is the same as the files a user writes. If a wrong proof got into mathlib, it would still have gone through the kernel; and conversely, remove all of mathlib and the kernel works exactly as before.


04

How checking works — at a beginner's pace

This is the heart of the page. So that you can follow it without ever having touched Lean, we start with an analogy and then look at the real thing one step at a time.

The counter-clerk analogy

Think of a proof as a completed form. The proposition is the form's layout, and the proof is one sheet with that layout filled in.

The kernel is a clerk at a counter who knows only the rules for filling in the form. The clerk does not look at whose hand wrote it, whether it is handwritten or printed, or how many drafts were thrown away. All the clerk looks at is whether the fields of the form are filled in according to the rules. Filled in, it is accepted; not filled in, it is handed back.

This "not looking" is the point. The tools that assemble the proof (tactics), the search programs, human intuition: all of these are outside the counter. What the clerk looks at is only the one sheet finally submitted.

What we have so far: the kernel looks not at "how it was made" but only at "whether the finished thing follows the rules".

Step one — closing by computation

We look at the smallest form there is.

theorem two_add_two : 2 + 2 = 4 := rfl

After the : is the layout (the proposition); after the := is the filled-in part (the proof). rfl is a one-word entry meaning "both sides compute to the same thing", and the kernel that receives it actually computes both sides.

The rules of computation are the definition of addition on the natural numbers itself. This is how it is in Lean.

#check @Nat.add_zero
#check @Nat.add_succ
Nat.add_zero : ∀ (n : ℕ), n + 0 = n
Nat.add_succ : ∀ (n m : ℕ), n + m.succ = (n + m).succ

"Adding 0 to something gives that thing" and "adding 'the successor of something' gives the successor of the sum". That is all there is: two rules. succ is the operation that makes "the next number", and the right-hand 2 in 2 + 2 is the successor of the successor of 0. So the kernel needs only three moves.

example : 2 + 2 = Nat.succ (2 + 1) := rfl
example : 2 + 1 = Nat.succ (2 + 0) := rfl
example : 2 + 0 = 2 := rfl
example : Nat.succ (Nat.succ 2) = 4 := rfl

All four lines pass the check. The first and second lines are Nat.add_succ, the third is Nat.add_zero, and the fourth is the fact that the notation 4 denotes the successor of the successor of 2. Chain these four together and you get from 2 + 2 to 4. What the kernel does for the theorem at the top is exactly this.

What we have so far: a kernel that receives rfl uses the defining rules a few times to bring both sides to the same form.

Step two — a proof of "and" is a pair

Propositions that do not close by computation fit the same frame. We look at the layout of "A and B".

#check @And.intro
@And.intro : ∀ {a b : Prop}, a → b → a ∧ b

Read it as: "for propositions a and b, given a proof of a and a proof of b, you get a proof of a ∧ b". In other words, a proof of a ∧ b is a pair of a proof of a and a proof of b.

example (p q : Prop) (hp : p) (hq : q) : p ∧ q := ⟨hp, hq⟩

(hp : p) declares "receive a proof of p under the name hp", and ⟨ ⟩ is the notation for making a pair. Put the wrong thing in one slot and the clerk hands it back with a reason.

example (p q : Prop) (hp : p) : p ∧ q := ⟨hp, hp⟩
error: Application type mismatch: The last
  hp
argument has type
  p
but is expected to have type
  q
in the application
  ⟨hp, hp⟩

"The second field should hold a proof of q, but a proof of p is in it." It is a note about a defect in the form. The same goes for the rfl side.

example : 2 + 2 = 5 := rfl
error: Type mismatch
  rfl
has type
  ?m.16 = ?m.16
but is expected to have type
  2 + 2 = 5

Strictly speaking, these two notes come not from the kernel but from the elaborator. The elaborator also watches the types itself as it assembles the term, so defects are usually caught before they reach the kernel. The kernel is the final gate that runs the finished term through the same rules once more.

What we have so far: each proposition fixes "how the form is to be filled in", and if the type of a field does not match, it does not pass.

Step three — a proof of "implies" is a function

The entry for "if A then B" is a function that takes a proof of A and returns a proof of B.

example (p : Prop) : p → p := fun hp => hp

example (p q : Prop) (h : p → q) (hp : p) : q := h hp

The first line is "p implies p", a function that returns whatever it receives. The second line says that given a proof h of "p implies q" and a proof hp of p, you obtain a proof of q, written as h hp (applying the function to the argument). A step of logic has the same shape as a function call. This is the Curry–Howard correspondence, felt by hand.

What we have so far: a proof of an implication is a function, and inference is calling that function.

Step four — even written with tactics, it ends up as a term

Real proofs cannot be written this briefly. So you line up tactics after by and have them assemble the term for you.

theorem and_swap (p q : Prop) (h : p ∧ q) : q ∧ p := by
  constructor
  · exact h.2
  · exact h.1

constructor is the tactic that splits the goal in two, "to make q ∧ p you need q and p"; exact is the tactic that puts what you have in hand into that field. The assembled result can be seen with #print.

#print and_swap
theorem and_swap : ∀ (p q : Prop), p ∧ q → q ∧ p :=
fun p q h => ⟨h.right, h.left⟩

The four lines of tactics are gone, and a one-line term remains. And its contents are exactly the shapes seen in steps two and three: a function that receives arguments (fun) and a pair of two proofs (⟨ ⟩). Tactics are a tool for writing this term, and this one line is all the kernel receives.

What we have so far: tactics are a tool for saving writing, and the form submitted is the same kind of term as one written by hand.

Why it can be believed

With all this in place, what has to be trusted and what does not fall apart cleanly.

What has to be trustedThe kernel's implementation / the axioms accepted (01) / how the statement is written: whether that proposition is what was meant (08)
What need not be trustedTactics and automation / the programs that search and filter / the provenance of the proof, whether written by a person or generated automatically / what the editor displays. All of these only make terms; whether they pass is decided by the kernel

Even so, "the kernel's implementation" remains. So there are means for re-reading a finished proof. Lean's official documentation lists ways of confirming, in order of strength.

Way of confirmingWhat it tells you
The blue double check mark in the editorThat the theorem's statement was elaborated and the kernel accepted the proof. For everyday work this is enough
#print axiomsThe list of axioms relied on. sorry holes, axioms added on one's own, and native_decide show up here
leancheckerRe-reads the declarations inside the .olean files produced by the build and runs them through the kernel again. Shipped with the Lean toolchain
A matcher plus external checkersExports the proof terms, runs them through both Lean's kernel and a checker of a different implementation, and further matches the statement proved against the statement in hand

The third, leanchecker, is a tool that runs Lean's own kernel once more, so it cannot catch defects in the kernel itself. What it catches are defects in the surrounding code that handles the kernel's state, and things of the kind where a metaprogram bypasses the check and adds a declaration.

The fourth is the strongest form currently available. The proof is built in an isolated environment, the proof terms are exported, and outside of it, that is, somewhere the proof-side programs cannot reach, they are run through both Lean's kernel and a separately written, independent checker. The external checker the official documentation names is one implemented independently in Rust, and a venue has been set up for lining up further checkers and comparing them. This is the dividend that the de Bruijn criterion actually pays. Because the proof remains as a term, a different checker can read it again.

Assumptions still remain. The official documentation lists those too: that Lean's logic itself is sound, that the plumbing for export and matching is correct, that the isolated environment is not breached, that there is no defect affecting all the checkers used at once, and that the statement contains no human error or misleading formulation.

What we have so far: what has to be trusted is narrowed to the kernel, the axioms, and the statement, and the kernel itself can be re-read by a different implementation.

Where it can still go wrong

The statement differs from what was meant

The most common form. Taking a definition too weakly, swapping the order of quantifiers, adding one hypothesis. All of these pass the check, and only what was proved becomes something else. The machine does not tell you about "the condition you dropped". Examples are in 08; how the recipient confirms is in 09.

An axiom was added

In Lean, a new axiom can be declared with axiom. Add one and anything at all can be proved, so check with #print axioms that nothing beyond the standard three appears (01).

A sorry hole

A mark that makes Lean accept an unfinished spot. A warning is issued but the check passes, and it appears under #print axioms as sorryAx. You miss it if you look only at errors (01).

native_decide

Compiles the decision procedure to machine code, runs it, and accepts the result as an axiom. The range of trust widens to the compiler and to every definition carrying a runtime replacement directive. Not used on this site (01).

A defect in the kernel

Small as it is, the kernel is a program. There is in fact a case where an attempt to rewrite the kernel in a different language turned up a type-checking defect, which was dealt with the same day it was reported (it concerned the handling of variables appearing in the type of a let binding). Defects of this kind are caught precisely by the fourth item in the table above, the form that lines up checkers of different implementations.

Who guarantees what

ItemWhat it guaranteesWhat it does not guarantee
kernelThat the submitted term is built from the definitions and axioms according to the typing rulesThat the statement is what was meant
tactics, automationNothing. They only make terms— (a failure does not mean the proposition is false)
#print axiomsThat the list of axioms relied on comes out with nothing missingWhether those axioms are sound
external checkersThat the term can be re-read without relying on the implementation of Lean's kernelThe soundness of Lean's logic itself
the writer, the reader—Whether the statement and the definitions match the intent remains a job for people to confirm

What machine checking moved is the boundary between what is confirmed and the means of confirming it. "Is the proof correct?" can be handed to the machine. "Is this theorem what was meant?" cannot. That the latter remains is not a weakness; it is the job that came to the fore once the former was taken care of.


References and sources

SectionItemSource
01The name of the Curry–Howard correspondence and Automath's use of it / LCF, tactics and the metalanguage / the years and people of CoC and CICThe Rocq (formerly Coq) reference manual, "Early history of Coq"
01Curry, "Functionality in Combinatory Logic" (1934)Proceedings of the National Academy of Sciences, November 1934
01de Bruijn, "The mathematical language AUTOMATH, its usage, and some of its extensions" (1970)Symposium on Automatic Demonstration, Lecture Notes in Mathematics
01Milner, "Logic for Computable Functions: Description of a Machine Implementation" (May 1972)Stanford Artificial Intelligence Project, Memo AIM-169 / STAN-CS-72-288
01Martin-Löf, "An Intuitionistic Theory of Types: Predicative Part" (1975)Logic Colloquium '73
01Coquand and Huet, "The calculus of constructions" (1988)Information and Computation
01The name "de Bruijn criterion" and the item "small proof kernel"The comparison table and footnotes in Wiedijk, "The Seventeen Provers of the World"
01The Coq check of the four colour theoremGonthier, "A computer-checked proof of the Four Colour Theorem" (Microsoft Research Cambridge)
01The Feit–Thompson theorem, 6 years of joint workGonthier et al., "A Machine-Checked Proof of the Odd Order Theorem" (ITP 2013)
01The Kepler conjecture checked in HOL Light and IsabelleHales et al., "A formal proof of the Kepler conjecture", Forum of Mathematics, Pi (2017)
02The Lean timeline (from the first commit to the present)Lean FRO, "A Brief History of Lean"
02Started in 2013; small trusted kernel; dependent type theoryde Moura et al., "The Lean Theorem Prover (System Description)" (CADE-25, 2015)
02Lean 4 as a rewrite in Lean itself; users can extend the parser and elaboratorde Moura and Ullrich, "The Lean 4 Theorem Prover and Programming Language" (CADE 28, 2021)
02Lean 0.2 having a standard mode and a homotopy type theory modeThe description in the (frozen) Lean 2 repository
02The design of mathlib and the organization of the communityThe mathlib Community, "The lean mathematical library" (CPP 2020)
02mathlib passing a million linesThe introduction on the official Lean website
02The dates of the Liquid Tensor Experiment (the challenge in 2020-12, the announcement on 2021-05-28, completion on 2022-07-14)The description in the project's repository
02The proof paper for the polynomial Freiman–Ruzsa conjecture and the completion of the first stage of the formalizationGowers, Green, Manners and Tao, "On a conjecture of Marton" / the description in the project's repository
03The five stages from parser to compiler and their roles / "the core theory is much simpler, which allows the kernel to be kept very small"The Lean language reference, "Elaboration and Compilation"
03Dependent types, inductive types, universes, quotients / definitional equality (β, δ, ι, ζ, quotient reduction, η, proof irrelevance) / "a proof term is sufficient evidence of the truth of a theorem, and it can be subjected to independent verification"The Lean language reference, "The Type System"
04The four ways of confirming and what each covers / the list of remaining assumptions / the external checker being implemented independently in RustThe Lean language reference, "Validating a Lean Proof"
04leanchecker being shipped with the Lean toolchainThe notice in the old lean4checker repository, and what ships with Lean 4.33.1 on our machine
04native_decide extending trust to the compiler and to definitions with replacement directivesThe Lean language reference (the entry on decide)
04The case of a kernel type-checking defect found and fixed the same day (handling of variables in the type of a let binding)Lean issue 10475 / the list of defects cited by the attempt to rewrite the kernel in another language
04The code snippets and output on this pageLean 4.33.1 (leanprover/lean4:v4.33.1) with mathlib. All checked on our machine, output copied verbatim. The procedure is in 02

The basics of Lean are in 01 What Lean is; running it on your own machine is in 02 Getting started; the meaning of the terms is in 12 Glossary.

Revised 2026-09-20: new page.