Part 3 · 1 chapters · ~8 min

Type Checking

Symbol tables and scopes, synthesising types bottom-up, checking against expected types, function signatures and calls, return-path checking, good error messages, type inference (unification and Hindley-Milner in outline), bidirectional checking as in TypeScript and Rust, and soundness.

4

Types computed bottom-up

code
// from src/checker.ts (abridged): the type of an expression, or an error with a position
function typeOf(e: Expr, scope: Scope): Type {
  switch (e.k) {
    case 'num': return 'int';
    case 'bool': return 'bool';
    case 'var': { const t = scope.lookup(e.name); if (!t) throw new CompileError(`unknown variable '${e.name}'`, e.line, e.col); return t; }
    case 'binary': {
      const l = typeOf(e.l, scope), r = typeOf(e.r, scope);
      if (['+', '-', '*', '/', '%'].includes(e.op)) {
        if (l !== 'int' || r !== 'int') throw new CompileError(`'${e.op}' needs two ints, got ${l} and ${r}`, e.line, e.col);
        return 'int';
      } …
    }
  }
}
// let x: int = 1 + true;   →   CompileError "1:16: '+' needs two ints, got int and bool"

Inference: this checker needs annotations on lets and functions. Hindley-Milner inference (ML, Haskell) assigns type variables and solves equations between them (unification), so no annotations are needed. TypeScript and Rust use bidirectional checking: infer where easy, require annotations at function boundaries.

WHAT THE CHECKER CHECKS
errors found before a single instruction runs
namesEvery variable and function isdeclared before use, in scope.operand types+ needs two ints; && needs twobools.assignmentslet x: int = true is an error.callsRight number and types ofarguments.returnsReturn type matches the functionsignature.positionsEvery error reports line:column.
swipe the figure sideways, or tap expand for full screen
1/4
scopes
The checker keeps a stack of scopes (maps from names to types), pushed on blocks and functions, so shadowing and undeclared names are caught.
a stack of mapsundeclared names caught