COMPILERS SERIES · PART 05 OF 10

Semantic Analysis
& Type Systems

Part 05 — Meaning, Names & Types: What the Grammar Can't Catch
Symbol Tables Type Checking Hindley–Milner Unification
Resolve names → Build scopes → Type-check → Infer → Annotated AST

After parsing proves a program is well-formed, semantic analysis proves it is well-meaning — resolving every name to a declaration, enforcing scope, and assigning each expression a type. We build symbol tables, write typing rules as judgements, and reconstruct types from scratch with Hindley–Milner and unification

Scope  ·  Attributes  ·  Γ ⊢ e : τ  ·  Inference
01

Topics

The parser handed us a tree that is syntactically legal. This deck makes it meaningful: every identifier bound, every operation type-checked, every static rule the grammar couldn't express now enforced.

Names & Scope

  • Static vs dynamic semantics; what the parser can't catch
  • Symbol tables: structure, entries, name resolution
  • Lexical scope, nesting, shadowing, namespaces

Attributes & Rules

  • Attribute grammars: synthesized vs inherited
  • S- and L-attributed; syntax-directed translation
  • Typing judgements Γ ⊢ e : τ and inference rules

Type Systems & Inference

  • Static/dynamic, strong/weak, nominal/structural
  • Soundness — “well-typed programs don't go wrong”
  • Hindley–Milner, Algorithm W, unification, let-polymorphism

Richer Checks & Diagnostics

  • Subtyping & variance, generics, type classes, coercions
  • Definite assignment, returns, exhaustiveness, borrows
  • Why HM error messages are notoriously hard
02

What Semantic Analysis Does

Parsing answers “is this a sentence of the grammar?” Semantic analysis answers “does this sentence mean anything sensible?” It is the last phase of the front end — its output is an annotated, type-checked AST plus a populated symbol table.

Parser → raw AST Semantic Analysis resolve · scope · type + symbol table typed AST → IR gen type / scope errors

Core Jobs

  • Name resolution — bind each use to a declaration
  • Type checking / inference — give every node a type
  • Static checks — assignment, returns, exhaustiveness

Why a Separate Phase?

These rules are context-sensitive — whether x is in scope depends on declarations elsewhere. Context-free grammars cannot express that, so it lives outside the parser.

Once this phase signs off, the rest of the compiler may assume the program is meaningful — IR generation never re-checks a type.

03

Static vs Dynamic Semantics

A language's semantics — what programs mean — splits into what can be decided before the program runs and what only emerges as it runs.

Static Semantics

Rules checkable at compile time from the program text alone, without running it. The job of semantic analysis.

  • Names are declared before use; types match operators
  • Function arity & argument types agree
  • Every path through a non-void function returns
  • Often called the language's well-formedness rules

Dynamic Semantics

What a well-formed program does when executed — defined by an operational / denotational / axiomatic semantics, enforced at run time.

  • Array index in bounds; pointer non-null; divisor non-zero
  • Order of evaluation; effects; the actual computed values
  • Caught by the runtime (a trap/exception), not the compiler

The Dividing Line Is a Design Choice

The more a language decides statically, the more errors caught before shipping — but the more the programmer must spell out. A null-dereference is dynamic in Java, but moved static in Kotlin/Rust via the type system. Pushing checks left (compile-time) is a recurring theme of modern language design.

04

What a Parser Can't Catch

Every snippet below is a perfectly valid parse tree — grammatically flawless. Each is rejected only by semantic analysis, because the offending property is context-sensitive.

int main(void) {
    y = 3;            // undeclared 'y'
    int x = "hello";  // type error: char* → int
    int x = 7;        // redeclaration of 'x'
    foo(1, 2);        // arity: foo takes 1 arg
    return;           // missing return value
}

The grammar happily accepts all five lines — assignment, declaration and call are each well-formed productions.

ErrorWhy the parser misses it
Undeclared nameNeeds the set of names in scope — not in the CFG
Type mismatchNeeds each subexpression's type
RedeclarationNeeds to know what's already bound here
Wrong arityNeeds the callee's signature
Missing returnNeeds control-flow reachability
Break outside loopNeeds surrounding context

The Theory Behind It

“Variable declared before use” is the classic example of a non-context-free property. You'd need a context-sensitive grammar to encode it — impractical, so we use ad-hoc checks plus a symbol table instead.

05

