Frontend

Type Systems

What the language can prove before it runs. Checking, typing rules, environments, inference, unification, polymorphism, subtyping and variance.

What a Type System Actually Proves

A type system is a lightweight proof system running on a decidability budget. What it proves is a theorem you can state; what it declines to prove is a design decision, not an oversight.

Q · What properties can a type system actually prove about my program before it runs, and what is it structurally unable to prove?
Static and Dynamic Typing, Compared Honestly

Not “safe versus unsafe”. Two placements of the same check, differing on seven axes that each cut both ways — and orthogonal to the strong/weak axis that gets confused with it constantly.

Q · Should I reach for a statically typed language or a dynamically typed one, and what does each choice actually make hard?
Type Checking: Is This Operation Defined for These Operands?

One expression — `1 + "hello"` — asked of eight languages, with eight answers and four of them from statically checked languages that disagree with each other. The answer is a design decision, not a fact about types.

Q · What is a type checker actually doing when it decides whether `1 + "hello"` is allowed?
Typing Rules: Reading the Notation With the Line Through It

Premises above the line, conclusion below, and a name in brackets. Once you can read one aloud you can read a language specification, and the shape of the rule set tells you what the checker’s algorithm has to be.

Q · How do I read the fraction-looking notation in a language specification or a types paper?
The Type Environment: What Γ Is, and Where the Compiler Keeps It

Γ = { x: int, name: string }, and `Γ ⊢ x + 1 : int` says “under those assumptions, this holds”. In a real compiler Γ is not a new structure — it is the symbol table, read by the type checker instead of by the resolver.

Q · What is the Γ in `Γ ⊢ x + 1 : int`, and what data structure is it in an actual compiler?
Type Inference: Leaving the Type Off
▶ lab

`let x = 42` gives `x : int` in every language that has inference at all. The differences start at the second line, and the reason most mainstream languages infer locally rather than globally is error messages, not difficulty.

Q · When can I leave the type off, and why do most mainstream languages only let me do it locally?
Hindley–Milner: Inference Without a Single Annotation
▶ lab

Fresh type variables, constraints, unification, and one clever step — generalization at `let` — buy whole-module inference with a principal type. Then subtyping, overloading and mutable references each break it in a different way.

Q · How does an ML compiler type a whole module with no annotations at all, and why did my language not do that?
Unification, and Why `T = List<T>` Must Fail
▶ lab

Three rules solve every type equation: decompose matching constructors, bind a variable, or fail. The fourth thing the algorithm must do is refuse to bind a variable to a term containing itself — skip that and the type is infinite and the compiler does not terminate.

Q · How does a type checker actually solve `T = List<U>`, `U = int`, and why must `T = List<T>` be rejected?
Parametric Polymorphism and the Theorems You Get Free

A genuinely parametric `identity<T>(x: T): T` can only return its argument. That is not a convention or a code review rule — it is a theorem about the type, provable because the function is forbidden from knowing anything about T.

Q · What does `<T>` actually guarantee, beyond saving me from writing the function twice?
Ad-Hoc Polymorphism: One Name, Different Code

Overloading, operator overloading, type classes, traits, concepts and protocols are one idea: different code per type behind one name. The interesting question is not the syntax but what each does to compilation — resolution, monomorphization or a dictionary.

Q · When one name means different code for different types, who decides which code runs, and when?
Subtyping: What `Dog <: Animal` Licenses

One rule — if `S <: T` then an `S` may appear wherever a `T` was demanded — and it applies to every expression, which is why adding it to a checker is a redesign rather than an addition. The compiler checks the signature; Liskov’s behavioural obligations are checked by nobody.

Q · What does `Dog <: Animal` actually license the compiler to do, and what does it not check?
Variance: Why `List<Dog>` Is Not a `List<Animal>`

A function is contravariant in its argument and covariant in its result; a mutable container must be invariant in its element. Java made arrays covariant anyway, and pays for it with a runtime check on every array store — `ArrayStoreException` is that decision, visible.

Q · If `Dog <: Animal`, is `List<Dog> <: List<Animal>` — and why is the answer usually no?