10.1 Why Compilers Use IR
A frontend understands meaning. A backend understands a machine. Between them sits the intermediate representation (IR): a program form designed for analysis, transformation, and retargeting rather than for humans or hardware.
You could, in principle, translate an AST straight to machine code. Small toy compilers do exactly that. But production compilers almost never do, and the reason is structural, not stylistic.
The N times M Problem
Suppose you support source languages and target architectures. Translating each language directly to each machine needs backends. Add one new CPU and you owe new code generators.
A shared IR collapses this. Each language lowers once into IR ( frontends), each machine consumes IR once ( backends), giving components instead of .
The optimization payoff is even bigger: write a pass once on IR and every language and every target benefits.
What Makes a Good IR
A useful IR is target-neutral, explicit, and easy to inspect. It exposes operations, data flow, and control flow without committing to one CPU's registers or ABI.
That neutrality is what lets constant folding, dead-code elimination, and common-subexpression elimination run once and serve everyone. Couple IR to a single chip and the abstraction collapses.
High, Mid, and Low IR
Real compilers rarely use a single IR. They use a tower of them, each lower and closer to the machine:
- High-level IR keeps language-ish constructs: array indexing, method calls, bounds checks. It is great for source-aware optimizations.
- Mid-level IR is the classic instruction-list form (three-address code, SSA). This is where most general optimization happens.
- Low-level IR mirrors the target: registers, memory addressing, machine-like operations. This is where instruction selection and register allocation live.
LLVM IR, GCC's GIMPLE/RTL, and the bytecode in JVM/.NET are all points on this spectrum. Lowering is the process of moving down one level, discarding abstraction in exchange for control.
A Concrete Walkthrough
Take total = price * qty + tax. As high IR it may stay close to source. Lowered to three-address mid IR it becomes:
t1 = price * qty
total = t1 + taxLowered further, price and qty become register or stack references, and the two operations become target multiply/add instructions. The same statement exists at three altitudes, each useful for a different job.
The Cost of No IR
Without an IR you do not just lose reuse, you lose a stable place to think. Optimizations need to ask "is this value constant?" or "is this code reachable?" Those questions are awkward on an AST and brutal on raw assembly. An explicit IR gives every pass one well-defined vocabulary to read and rewrite.