Symbol Tables — Structure & Entries

The symbol table maps names to everything the compiler knows about them. Lookups must be fast and frequent, so it is usually a hash table — or a stack of them, one per scope.

FieldHolds
namethe identifier (the hash key)
kindvar / function / type / param / field
typeint, (int,int)→bool, …
scopenesting level / owning block
locationstack offset / register / global addr
attrsconst, mutable, visibility, line/col

Operations

insert(name, entry) on declaration; lookup(name) on use — searching the current scope, then enclosing scopes outward. enterScope() / exitScope() push/pop tables.

struct Symbol {
  std::string  name;
  Kind         kind;   // Var, Func, Type, ...
  Type*        type;
  int          scopeLevel;
  Location     loc;    // offset / reg / addr
  Attrs        attrs;  // const, mut, visibility
};

// A scope is one hash table; scopes nest.
struct Scope {
  std::unordered_map<std::string, Symbol> table;
  Scope* parent;   // enclosing scope (or null)
};

Symbol* lookup(Scope* s, const std::string& n) {
  for (; s; s = s->parent)
    if (auto it = s->table.find(n); it != s->table.end())
      return &it->second;   // first hit wins → shadowing
  return nullptr;            // undeclared
}
06

Scope Tree & the Symbol-Table Stack

Nested blocks form a tree of scopes. At any point the live scopes are a stack, root at the bottom. lookup walks the stack outward; the innermost binding wins — that is shadowing.

int g = 0; // global void f(int p) { // fn f int x = p; { // block B1 int x = 9; // shadows x int y = x + g; } return x; // outer x } Global scope g:int · f:(int)→void f's scope p:int · x:int block B1 scope x:int (shadows) · y:int parent parent push on enterScope() top of stack lookup(x) → finds 9 first pop on exitScope() → x=p lookup walks outward

The two x bindings coexist; the reference resolves to whichever scope is nearer on the stack.

07

Name Resolution & Binding

Binding connects each use of a name to the declaration it refers to. Resolution is the algorithm that finds that declaration — and the rules for it are surprisingly subtle.

Lexical (Static) Scope

A name binds to the nearest enclosing declaration in the program text — decidable at compile time. The dominant rule (C, Java, Rust, ML, Scheme…).

Dynamic Scope

A name binds to the most recent definition on the call stack at run time. Rare — early Lisp, Bash, Emacs Lisp specials. Hard to reason about; mostly abandoned.

Forward References

A use that precedes its declaration. Handled by a separate declaration-gathering pass (or two passes): collect all top-level names first, then resolve bodies — so mutual recursion just works.

Namespaces & Overloading

One identifier can name different things in different namespaces (C: tags vs ordinary vs labels). With overloading, a name maps to a set of entries; resolution picks one by argument types.

Why Lexical Won

Under dynamic scope, what x means depends on who called you — you can't read a function in isolation. Lexical scope makes a binding determinable from the source alone, which is essential for both humans and closures.

08

Attribute Grammars

Knuth (1968) gave a formal way to attach meaning to a grammar: decorate each production with attributes and semantic rules that compute them. This is the theory behind syntax-directed translation.

Synthesized Attributes

Computed bottom-up: a node's value is built from its children. A node's type from its operands; a constant's value. Flow up the tree.

Inherited Attributes

Computed top-down: a node's value comes from its parent and/or siblings. The current scope, the expected type, an indentation level. Flow down & across.

S-Attributed

Uses only synthesized attributes. Evaluable in a single bottom-up pass — exactly what a bottom-up (LR) parser can do on reductions.

L-Attributed

Synthesized + inherited, but each inherited attribute depends only on the parent and on left siblings. Evaluable in one left-to-right depth-first pass — fits top-down (LL) / recursive-descent parsing. Every S-attributed grammar is L-attributed.

In practice few compilers run a full attribute-grammar engine; they hand-write tree walks. But the vocabulary — what flows up, what flows down, and the legal dependency order — is exactly how we reason about an AST traversal.

09

Syntax-Directed Translation — an Example

Attaching a synthesized .type attribute to an expression grammar gives a one-pass bottom-up type-checker. Each rule combines its children's types into its own.

E → E1 + E2   { E.type =
                  unify(E1.type, E2.type) }
E → E1 * E2   { E.type =
                  numeric(E1.type, E2.type) }
