Type Checking of Expressions, Statements and Functions
The practical SDD rules a compiler uses to check types in expressions, statements, and functions — each rule stated plainly and shown with a worked example.
How the Rules Work
Type checking is written as a syntax-directed definition (SDD): each grammar production gets a rule that computes a .type attribute, bottom-up from the leaves to the root.
Definitions you need:
E.type— the type of an expression node, computed from its children.lookup(id)— ask the symbol table for the declared type ofid.type_error— a sentinel meaning "type rule violated". If any child istype_error, the parent becomestype_errortoo.s → t— a function type: takes an argument of types, returns typet.void— the type of a correct statement (statements do work; they have no value).
A program is well-typed if no node ends up as
type_error.
Coercion: widening (integer → float) is usually allowed automatically. Narrowing (float → integer) needs an explicit cast.
Type Checking Expressions
An expression has a value, so it has a type. Compute it bottom-up.
Leaves: literals and identifiers
Literals carry their own type. An identifier's type comes from the symbol table; an undeclared name gives type_error.
Arithmetic: + − * /
Plain version: int + int is int; mixing int and float gives float; anything non-numeric (like int + bool) is an error.
Relational: < > <= >= == !=
A comparison always returns boolean, whatever the operand precision.
Logical: && || !
Logical operators demand boolean and produce boolean.
Array access and pointer dereference
Indexing an array of element type t gives t (index must be an integer). Dereferencing pointer(t) gives t.
Worked example: a[i] < 3
Symbol table: a : array(10, float), i : integer.
Type Checking Statements
A statement has no value. If it is correct its type is void; if any rule fails its type is type_error.
Assignment — left and right must match
If widening is allowed, integer on the right may be stored in a float variable.
Conditions must be boolean
Every if/while rule says the same thing: the guard E must be boolean, and the body must be well-typed.
Sequences and declarations
A block S₁; S₂; S₃ is just this rule applied repeatedly — every statement must be void.
Worked example
Symbol table: x : integer, flag : boolean.
Now break it — make the guard x instead of flag:
Type Checking Functions
A function's type is s → t: argument type s, return type t. With several parameters, s is a tuple.
Definition — record the type, check the return
Call — arguments must match the domain
In plain words: id must name a function; the argument list A must match the domain s exactly (count and types); the call's type is the return type t.
Argument lists build a tuple left to right:
Worked example
g : integer × float → boolean. Check g(1, 2.5):
Common call errors
Full Worked Example
Symbol table (already built):
Statement to check: x = f(y) + 3;
The AST
Compute types bottom-up
No node is type_error, so the statement is well-typed.
Two ways it could fail
The Annotated AST
After checking, every node carries its .type. This annotated AST is what the next phase (IR generation) consumes.
Synthesised vs inherited
| Attribute | Direction | Meaning |
|---|---|---|
E.type | up (synthesised) | type built from children |
| return type | down (inherited) | passed into the body so return can check itself |
Almost everything is synthesised (computed from children). The one common inherited attribute is the expected return type flowing down into a function body.
Error recovery
On an error, the compiler records it and sets that node to type_error, then keeps going — so a single pass can report all type errors, not just the first.
Summary
Expressions
| Node | Rule | Result |
|---|---|---|
| literal | inherent | int / float / bool / char |
id | lookup(id) | declared type |
E₁ op E₂ (arith) | both numeric | int if both int, else float |
E₁ < E₂ | both numeric | boolean |
E₁ && E₂ | both boolean | boolean |
E₁[E₂] | array(s,t), index int | t |
id(A) | id:s→t, A=s | t |
Statements (all produce void or type_error)
| Statement | Rule |
|---|---|
id = E | lookup(id) = E.type |
if/while (E) … | E must be boolean, body void |
S₁ ; S₂ | both void |
The three things to remember
- Literals carry their type; identifiers get theirs from the symbol table.
- Expressions compute type bottom-up; statements are
voidortype_error. - A call is well-typed only when the argument types exactly match the function's domain.