The
This page is a map of that landscape: the levels of abstraction an IR can sit at, the difference between graphical and linear IRs, and why the journey from source to machine is best travelled as a staircase of representations rather than a single leap. We call each downward step lowering.
Sort IRs by how much they still resemble the source versus the target. Cooper & Torczon call this the IR's level; it is the single most useful axis for placing any IR you meet.
| Level | Typical form | Keeps | Hides | Real example |
|---|---|---|---|---|
| High-level | abstract syntax tree (AST) | source structure: loops, expressions, scopes | address computation, control flow as jumps | a parser's AST; Java's early tree |
| Mid-level (linear) | three-address code, bytecode, SSA | a simple explicit operation sequence with temporaries | the exact target instructions and registers | LLVM IR, JVM bytecode, GCC GIMPLE |
| Low-level | near-machine ops / RTL | registers, addressing modes, machine idioms | almost nothing — one step above assembly | GCC RTL, LLVM MIR |
A high-level IR is where source-shaped optimisations live (e.g. array-bounds reasoning, which needs to know an array access is an array access). A low-level IR is where machine-shaped optimisations live (instruction scheduling, register allocation). The mid-level is the sweet spot for the great general-purpose optimisations — constant folding, dead-code elimination, common-subexpression elimination — which is exactly why the industry's shared IRs (LLVM IR, GIMPLE) live there.
Cutting the space a different way: an IR is either a graphical structure (nodes and edges you traverse) or a linear sequence (a list of instructions, like a tiny assembly program). Most compilers use both, at different moments.
These are complementary, not rival. A compiler typically builds a graphical IR (an AST, then a CFG whose blocks hold linear TAC), analyses the graph, and finally walks it to emit a linear form for the back end. The graph is good for reasoning about relationships; the list is good for reasoning about order.
Consider the humble expression
Each representation is better than its neighbour for some specific job, and no single IR is best at all of them — which is precisely why compilers descend the staircase instead of committing to one level. The tree is closest to the programmer's intent; the DAG exposes shared work; the linear code is ready for the back end. Optimising is largely a matter of choosing the right rung to stand on.
All three are real mid-level linear IRs you can print and read. LLVM IR is a typed,
SSA-form three-address code: %t = fmul float %rate, 6.0e1 — infinitely many virtual
registers, explicit types, one operation per line. Java bytecode is a
stack machine: to compute rate * 60 it emits fload rate; ldc 60.0; fmul
— no register names at all, operands flow through an operand stack, which keeps class files compact and
portable. GCC's GIMPLE is a three-address form GCC lowers its parse trees into before
optimising, later dropping further to the low-level RTL for the back end. Three different
languages, three different IRs — but all sitting at the same mid-level rung, for the same reason: it is
the best place to optimise.
A tempting misconception is that the IR is simply the target assembly produced ahead of time — so a
compiler could skip the IR and emit machine code directly. Two things are wrong with this. First, a good
mid-level IR is deliberately machine-independent: it has an unbounded supply of virtual
registers, no fixed calling convention, no target opcodes. That independence is the whole point — it is
what lets one optimiser and many back ends share it (the
Second, an IR usually carries more semantic structure than assembly, not less: types, that a value is a pointer, that a block is a loop header, that two names cannot alias. Assembly has forgotten all of that. So an IR is not "assembly early" — it is a distinct, richer, more abstract language chosen to make analysis and retargeting possible. Emitting assembly directly from an AST is possible for a toy compiler, but it forfeits every optimisation the mid-level exists to enable.