E → ( E1 )    { E.type = E1.type }
E → num       { E.type = int }
E → real      { E.type = float }
E → id        { E.type =
                  lookup(id.name).type }

.type is synthesized — this grammar is S-attributed, so an LR parser can type-check during reduction.

typing 3 + x * 2.0 (x : float) + :float 3 :int * :float x :float 2.0 :float types flow ↑ bottom-up; int 3 coerced to float
10

Annotating the AST — the Typed Tree

The product of type checking is a typed AST: every expression node carries a resolved type, every identifier a pointer into the symbol table, and implicit conversions are made explicit as Coerce nodes.

double r = n + 1; (n : int, r : double) Assign :double Var r :double → symtab[r] Coerce int→double Add :int Var n :int Lit 1 :int

Implicit Made Explicit

The int result of n + 1 must become a double to store in r. The checker inserts a Coerce node so the back end never has to re-derive the conversion.

Decorations Carried

Each node gains: a resolved type, a symbol reference for identifiers, and sometimes a constant-folded value or an effect annotation.

11

Type Systems — the Axes

A type system is a tractable, syntactic method for proving the absence of certain bad behaviours by classifying values. Languages sit at different points on several independent axes.

AxisOne endOther endExamples
When checkedStatic — at compile timeDynamic — at run timeRust / C++  vs  Python / JS
StrengthStrong — few implicit holesWeak — free reinterpretationHaskell / Python  vs  C
Manifest?Manifest — annotations writtenInferred — reconstructedJava  vs  ML / Haskell
IdentityNominal — by nameStructural — by shapeJava / Rust  vs  TypeScript / Go ifaces

Strong ≠ Static

Orthogonal axes, endlessly confused. Python is dynamically but strongly typed ("a" + 1 raises, never silently coerces). C is statically but weakly typed (casts reinterpret bits). “Strong” is informal; “static” is precise.

Nominal vs Structural

Two records with identical fields are the same type structurally, but different types nominally unless one declares it implements the other. Nominal catches “accidental” matches; structural is more flexible.

12

Soundness — “Well-Typed Programs Don't Go Wrong”

Milner's 1978 slogan is the promise a type system makes: if the checker accepts a program, certain run-time errors cannot happen. Soundness is proved as two lemmas.

Progress

A well-typed term is either a value or it can take an evaluation step — it never gets “stuck” in a meaningless state (like adding a function to an integer).

Preservation (Subject Reduction)

If a well-typed term takes a step, the result is still well-typed, with the same type. Types are an invariant of execution.

Progress + Preservation = Safety

Together they guarantee a well-typed program never reaches a stuck state — the formal content of “don't go wrong”. (Wright & Felleisen, 1994.)

The Two Ways to Fail

  • Unsound — accepts some program that does go wrong (a hole in the system; e.g. Java array covariance throws ArrayStoreException)
  • Incomplete — rejects some program that would have been fine (the conservative price of decidability)

The Fundamental Trade-off

Type-checking a Turing-complete property is undecidable, so every sound, decidable system is necessarily incomplete — it must reject some safe programs. Good design minimises the surprising rejections.

13

Typing Judgements — Γ ⊢ e : τ

Type rules are written as inference rules: premises above the line, conclusion below. The judgement Γ ⊢ e : τ reads “in context Γ, expression e has type τ”. Γ is the typing environment — the symbol table, formally.

(T-Int) Γ ⊢ n : int (T-Var) x : τ ∈ Γ Γ ⊢ x : τ (T-If) Γ⊢e₁:bool  Γ⊢e₂:τ  Γ⊢e₃:τ Γ ⊢ if e₁ then e₂ else e₃ : τ (T-App) Γ ⊢ e₁ : σ→τ    Γ ⊢ e₂ : σ Γ ⊢ e₁ e₂ : τ

Reading the Rules

  • T-Int — a literal needs no premises; an axiom
  • T-Var — look the name up in Γ
  • T-If — condition bool; both branches the same τ
  • T-App — argument type must match the function's domain σ; result is the codomain τ

Type Checking = Proof Search

To type an expression is to build a derivation tree of these rules with the expression at its root. A type checker is a proof searcher; a type error is a failure to find any derivation.

T-Abs (functions)

To type λx:σ. e, extend the context: if Γ, x:σ ⊢ e : τ then Γ ⊢ λx:σ.e : σ→τ. Entering a binder extends Γ — exactly enterScope.

