Every time a compiler rejects "hello" + true or a call with the wrong number of arguments, it
has just proved a little theorem about your program — before it ever runs. That is the quiet magic of a
type system: it is a lightweight, automatic correctness proof, run on every program, that rules
out a whole class of disasters. Robin Milner distilled the promise into a slogan in 1978:
"well-typed programs don't go wrong."
This page is about what that slogan means precisely, and how you actually prove it. "Go wrong"
isn't a mood — it is a formal notion of a stuck, nonsensical state. And the guarantee that well-typed
programs never reach one is a genuine theorem, type soundness, with a two-part structure
that has been the template for language design ever since. It sits atop ordinary
To prove programs don't go wrong we must first say, exactly, how they run and what "wrong" is. A
small-step operational semantics defines a relation
A stuck state is the formal ghost of a segfault, a NoMethodError, or the C-style undefined
behaviour where a bit pattern is used as the wrong kind of thing. Type safety is precisely the promise
that a well-typed term never lands in the third case.
Write
Now chain them. Start well-typed. By Preservation, every step keeps you well-typed. By
Progress, at every well-typed point you are either done (a value) or can step again. So
you can never reach a stuck non-value: the two lemmas interlock into an induction over the whole
evaluation sequence. That is the entire proof of "well-typed programs don't go wrong" — a
Both lemmas are proved by induction on the structure of typing derivations, using inference rules of the
same shape you met in
The left rule says a lambda has an arrow type when its body has the result type assuming the
parameter's type (the context
Yes — and this is the deep idea. A typing derivation
A type system that admits a stuck state is unsound — its "proof" has a bug, and a well-typed program can still go wrong. Two famous holes in mainstream languages:
Object[] a = new String[1]; a[0] = 42; type-checks — but storing an
Integer into a String array must fail. Java patches the hole with a
runtime ArrayStoreException: the static type system alone is unsound here, and a
dynamic check papers over it.null inhabits every reference type, a
well-typed String s = null; s.length(); passes the type checker yet crashes — Tony
Hoare's self-described "billion-dollar mistake." The type says "a String", the value is nothing, and
the method dispatch goes wrong. Modern languages close this with non-nullable types and explicit
Option/? types.
Both are Preservation failures in spirit: a step (an array store, a method dispatch) lands in a state the
static type promised couldn't happen. Studying them is the best way to feel why soundness is a
theorem to be proved, not a property to be assumed — every escape hatch (a cast, covariance,
null, C's unchecked unions) is a hole an adversary can drive a stuck state through.
Soundness is a modest promise, and over-reading it is the classic mistake. It guarantees your program
won't reach a stuck state — no adding a boolean to a function, no calling a number. It says
nothing about whether your program computes the right answer: a perfectly
well-typed function add(a: number, b: number) { return a - b; } is type-safe and utterly
wrong. "Doesn't go wrong" means "doesn't get stuck", a specific technical fate — not "is bug-free".
Nor does it forbid non-termination or logic errors or exceptions the language chooses to define. The type
checker proves one small theorem for free; proving your program does what you meant is the job of
the heavier machinery — contracts, Hoare logic, and verification — elsewhere in this course.