Programs, properties, proofs, and what each proof actually means.
Home · Docs · Type Theory · Proofs · Verified Software · ProofScript → Lean + TypeScript · Reference · Examples · Download
These examples are ordered from ordinary programming toward stronger formal specifications.
Syntax note:
defis canonical.functionand top-level binderlessconstare aliases used in beginner-facing examples when they improve readability for TypeScript developers.
const answer: Nat := {
42
}
“Define
answerto have typeNat, with value42.”
: — “has type.”Nat — natural-number type.:= — “is defined as.”Canonical form:
def answer: Nat := {
42
}
function add(a: Nat, b: Nat): Nat := {
a + b
}
“Define
add; it receives natural numbersaandb, and returns their sum as a natural number.”
Canonical form:
def add(a: Nat, b: Nat): Nat := {
a + b
}
Nothing beyond type correctness yet. This is a program definition, not a theorem.
theorem answerIsFortyTwo: answer = 42 := by {
rfl
}
“Prove that the previously defined
answeris mathematically equal to 42.”
= instead of ==?answer = 42 is a proposition.
answer == 42 would be a Boolean computation.
rfl?answer unfolds to 42, so both sides are definitionally the same.
:=, =, and == togetherconst n: Nat := {
5
}
theorem nIsFive: n = 5 := by {
rfl
}
#eval n == 5
Read each line differently:
n := 5 define n
n = 5 proposition: n equals 5
n == 5 compute Boolean equality
This is one of the most important notation distinctions in the language.
function identity{A: Type}(x: A): A := {
x
}
“For any inferred type
A, take anAand return anA.”
{A: Type} mean?It is an implicit type parameter. The elaborator can usually infer A from x.
function identity<T>(x: T): T {
return x;
}
The generic intuition transfers; the underlying type theory is much richer.
Type:
(n: Nat) → Fin(n + 1)
“Given
n, return a natural number smaller thann + 1.”
The output type literally contains the input.
That makes it a dependent function type.
Fin(n)def lastIndex{n: Nat}(h: 0 < n): Fin(n) := {
⟨n - 1, by omega⟩
}
“Given a positive
n, returnn - 1as an index belown.”
⟨value, proof⟩ constructs a value whose expected type requires both data and evidence.
by omega proves the arithmetic bound.
For the returned value, the bound required by Fin(n) is established.
Nothing here says anything about a JavaScript array, memory safety, or external mutation.
def first(xs: Array(Nat), h: 0 < xs.size): Nat := {
xs[0]
}
“Read the first element of
xsunder a proof thatxshas positive size.”
The precondition is represented as proof parameter h.
A caller cannot obtain this function’s checked guarantee without satisfying the logical precondition.
Conceptual interface:
def get{A: Type}(xs: Array(A), i: Fin(xs.size)): A := {
xs[i]
}
“The index itself carries evidence that it is valid for the size of this array.”
This style moves the relationship into the argument type instead of a separate proof parameter.
def getOrZero(x: Option(Nat)): Nat := {
match (x) {
| none => 0
| some n => n
}
}
“If there is no number, return zero. If there is a number
n, return it.”
match (x) — inspect x by constructor.| — start a case.=> — branch result.def length{A: Type}: List(A) → Nat
| [] => 0
| _ :: xs => length(xs) + 1
“Empty list length is zero. A non-empty list has one plus the length of its tail.”
The recursive call uses the structurally smaller tail xs.
theorem keep(P: Prop, h: P): P := by {
exact h
}
“If
halready provesP, usehas the proof ofP.”
A proof can be an ordinary value passed as an argument.
theorem identityImplication(P: Prop): P → P := by {
intro h
exact h
}
“If
Pis true, thenPis true.”
Construct a function from proof of P to proof of P.
theorem successorGreater(n: Nat): n < n + 1 := by {
omega
}
“Every natural number is smaller than its successor.”
The variable n is arbitrary, so the theorem covers the entire Nat domain described by the statement—not a finite list of examples.
theorem replaceEquals(a: Nat, b: Nat, h: a = b):
a + 1 = b + 1 := by {
rw [h]
}
“If
a = b, then replacingabybpreserves equality after adding one.”
rw [h] uses the equality proof to substitute equals for equals.
theorem addZero(n: Nat): n + 0 = n := by {
simp
}
“Adding zero to any natural number returns that number.”
simp simplifies the goal using definitions and proved simplification rules while constructing evidence.
Optiontheorem 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)
}
“Every option is either the
somecase described byisSome, or it isnone.”
∨P ∨ Q means “P or Q.”
cases xSplit the proof according to every constructor that could have produced x.
Desired theorem:
length(append(xs, ys)) = length(xs) + length(ys)
Proof strategy:
induct on xs
[]:
simplify
x :: rest:
unfold append/length
use induction hypothesis for rest
simplify
In the recursive case, the proof receives evidence that the theorem already holds for the smaller tail rest.
This is how the inductive data structure supplies the logical proof structure.
A classic list theorem:
reverse(reverse(xs)) = xs
This is stronger than checking a few lists.
A typical proof requires induction and helper lemmas about how reverse interacts with append.
Large proofs often become manageable by identifying reusable algebraic lemmas, just like large programs become manageable through good abstractions.
theorem normalizeIdempotent(x: Config):
normalize(normalize(x)) = normalize(x) := by {
...
}
“Once a configuration is normalized, normalizing it again changes nothing.”
This does not say normalization preserves the intended meaning of x. That needs a separate theorem if important.
decode(encode(x)) = some(x)
“Every valid value survives an encode-then-decode round trip.”
Correctness for values generated from the modeled source domain.
Behavior on every malformed byte sequence.
Weak specification:
Ordered(sort(xs))
Better combined specification:
Ordered(sort(xs))
∧
Permutation(xs, sort(xs))
∧ means logical “and.”
“The output is ordered and contains exactly the input elements up to permutation.”
Proof does not rescue an incomplete specification.
authorize(user, action) = allowed
→
PolicyPermits(user, action)
“Whenever executable authorization returns
allowed, the declarative policy says the action is permitted.”
If you also want to ensure legitimate actions are not incorrectly denied, prove a completeness direction as well.
Invariant(s)
→
ValidTransition(s, s')
→
Invariant(s')
“Starting from a valid state, every valid transition preserves the invariant.”
Combined with an initial-state theorem, induction over transition sequences can establish the invariant for all reachable modeled states.
def pred(n: Nat): Nat
contract value {
requires hPositive: 0 < n;
ensures (result) => result < n := by {
omega
};
}
:= {
n - 1
}
“Assume
nis positive. Returnn - 1. Prove the returned value is smaller thann.”
requires becomes proof context/assumption according to the pure contract lowering.
ensures becomes an ordinary theorem/proof obligation about the result.
The verification vocabulary does not bypass the logical checker.
Imagine:
requires n < 0
with n: Nat.
The precondition is impossible.
A postcondition can be formally valid under that assumption and still tell you nothing useful about real calls.
A serious specification review therefore asks whether the precondition describes a reachable/meaningful domain.
Suppose you implement a Boolean check:
isSorted(xs) : Bool
and separately have proposition:
Sorted(xs) : Prop
A useful theorem is a bridge:
isSorted(xs) = true ↔ Sorted(xs)
↔ reads “if and only if.”
Now a fast executable Boolean computation is connected to the logical property by a checked theorem.
Without the bridge, a Boolean named isSorted is only a name and implementation.
ProofScript source:
function increment(n: Nat): Nat := {
n + 1
}
Lean view:
def increment (n : Nat) : Nat :=
n + 1
TypeScript view, simplified:
function increment(n: RuntimeNat): RuntimeNat {
return runtimeNatAdd(n, runtimeNatOne);
}
Desired correspondence statement, conceptually:
if runtimeN represents n,
then TS_increment(runtimeN)
represents n + 1
Correspondence is a theorem/relation about meanings, not a comparison of syntax.
Source theorem:
∀ n: Nat, n < increment(n)
Compiler correspondence:
RuntimeIncrement(rep(n)) = rep(increment(n))
Together, relative to the representation relation and runtime model, the executable result inherits the source property.
This is the deeper purpose of verified compilation.
A certificate may bind:
source hash
Lean artifact hash
TypeScript artifact hash
theorem type
assumption manifest
toolchain versions
correspondence profile
validation outcomes
The hashes identify the artifacts.
They do not by themselves prove semantic correspondence.
The proof/validator outcome is what supports the semantic claim.
A useful report might read conceptually:
Source: src/order.ps
Source identity: <hash>
Logical status: accepted
Theorem: Order.preserveTotal
Assumptions: approved baseline only
Lean replay: accepted under pinned baseline
TypeScript profile: ps-ts-pure-v1
TS artifact: <hash>
Correspondence: accepted for profile
External boundary: none in verified function
The exact product fields should follow the actual release format.
The important idea is that “verified” becomes inspectable evidence rather than a single unexplained badge.
A practical hierarchy:
Use types such as:
Fin(n)
Option(A)
structured validated data
idempotence
round trips
length relationships
authorization
state transitions
financial conservation
protocol safety
That is where proofs start saying something strong about production execution rather than only an abstract model.
Schematic model:
function applyEvent(state: Inventory, event: InventoryEvent): Inventory := {
...
}
theorem inventoryInvariant(
state: Inventory,
event: InventoryEvent,
hValid: ValidInventory(state),
hAccepted: Accepted(event, state)
): ValidInventory(applyEvent(state, event)) := by {
...
}
“If inventory is valid before an accepted event, it remains valid afterward.”
If ValidInventory includes quantity >= 0 for every item, the theorem prevents an accepted transition from creating modeled negative stock.
The bug may require a sequence of reservations, cancellations, retries, and returns. An invariant theorem is not tied to one hand-written sequence.
Warehouse hardware, concurrent database transactions, and external marketplace synchronization remain outside the theorem unless modeled and connected explicitly.
function transfer(state: Ledger, from: AccountId, to: AccountId, amount: Nat): Ledger := {
...
}
theorem transferConservesValue(
state: Ledger,
from: AccountId,
to: AccountId,
amount: Nat,
hAllowed: CanTransfer(state, from, amount)
): TotalBalance(transfer(state, from, to, amount)) = TotalBalance(state) := by {
...
}
“For every allowed modeled transfer, the total balance after the transfer equals the total balance before it.”
This is a conservation law. It is stronger than checking a few example transfers, but it does not by itself prove database atomicity or an external payment processor.
function applyRequest(req: Request, state: State): State := {
...
}
theorem retryIdempotent(req: Request, state: State):
applyRequest(req, applyRequest(req, state)) =
applyRequest(req, state) := by {
...
}
“Applying the same request twice has the same modeled result as applying it once.”
Useful for retry-safe APIs, webhook consumers, provisioning, and job processing.
The theorem is about the modeled state transition. Exactly-once external side effects need additional correspondence/model assumptions.
function parseConfig(text: String): Option(Config) := {
...
}
theorem parsedConfigValid(text: String, config: Config):
parseConfig(text) = .some(config) → ValidConfig(config) := by {
...
}
“Whenever parsing succeeds with
config, that configuration satisfies the formal validity predicate.”
This is different from a round-trip theorem. A good verified parser may need both.
function transition(order: Order, event: OrderEvent): Order := {
...
}
theorem transitionSafe(
order: Order,
event: OrderEvent,
hValid: ValidOrderState(order),
hAccepted: EventAllowed(order, event)
): ValidOrderState(transition(order, event)) := by {
...
}
A rule inside ValidOrderState might encode:
Shipped(order) → Captured(order)
Read:
“Every shipped order must already satisfy the captured-payment condition in the formal model.”
function migrate(old: SchemaV1): SchemaV2 := {
...
}
theorem migrationValid(old: SchemaV1):
ValidV1(old) → ValidV2(migrate(old)) := by {
...
}
A stronger migration may also prove that a compatibility projection recovers the original information:
projectV1(migrate(old)) = old
when that is actually the intended semantics.
This is valuable because migration code can touch every stored record at once.
function authorize(user: User, resource: Resource, action: Action): Decision := {
...
}
theorem noUnauthorizedAllow(user: User, resource: Resource, action: Action):
authorize(user, resource, action) = .allowed →
PolicyPermits(user, resource, action) := by {
...
}
“If the implementation says
allowed, the formal policy must also say the action is permitted.”
This targets one of the highest-value classes of mistakes: an implementation branch that is more permissive than the policy.
Instead of one oversized phrase such as “sort is correct,” use several theorems:
Ordered(sort(xs))
Permutation(xs, sort(xs))
List.length(sort(xs)) = List.length(xs)
If stability matters:
StableRelativeOrder(xs, sort(xs))
Each theorem says something precise. Together they characterize intended behavior more strongly than a single vague correctness label.
Formal verification rewards precise claims.
For every theorem, ask:
What exact proposition was checked?
What assumptions does it use?
Does the proposition characterize the behavior strongly enough?
Which runtime/environment boundaries remain outside the model?
If TypeScript is generated, what correspondence evidence connects it to the checked semantics?
The goal is not to weaken the value of proof. It is to make the value specific and trustworthy.
For the theory behind these examples, see:
Next: Download →