14

A Worked Derivation

Type if b then x else 0 in context Γ = { b : bool, x : int }. We compose the rules into a derivation tree — leaves are axioms, the root is the goal.

b:bool∈Γ Γ ⊢ b : bool x:int∈Γ Γ ⊢ x : int (T-Int) Γ ⊢ 0 : int (T-If) Γ ⊢ if b then x else 0 : int both branches : int ⇒ whole expr : int ✓

Top to Bottom

  • Leaves discharge T-Var / T-Int
  • The if's three premises are satisfied: guard bool, both arms int
  • T-If fires → the whole term is int

Where It Would Fail

Had the else arm been "hi", the two branch premises (int vs string) couldn't both equal one τ — no derivation exists, so it's a type error.

15

Type Inference — Hindley–Milner

The rules above need types written down. Hindley–Milner (HM) reconstructs them: with no annotations it finds the most general type, if one exists. Discovered independently by Hindley (1969) and Milner (1978); Milner's Algorithm W is the implementation.

-- No annotations anywhere...
compose f g x = f (g x)
-- W infers the principal type:
-- compose :: (b -> c) -> (a -> b) -> a -> c

twice f x = f (f x)
-- twice :: (a -> a) -> a -> a

id x = x
-- id :: a -> a   (a is a fresh type variable)

The Algorithm, in Three Moves

  1. Give every unknown a fresh type variable
  2. Walk the AST emitting equality constraints between types
  3. Unify all constraints into one substitution

Principal Types

HM guarantees a principal type: a single most-general type from which every other valid type is an instance. id's principal type is a → a; Int → Int is just one instance.

Why HM Is the Sweet Spot

  • Decidable & efficient (near-linear in practice)
  • No annotations required — full inference
  • Powers ML, OCaml, Haskell, F#, Rust & Swift locals, Elm

The Boundary

HM trades power for decidability: no first-class polymorphism (rank-N), no subtyping. Add those and full inference becomes undecidable — hence annotations creep back in.

16

Unification & the Occurs-Check

Unification (Robinson, 1965) is HM's engine: given two type terms, find the most general substitution making them equal — or fail. It solves the equality constraints W generates.

unify(t1, t2):
  t1, t2 = resolve(t1), resolve(t2)   # follow bound vars
  if t1 == t2:                  return            # done
  if t1 is a var α:    occurs_check(α, t2)
                       bind α := t2;  return
  if t2 is a var β:    occurs_check(β, t1)
                       bind β := t1;  return
  if t1 = C(a1..an) and t2 = C(b1..bn):  # same constructor
       for i: unify(ai, bi)              # arity must match
       return
  fail("cannot unify " + t1 + " with " + t2)

The Occurs-Check

Before binding α := t, verify α does not occur inside t. Binding α := α → β would build an infinite type. The occurs-check is exactly what rejects the untypable λx. x x.

Three Outcomes

  • Same constructor → recurse on arguments pairwise
  • Variable → bind it (after the occurs-check)
  • Clash (int vs bool, or wrong arity) → type error

Most General Unifier

Unification returns the mgu — the substitution that commits to nothing more than necessary. That minimality is precisely why HM yields principal types.

Implementation

Type vars are mutable cells (union-find); resolve path-compresses. This is why HM is near-linear despite the recursion.

17

Unification — a Worked Example

Infer the type of apply f x = f x. Give fresh variables, generate the one constraint f imposes, and unify.

f : α          x : β        (fresh)
apply : α → β → γ           (γ fresh, result)

the call  f x  demands:
   α  =  β → γ        ← f used as a function

unify(α, β → γ):
   α is a var, occurs-check(α, β→γ) ✓ (α ∉ β→γ)
   bind  α := β → γ

substitute back:
   apply : (β → γ) → β → γ
generalise free vars β, γ →  ∀β γ. ...

   apply :: (b -> c) -> b -> c     ✓ principal
unify( α , β → γ ) α a variable β → γ → constructor bind α occurs-check: α ∈ β→γ ? no → safe to bind ✓ mgu = { α ↦ β → γ }
18

Let-Polymorphism & Generalisation

HM's polymorphism lives at let. When a binding is generalised, its free type variables are closed over into a type scheme ∀α. τ — each use then gets fresh copies (instantiation).

let id = \x -> x in
  (id 3, id True)
