notesonly.in

One notebook for every subject — open it anywhere.

Log in

Intermediate Code Generation in Compilers For students.

Intermediate code generation is the compiler phase that translates the abstract syntax tree into a simpler, lower-level representation called intermediate representation (IR) — a kind of portable pseudo-assembly. Instead of jumping straight from your source code to machine code for one specific CPU, the compiler first produces IR that is independent of any processor.

This extra step matters enormously. With one IR, a compiler needs only m front ends (one per language) plus n back ends (one per CPU) instead of m times n direct translators — so 5 languages and 4 CPUs need 9 components instead of 20. IR is also where most optimizations happen, on a clean uniform format rather than on messy source syntax.

Key Concepts

  • Three-address code: the classic IR where each instruction has at most three operands, for example t1 = b * c.
  • Temporaries: compiler-created variables (t1, t2, and so on) that hold intermediate results of complex expressions.
  • Basic blocks: straight-line sequences of IR with no jumps in the middle — the unit that optimizers work on.
  • Quadruples: a table form of three-address code with four fields: operator, two operands, and result.
  • SSA (static single assignment): a modern IR form where each variable is assigned exactly once, making data-flow analysis much simpler.

How It Works

  1. The generator walks the syntax tree bottom-up: for each subexpression it emits IR that computes the value into a fresh temporary.
  2. Complex expressions are flattened — a tree of nested operations becomes a linear list of three-address instructions.
  3. Control flow (if, while) becomes conditional jumps to generated labels, for example "if x < 10 goto L1".
  4. Function calls become argument-passing and call instructions that any back end can lower to the target CPU's conventions.

Worked Example

Generate three-address code for a = b * c + d / e with b=2, c=3, d=10, e=5:

  1. t1 = b * c, so t1 = 2 * 3 = 6.
  2. t2 = d / e, so t2 = 10 / 5 = 2.
  3. t3 = t1 + t2, so t3 = 6 + 2 = 8.
  4. a = t3, so a = 8.

Four simple instructions replace one complex expression, using 3 temporaries. The optimizer can now see each operation individually — it might notice, for instance, that if b and c were constants, t1 could be folded to 6 at compile time.

Common Misconceptions

  • IR runs directly on the CPU. It does not — IR is a compiler-internal format; a back end must still translate it to real machine code.
  • More IR stages are always better. Each level adds engineering cost; most compilers use one well-designed IR (like LLVM IR) for everything.
  • IR must look like assembly. High-level IRs exist too — some keep loops and arrays intact so high-level optimizations stay possible.

Real-World Applications

  • LLVM: the IR behind Clang, Rust, and Swift compilers — one IR serving dozens of languages and CPUs.
  • GCC: uses GIMPLE and RTL as its intermediate languages before machine code.
  • JIT compilers: Java's HotSpot and JavaScript's V8 compile bytecode IR to machine code at run time.
  • Decompilers: tools that recover source-like code from binaries first lift machine code back into IR.

Intermediate code generation bridges the gap between human-friendly syntax trees and machine-specific code. By linearizing expressions into simple three-address instructions, it creates a clean, portable layer where optimization and retargeting become tractable engineering instead of chaos.