Yasir Explains/Compiler Design/Syntax-Directed Translation/Type Checking
Syntax-Directed Translation

Type Checking

On this page

What is Type Checking?Type Checking as an SDDStatic vs. Dynamic CheckingStrong, Weak, and Sound SystemsType Rules as Inference RulesSynthesis, Inference, and ErrorsKey Points
Syntax-Directed Translation

Type Checking

Type checking is the semantic-analysis pass that walks the AST and verifies every operation receives operands of compatible types, using a formal type system.

What is Type Checking?

Definition. Type checking is the semantic-analysis pass that verifies every operation in a program receives operands whose types are permitted by the language. It runs after parsing, on the AST.

After parsing, the program is structurally correct but may still be meaning-wrong. Adding two integers is fine; multiplying a pointer by a string is not. The type checker enforces those rules.

Example
Source → Lexer → Parser → AST → SEMANTIC ANALYSIS → IR → …
└─ Type checking lives here

It takes the AST plus the symbol table (the compiler's dictionary of declared names and their types) and produces an annotated AST where every node carries a verified type.


Two more definitions

Type system — a set of rules that assigns a type expression (e.g. int, float, int → int, int[10]) to every construct in a program. A type checker implements a type system.

Type error — applying an operation to operands whose types the type system does not permit.

Type Checking as an SDD

Type checking is syntax-directed. The checker attaches one synthesized attribute, type, to every AST node and computes it bottom-up: type the children first, then derive the parent.

Example
(+) → type = int
/ \
a 2
type=int type=int

The rule used above: if both children are int and the operator is +, the result is int.

Leaves that are identifiers have no children, so their type is looked up in the symbol table instead of synthesized.

Example
Symbol Table Node How type is obtained
a │ int Integer literal always int
b │ float Float literal always float
Identifier look up name in symbol table
BinaryOp apply operator's rule to child types
Assignment check RHS matches LHS; type = LHS
FunctionCall check args; type = return type

This is exactly a post-order, S-attributed SDD walk of the AST.

Static vs. Dynamic Checking

Languages differ in when they check types.

Static type checking — types are checked at compile time, before the program runs. (C, Java, Go, Rust, TypeScript.)

Dynamic type checking — types are checked at run time, as each operation executes; values carry a type tag. (Python, JavaScript, Ruby, Lisp.)

Example
Property Static Dynamic
─────────────── ──────────────── ──────────────────
When checked Compile time Run time
Errors found Before running Only when code runs
Runtime cost None (for types) Tag check per op
Examples C, Java, Rust Python, JavaScript

Example — same bug, different time:

Example
Java (static): int x = "hi"; → rejected at compile time
Python (dynamic): y = 5 + "hi" → TypeError only when this line runs

Strong, Weak, and Sound Systems

Three properties describe how strict a type system is. They are independent of static vs. dynamic.

Strongly typed — the language never silently reinterprets a value's bits as another type.

Weakly typed — implicit reinterpretations are allowed.

Example
Python (strong): 5 + "3" → TypeError, no silent coercion
JS (weak): 5 + "3" → "53", number silently coerced to string
C (weak): a pointer can be cast to int and used in arithmetic

Strength is independent of timing: Python is strongly but dynamically typed.

Sound — a type system is sound if well-typed programs cannot go wrong: if a program passes the static checks, no type error can happen at runtime.

Example
Sound: ML, Haskell, (mostly) Java
Not sound: C — a bad cast is undefined behaviour

Why soundness matters: a sound, static system needs no runtime type checks for the properties it guarantees, so it can emit faster code.

Type systemWhat the compiler does
Sound + staticCheck once; emit code with no type-tag overhead
Unsound / dynamicEmit runtime checks; carry type tags in memory

Type Rules as Inference Rules

A type system is specified as type rules, each written like a logical inference rule.

Example
premise₁ premise₂
────────────────────── (rule name)
conclusion

Read it as "if all premises hold, the conclusion holds." The form ⊢ e : T means "expression e has type T."

Example 1 — integer addition:

Example
⊢ e₁ : int ⊢ e₂ : int
────────────────────────────── (T-AddInt)
⊢ e₁ + e₂ : int

To check a + 2 where a : int: left is int (premise 1), right is int (premise 2), so a + 2 : int.

Example 2 — if-else:

Example
⊢ e : boolean ⊢ s₁ : void ⊢ s₂ : void
───────────────────────────────────────────── (T-IfElse)
⊢ if (e) s₁ else s₂ : void

The condition must be boolean — not merely "truthy".

Example 3 — assignment:

Example
x : T ∈ SymbolTable ⊢ e : T
───────────────────────────────────── (T-Assign)
⊢ x = e : T

The RHS type must equal x's declared type.

Each rule maps directly to one case in the recursive checkType function — that is why the notation is useful.

Synthesis, Inference, and Errors

Type synthesis — compute a node's type bottom-up from its sub-expression types, using the type rules. Requires that variables are declared (it looks them up). This is what C and Java do.

Example
synthesize (a + 2) * b where a : int, b : int
a+2 : int (int + int)
(a+2)*b : int (int * int)

Type inference — the programmer writes no annotations; the compiler deduces types from usage. (ML, Haskell, parts of Rust/TypeScript, via the Hindley-Milner algorithm.)

Example
let x = 5 + 3 ← no annotation
5 : int, 3 : int → + yields int → infers x : int

This course focuses on synthesis; full inference is an advanced topic.


Error handling: the type_error type

When a rule fails, the checker must not crash. It assigns the special type type_error to the bad node and keeps going (so it can report many errors in one pass).

Key rule. If any operand has type type_error, the result is also type_error — and no new error is reported.

This stops one real error from cascading into spurious follow-on errors.

Example
Source: result = (x + "hello") * b;
(x + "hello") → type_error ← ONE real error: int + string
type_error * b → type_error ← propagated, no error reported
result = ... → type_error ← propagated, no error reported
Result: exactly one error message.

Next topic: Type Checking — Expressions, Statements, and Functions works through the concrete rules, coercions (int → float), and annotated-AST examples for each construct.

Key Points

  • Type checking (semantic analysis) verifies every operation gets operands of permitted types. A type system is the rule set; a type checker implements it.

  • It is syntax-directed: a type synthesized attribute computed bottom-up, with the symbol table supplying identifier types — an S-attributed SDD.

  • Static = checked at compile time (C, Java, Rust); dynamic = checked at run time (Python, JavaScript).

  • Strong = no silent reinterpretation; weak = implicit casts allowed; sound = well-typed programs cannot go wrong (so no runtime checks needed). Strength is independent of timing — Python is strong but dynamic.

  • Type rules are inference rules (premises over a line, conclusion below); each maps to one case in checkType.

  • Synthesis computes types bottom-up from declarations (C, Java); inference deduces them from usage (Hindley-Milner: ML, Haskell).

  • On a failed rule, assign type_error and propagate it silently upward to avoid a cascade of spurious messages.