-- id generalised to  ∀a. a -> a
-- use 1: instantiate a := Int   → id 3
-- use 2: instantiate a := Bool  → id True
-- BOTH type-check ✓
-- λ-bound params are NOT generalised:
\id -> (id 3, id True)   -- TYPE ERROR
-- one monomorphic α can't be both Int and Bool

Generalisation (gen)

At a let, quantify over type variables free in τ but not free in the environment Γ — those are still constrained elsewhere and must stay fixed.

Instantiation (inst)

At each use of a scheme ∀α.τ, replace the bound α with fresh variables — so distinct uses don't constrain each other.

The Value Restriction

Generalising arbitrary expressions is unsound with mutable references (a polymorphic ref leaks). ML restricts generalisation to syntactic values — the famous value restriction.

19

Subtyping & Variance

Subtyping S <: T means an S may stand in wherever a T is expected (Liskov substitution). The hard question: how does subtyping lift through type constructors?

VarianceRule (if S <: T)Where
CovariantF<S> <: F<T>read-only, return types
ContravariantF<T> <: F<S>fn argument positions
Invariantneither directionmutable cells

The Function Rule

(A→B) <: (C→D) iff C <: A (contravariant in the argument) and B <: D (covariant in the result). Arguments flip; results follow.

Why Mutable = Invariant

If Cat <: Animal made List<Cat> <: List<Animal>, you could insert a Dog into the aliased List<Animal> and break the List<Cat>. Java's covariant arrays have exactly this hole — checked at run time via ArrayStoreException.

In Practice

  • Scala +T / -T declaration-site variance
  • C# out / in on generic params
  • Java use-site: ? extends / ? super
20

Generics, Type Classes & Overloading

“Polymorphism” — one name, many behaviours — comes in distinct flavours (Strachey, Cardelli–Wegner). Each is resolved differently by the checker.

Parametric

One body, any type, uniformly: List<T>, fn id<T>(x:T)->T. Generics / ML polymorphism. The code can't inspect T.

Impl: erasure (Java/HM) or monomorphisation (Rust/C++ templates).

Ad-hoc

Different code per type, chosen by type. Overloading & type classes / traits: (+) on Int vs Float, Show a.

Impl: the compiler passes a hidden dictionary of methods.

Subtype

Inclusion polymorphism via S <: T — the OO kind. A Circle works wherever a Shape does, dispatched dynamically.

Impl: vtables / dynamic dispatch.

Type Classes vs Overloading

Haskell's classes are principled overloading: a class declares an interface (class Eq a where (==) :: a→a→Bool); instances supply it; constraints (Eq a =>) thread through inference. Rust traits are the same idea with coherence rules.

Overload Resolution

With C++/Java overloading the checker ranks candidates by how well argument types match (exact > promotion > conversion), preferring the most specific. Ambiguity — two equally good candidates — is an error. This interacts badly with full inference, so HM languages avoid raw overloading.

21

Coercions, Conversions & Aliases

Beyond accepting or rejecting, the checker sometimes inserts work: implicit conversions that bridge nearly-compatible types — the source of much convenience and many bugs.

Implicit Coercions

  • Widening — int → long → double, value-preserving, usually safe & silent
  • Narrowing — double → int, lossy; many languages require an explicit cast
  • Inserted as the Coerce nodes from slide 10

The Hazard

C's integer promotions & usual arithmetic conversions are a notorious footgun — unsigned/signed mixing flips comparisons. Strongly-typed languages keep coercions few and explicit.

Type Aliases vs New Types

An alias (typedef, type X = Y) is the same type under a new name — interchangeable, no safety added. A nominal newtype (Haskell newtype, Rust tuple-struct) is a distinct type, so Metres can't be mixed with Seconds.

Coercion as Subtyping

One can model coercions as a subtype relation with an inserted conversion function. Scala's implicit conversions and C++ converting-constructors formalise exactly this — powerful, and easy to abuse.

22

Other Semantic Checks

Type checking is the headline, but the same phase runs a battery of other flow-sensitive analyses — many are small dataflow problems over the AST/CFG.

Definite Assignment

Every local is written on all paths before it is read. Java & C# reject reads of possibly-unassigned variables.

Reachability & Returns

Code after a return is unreachable; a non-void function must return on every path. A forward CFG dataflow check.

Exhaustiveness

