A proof is not a comment, a test result, or a Boolean that happened to return true. It is checked evidence for an exact proposition.
Home · Docs · Type Theory · Proofs · Verified Software · ProofScript → Lean + TypeScript · Reference · Examples · Download
This page starts with one tiny theorem and gradually opens the machinery underneath it. If you have never used a theorem prover, read the English translations before worrying about the formal names.
Syntax note: canonical definitions use
def; beginner examples may use thefunction/top-levelconstaliases. The theorem and proof semantics are unchanged.
Start here:
theorem twoPlusTwo: 2 + 2 = 4 := by {
rfl
}
“Declare a theorem named
twoPlusTwo. Its proposition is that2 + 2equals4. Provide a proof by showing that both sides reduce to the same value.”
| Part | Meaning |
|---|---|
theorem |
introduce a named proved proposition |
twoPlusTwo |
theorem name |
: |
“has the following type” |
2 + 2 = 4 |
proposition being proved |
:= |
provide the theorem’s value/proof |
by |
begin tactic-style proof construction |
rfl |
close equality by reflexivity after definitional computation |
The important fact is:
twoPlusTwo : 2 + 2 = 4
The theorem name is not the guarantee. The theorem’s type is the guarantee.
From Type Theory:
2 + 2 = 4 : Prop
The equality statement is a value of universe Prop: a proposition.
A proof of that statement has the proposition as its type:
proof : 2 + 2 = 4
So a theorem declaration can be understood like a definition whose declared type happens to be a proposition.
The key pattern is:
ordinary definition
name : DataType := dataValue
proof definition
theoremName : Proposition := proofValue
This is one reason := is so natural in theorem syntax: it is literally providing a term of the claimed type.
Before tactics, imagine this ordinary function:
function keepValue{A: Type}(x: A): A := {
x
}
It receives an A and returns an A.
Now replace the ordinary type A with a proposition P:
P → P
Read:
“Given a proof of
P, return a proof ofP.”
That is also the logical statement:
“If
Pis true, thenPis true.”
A tactic proof such as:
theorem keep(P: Prop): P → P := by {
intro h
exact h
}
is therefore constructing something very much like the identity function on proof evidence.
This propositions-as-types reading is central in Lean. Official background: Theorem Proving in Lean 4 — Propositions and Proofs and Lean Language Reference — Propositions.
rfl actually doesrfl is short for reflexivity.
Mathematically, every value is equal to itself:
x = x
But theorem provers also have definitional equality: expressions may count as the same because they compute/unfold to the same core expression.
Consider:
function double(n: Nat): Nat := {
n + n
}
Then:
theorem doubleTwo: double(2) = 4 := by {
rfl
}
A simplified computation story is:
double(2)
→ 2 + 2
→ 4
So after reduction, the goal is essentially:
4 = 4
and reflexivity closes it.
rfl is therefore a beautiful first example of the interaction between computation and proof.
Interactive theorem provers often show a proof state conceptually like:
n : Nat
────────────
n = n
Read it as:
“We have an arbitrary natural number
n. Our current goal is to proven = n.”
For a more complex context:
a : Nat
b : Nat
hab : a < b
────────────
a + 1 ≤ b
The lines above the divider are the local context: values and assumptions currently available.
The line below is the goal.
A tactic transforms one proof state into zero or more new proof states while constructing evidence.
The proof is finished when no goals remain and the constructed term passes final checking.
exact: use the evidence you already havetheorem keep(P: Prop, h: P): P := by {
exact h
}
“Given a proposition
Pand a proofhofP, provePby returning exactly that proof.”
Before exact h:
P : Prop
h : P
────────
P
The goal asks for a value of type P. h already has exactly that type.
So:
exact h
means:
“Use
h; it exactly matches the goal.”
This is not search or magic. It is ordinary typed value use.
intro: proving implication by writing a functionSuppose the goal is:
P → P
Under propositions-as-types, that is a function type.
To construct a function, introduce its input:
theorem identityImplication(P: Prop): P → P := by {
intro h
exact h
}
After intro h, the proof state becomes:
P : Prop
h : P
────────
P
Then exact h finishes.
You proved:
“If
Pholds, thenPholds.”
You constructed the identity function on proof values:
fun h => h
This is Curry–Howard in action without any mystical step: proving implication is constructing a function.
assumption: find a matching local proofThis proof:
theorem identityImplication2(P: Prop): P → P := by {
intro h
assumption
}
means the same idea.
assumption searches the local context for an available value whose type matches the goal.
It does not invent a theorem. It reuses evidence already present.
theorem successorGreater(n: Nat): n < n + 1 := by {
omega
}
Read:
“For any natural number
n, prove thatnis smaller thann + 1.”
The theorem does not test several examples such as 0, 1, 2, and 10_000.
The parameter n is arbitrary. The proof establishes the proposition for every n in the theorem’s domain.
Conceptually the theorem has the dependent function type:
(n: Nat) → n < n + 1
That is universal quantification expressed as a function that returns proofs.
omega: automation is still proof constructionIn:
theorem successorGreater(n: Nat): n < n + 1 := by {
omega
}
omega is an arithmetic decision procedure/tactic for a supported fragment of arithmetic.
A beginner can read it as:
“Use arithmetic reasoning to construct evidence that this goal follows.”
The important trust model is not:
omega says true
→ accept theorem
It is conceptually:
arithmetic goal
↓
omega reasoning
↓
proof evidence / checked result
↓
core checker
Proof automation can be sophisticated while the final logical acceptance criterion remains strict.
rw: equality lets you replace equals by equalsSuppose:
h : a = b
and your goal contains a.
Then:
rw [h]
means:
“Rewrite using the equality proof
h.”
Example:
theorem replaceEquals(a: Nat, b: Nat, h: a = b):
a + 1 = b + 1 := by {
rw [h]
}
“If
aequalsb, then adding one to each gives equal results.”
The equality proof justifies substitution.
This is propositional rewriting, not text replacement.
simp: proof-producing simplificationMany theorems reduce through common identities and registered simplification lemmas.
theorem addZero(n: Nat): n + 0 = n := by {
simp
}
Read:
“Simplify the goal using definitions and trusted/proved simplification rules until the result closes.”
The important point is that simp is not merely a pretty-printer. It is part of proof construction and must justify its rewrites.
Use rw when you want an explicit rewrite sequence. Use simp when a normalization-oriented collection of simplification rules is appropriate.
cases: prove by exhausting constructorsSuppose x: Option(A).
There are two constructors conceptually:
none
some(a)
A proof by cases x splits the theorem into the possible ways x could have been built.
Example from the ProofScript reference style:
theorem optionCases{A: Type}(x: Option(A)):
x.isSome = true ∨ x = .none := by {
cases x with
| none =>
exact Or.inr(rfl)
| some a =>
exact Or.inl(rfl)
}
x.isSome = true ∨ x = .none
∨ is logical “or.”
So the theorem says:
“For any option
x, either itsisSomeproposition holds as stated here orxisnone.”
Then cases x handles the none and some constructors separately.
The proof structure follows the data structure.
induction: cases plus an induction hypothesisFor recursive data such as lists, proving a property for all lists often needs a stronger principle than simple case analysis.
Suppose a list is either:
[]
or:
x :: xs
For the second case, induction gives you an extra assumption: the theorem is already known for the smaller xs.
That assumption is the induction hypothesis.
Conceptually:
Base:
prove P([])
Step:
assume P(xs)
prove P(x :: xs)
The reason this is valid comes from the inductive structure of lists.
Suppose append has the usual list semantics and we want:
length(append(xs, ys)) = length(xs) + length(ys)
A proof by induction on xs follows the list constructors.
xs = [].
Then:
append([], ys) = ys
so the theorem reduces to:
length(ys) = 0 + length(ys)
which simplifies.
xs = x :: rest.
Assume the induction hypothesis:
length(append(rest, ys))
=
length(rest) + length(ys)
Then unfold append and length for x :: rest, rewrite using the induction hypothesis, and simplify arithmetic.
The proof is finite because the inductive rule lets one assumption stand for the already-proved property of the smaller recursive part.
There are infinitely many lists.
Induction does not enumerate them.
It uses the fact that every finite list is generated by a finite number of applications of the constructors.
If:
then it holds for every value generated by the inductive definition.
This is a structural argument, not exhaustive testing.
Tactics are a convenient way to construct proof terms.
For a simple implication:
theorem keep(P: Prop): P → P := by {
intro h
exact h
}
The underlying idea is equivalent to a direct function term:
fun h => h
The tactic view says:
1. introduce the premise
2. solve the goal with the premise
The term view says:
return the argument
Both inhabit the same proposition type.
Learning both views is valuable:
This distinction is central to trust.
Does the explicit resulting declaration/term satisfy the core type theory?
Lean deliberately keeps its trusted kernel smaller than the rich source language. The official Lean reference describes parsing, macro expansion, elaboration, kernel checking, and compilation as separate concerns.
The ideal trust shape is:
powerful producer
↓
explicit evidence
↓
small strict checker
This is much easier to trust than making every tactic and search heuristic part of the logical foundation.
Formal validity is not the same as useful specification.
Suppose your “sort correctness” theorem says only:
Ordered(sort(xs))
A broken implementation:
sort(xs) = []
could still produce an ordered list.
So you also want something like:
Permutation(xs, sort(xs))
Now you know:
This is the specification problem: the checker proves exactly what you asked, not what you meant but forgot to state.
Consider a theorem:
P → Q
If P can never hold, the implication is logically easy to prove—but perhaps useless for the intended software domain.
The same risk appears in contracts.
A function can have a postcondition that is correct only under an impossible precondition.
So high-quality formal verification asks two questions:
These are different proof obligations.
An axiom introduces a proposition/value without proving it from the current theory.
That can be legitimate: formal developments sometimes state assumptions intentionally.
But a theorem depending on an axiom is conditional on that assumption.
A mature verification report should therefore expose transitive assumptions rather than presenting every accepted theorem as equally assumption-free.
Think:
theorem
+
assumption manifest
not merely:
green checkmark
sorry is not proofDuring development, proof assistants may support placeholders such as sorry for incomplete work.
A strict verified artifact must not silently treat unresolved placeholders as completed proof evidence.
For production assurance, the verification profile should report and reject unapproved sorry dependencies.
The principle is simple:
A placeholder is useful for editing. It is not a finished proof.
Some proof procedures can rely on computation outside the smallest kernel reduction path for performance.
That does not make them automatically bad, but it changes the trusted computing base.
A serious verifier records which assumptions or native-computation mechanisms a theorem transitively relies upon.
The question is not:
“Was automation used?”
The better question is:
“What evidence and trusted mechanisms does the final theorem depend on?”
Beginners often respond to a failed proof by trying more tactics. A better debugging habit is:
1. Read the goal in English.
2. Read every local assumption in English.
3. Check whether the statement is actually true.
4. Check whether an assumption you need is missing.
5. Only then choose a tactic.
Suppose the goal is:
n : Nat
────────────
n - 1 < n
This is not true for all n : Nat, because n = 0 gives 0 - 1 = 0 in natural-number subtraction, so the goal becomes 0 < 0.
The missing idea is a precondition:
h : 0 < n
Then the intended theorem becomes:
theorem predLess(n: Nat, h: 0 < n): n - 1 < n := by {
omega
}
This is an important lesson in formal verification: a failed proof is often not “the prover being difficult.” It may be exposing an underspecified or false requirement.
Before celebrating a proof, inspect what was actually proved.
Conceptually, a verification tool should let you answer:
What is the exact theorem type?
What definitions does it reference?
What assumptions does it depend on?
Which source artifact produced it?
Which toolchain checked it?
This matters because a frontend can, in principle, emit a perfectly valid proof of the wrong proposition.
Kernel validity answers:
“Is this elaborated proposition proved?”
Source/specification fidelity asks:
“Is this the proposition the source and user intended?”
Imagine a challenge asks you to prove:
secureTransferPreservesBalanceInvariant
but a buggy translator emits and proves:
1 + 1 = 2
The kernel is correct to accept 1 + 1 = 2.
But the workflow failed.
Therefore high assurance must bind:
source identity
+
exact theorem statement
+
definition/environment identity
+
assumptions
+
checker/toolchain
Proof replay alone is not enough if the replayed theorem is not the intended theorem.
Explicit proof terms/declarations allow another checker to replay the logical claim.
For Lean-compatible artifacts, a high-assurance workflow can include:
ProofScript source
↓
generated Lean artifact
↓
Lean kernel replay
↓
optional independent checker
This reduces some classes of implementation risk.
It does not prove that all checkers are bug-free or that source-to-Lean translation was correct. Multiple implementations provide diversity, not metaphysical certainty.
Lean’s official documentation has a dedicated guide to validating proofs. High-assurance workflows distinguish ordinary checking from assumption inspection, environment replay, and stronger external/comparator workflows. This supports the ProofScript rule that “the kernel accepted a proof” and “this source artifact proves exactly the claim we intended” are related but distinct assurance questions.
Official background: Lean Language Reference — Validating a Lean Proof.
Suppose you have proved:
normalize(normalize(x)) = normalize(x)
about the checked ProofScript definition.
If a buggy TypeScript emitter accidentally outputs:
normalize(x) = randomValue()
then the source theorem remains valid while the executable translation is wrong.
That is why executable correctness needs a compiler/execution correspondence claim in addition to source-level theorem checking.
The verified chain is conceptually:
source theorem
↓
logical validity
source program
↓
semantic-preserving translation
↓
TypeScript program
Two different questions. Two different kinds of evidence.
Read ProofScript → Lean + TypeScript.
Even perfectly generated TypeScript eventually touches things outside pure mathematics:
A theorem can cover those only to the extent that their behavior is included in the formal model and the runtime correspondence assumptions are justified.
This is why real software assurance is layered.
Proof gives especially strong leverage where you can write a clear mathematical model of a high-value invariant.
Examples:
normalize(normalize(x)) = normalize(x)
length(append(xs, ys)) = length(xs) + length(ys)
i : Fin(xs.size)
decode(encode(x)) = some(x)
Invariant(s) → ValidTransition(s, s') → Invariant(s')
authorize(u, a) = allowed → PolicyPermits(u, a)
These statements can cover entire mathematical domains rather than a finite test suite.
A test says:
“For this selected input and environment, I observed this result.”
A theorem says:
“For every value covered by these quantified variables and assumptions, this formal proposition follows.”
But tests can inspect aspects a theorem model may not cover:
So the right relationship is:
proofs + tests + review + runtime validation
not:
proofs replace all engineering
When you see:
theorem T(args...): P := by {
tactics...
}
read it as:
1. `P` is a proposition/type.
2. The theorem parameters define arbitrary inputs and assumptions.
3. The tactic script tries to construct a term of type `P`.
4. The checker validates the resulting term/declaration.
5. If accepted, `T` is evidence for exactly `P` under exactly those assumptions.
6. The theorem says nothing automatically about properties not contained in `P`.
7. Connecting `P` to generated TypeScript execution requires separate correspondence evidence.
That is how ProofScript proves things.
grind tacticNext: Verified Software →