A parser hands you a tree; a processor wants a list. Somewhere between the two, a compiler chooses an intermediate representation — a private, machine-agnostic language that is low enough to reason about like assembly, yet high enough to stay portable across every target chip. The workhorse of that middle ground is three-address code (TAC): a linear sequence of instructions, each of which does almost nothing.
The defining rule is right there in the name. Every instruction has at most one operator
on the right-hand side and refers to at most three addresses — typically two operands
and one result. A rich source expression like
Watch a single assignment melt into TAC. The source
holds three operators, so it becomes three instructions, threaded together by two temporaries
Notice what happened. The nested structure of the tree became evaluation order in the list —
the leaves are computed first, and each operator waits until its arguments already sit in a temporary.
This is exactly a post-order walk of the expression tree, emitting one instruction per interior node.
Each temporary is written once and then read; the last instruction lands the answer in the real
variable
TAC is small. A handful of instruction forms covers every construct a source language can throw
at it — arithmetic, assignment, branching, arrays, pointers and procedure calls. Here is the canonical
repertoire (the Dragon Book's set), written with symbolic addresses
| Form | Shape | Meaning |
|---|---|---|
| Binary assignment | x = y op z | arithmetic/logical op on two operands |
| Unary assignment | x = op y | negation, logical not, type conversion |
| Copy | x = y | move a value with no operator |
| Unconditional jump | goto L | transfer control to label L |
| Conditional jump | if x relop y goto L | branch when the relation holds |
| Conditional jump (unary) | if x goto L / ifFalse x goto L | branch on a boolean |
| Label | L: | a named jump target (not executed) |
| Procedure call | param x … call p, n | push n args, then call p |
| Return | return y | return (optionally a value) |
| Indexed copy (read) | x = y[i] | fetch element at offset i |
| Indexed copy (write) | x[i] = y | store into element at offset i |
| Address-of | x = &y | take the address of y |
| Pointer read | x = *y | load through pointer y |
| Pointer write | *x = y | store through pointer x |
That is essentially the entire language. Everything else — a while loop, a
An address in TAC is one of exactly three things, and keeping them straight is half the game:
Temporaries are the connective tissue of TAC. Because each holds a single sub-expression's value, an
optimiser can treat "the value in
Because TAC is so regular, you can execute it directly with a short interpreter — a program counter, a
map of variable values, and a dispatch on the instruction form. Below, we hand-write a TAC program that
computes gotos do the work that a
while would in the source.
Run it: the same nine-instruction program prints sum = 55 and then sum = 5050.
The loop is nothing but two labels and a conditional jump — precisely the shape into which a source
while is lowered, which is the subject of a
The number three is not sacred; it is convenient. A single binary operation
x86's
add x, y meaning x = x + y) and save space, but they destroy an input and
muddy analysis. One-address codes are stack machines: elegant, but every value's location is implicit in
the stack depth. Three-address code hits the sweet spot where every value has an explicit name,
which is exactly what an optimiser wants to talk about.
The single most common misreading of TAC is to see t1 through t500 — as if the machine had infinitely
many registers. That is deliberate: at IR level you should not be worrying about the target's
real register file (which might have only 16 or 32 registers). Deciding which virtual temporaries live in
real registers, and which must spill to memory, is a completely separate later phase called