match must cover every case of a sum type; missing variants warn or error. Rust/ML/Swift enforce this — a key safety win.

Const-ness & lvalues

You may not assign to a const / final; the LHS of = must be an lvalue (has an address), not a temporary.

Effects & Purity

Effect systems track I/O / exceptions / async in the type (Koka, checked exceptions, async colouring) — what a function may do, not just return.

Initialisation Order

Use-before-init of globals, cyclic static init, this escaping a constructor — all caught here.

These reuse the symbol table and run as AST/CFG traversals — the attribute-grammar machinery from earlier, applied to control flow rather than types.

23

Frontiers — Borrows, Dependent & Refinement Types

The richest semantic checks push the type system to encode memory safety, resource ownership and even logical specifications.

Rust's Borrow Checker

  • Every value has one owner; ownership moves
  • Either many & shared or one &mut, never both
  • Lifetimes 'a are type variables bounding how long a borrow is valid
  • A flow-sensitive (NLL) analysis — no GC, no use-after-free, at compile time

Dependent Types

Types that depend on values: Vec n A — a vector of statically-known length n. append provably returns length n+m. Idris, Agda, Coq, Lean. Type-checking subsumes proof-checking.

Refinement Types

A base type plus a predicate: { v:Int | v > 0 }. An SMT solver discharges the obligations. Liquid Haskell, F*, Dafny — lighter than full dependent types, still catches off-by-one & division-by-zero statically.

The Common Thread

All three move properties that were dynamic (a crash, a leak, a bad index) into the static type system — the same “shift-left” principle that motivated type checking in the first place, taken to its limit.

24

Diagnostics & Error Recovery

A type system is only as good as its error messages. The cruel irony: the more inference a system does, the harder good diagnostics become — the error surfaces far from its cause.

Why HM Errors Are Hard

  • Unification fails at one constraint, but the real mistake may be anywhere that contributed to it
  • “Cannot unify Int with Bool” names the symptom, not the bug
  • Reported location depends on traversal order — left-to-right bias blames the wrong expression
  • Inferred types can be huge & full of fresh variables (t47 → t48)

Making Them Better

  • Track the provenance of every constraint to point at source spans
  • Prefer the user's annotation as “expected”, inferred as “actual”
  • Suggest fixes; show the minimal conflicting set (type-error slicing)
// Elm / Rust set the bar: span-precise, fix-it
error[E0308]: mismatched types
  --> src/main.rs:3:18
   |
 3 |     let x: i32 = "hello";
   |            ---   ^^^^^^^ expected `i32`,
   |            |             found `&str`
   |            expected due to this
   = help: try parsing with `.parse()`

Error Recovery in the Checker

After an error, don't cascade: assign the broken node an error type (⊥) that unifies with anything and suppresses downstream complaints. One mistake → one message, not fifty.

25

Summary & Further Reading

Key Takeaways

  • Semantic analysis enforces context-sensitive rules the CFG can't — names, types, arity
  • Symbol tables (scoped hash tables) drive name resolution; lexical scope & shadowing fall out of stack lookup
  • Attribute grammars frame the AST walk: synthesized flows up, inherited flows down (S- / L-attributed)
  • Typing rules are inference rules Γ ⊢ e : τ; checking = building a derivation
  • Hindley–Milner reconstructs principal types via fresh vars + constraint unification (with the occurs-check)
  • Let-polymorphism generalises; subtyping, variance, generics & type classes extend the system
  • Soundness = progress + preservation: “well-typed programs don't go wrong”

Exercise

Hand-run Algorithm W on map f xs = case xs of [] -> []; (y:ys) -> f y : map f ys and confirm the principal type (a->b) -> [a] -> [b].

Further Reading

  • Pierce — Types and Programming Languages (TAPL), 2002: the definitive text on type systems, soundness, subtyping & inference
  • Aho, Lam, Sethi & Ullman — the Dragon Book, ch. 5 (SDT) & ch. 6 (type checking)
  • Milner (1978) — “A Theory of Type Polymorphism in Programming”
  • Damas & Milner (1982) — principal-type theorem for Algorithm W
  • Robinson (1965) — the unification algorithm
  • Krishnamurthi — PLAI (free online)

Next → Part 06

Intermediate Representations — lowering the typed AST into three-address code & SSA, control-flow graphs, and the IR that the optimiser will chew on.

Part 05 ✓ → Part 06: Intermediate Representations