Intermediate Representations

Between the checked AST and the final target sits the IR: a simplified program that is easy to transform, optimize, and translate. Choosing an IR is the middle-end’s one big design decision.

Why an IR at All

Whether a compiler is six lines or six million, it lowers through at least one intermediate form — because IRs make hard things easy.

Reuse Across Fronts and Backs

One IR plus N back-ends means each new target needs no front-end work, and each new language needs no back-end work (LLVM exists because of exactly this).

A Substrate for Analysis

Simplified programs are analyzable programs: dataflow, liveness, and type reasoning all run on the IR, not on the surface syntax.

The IR Ladder

IR ladder: AST, three-address code, SSA, bytecode, and LLVM IR as the production end

Figure 1 — Lower IRs add machine detail; higher IRs keep meaning. Pick the lowest one you can still read.

Trees (the AST itself)

The checked AST is already an IR: readable, debuggable, and directly interpretable (tree-walking). For small DSLs it is the only IR needed.

Three-Address Code

Statements of the form t1 = t2 op t3 — at most one operator per line, temporaries between results. The sweet spot for generating both bytecode and native code.

SSA Form

Every variable is assigned exactly once; joins use phi nodes. This is what makes optimization sound and fast (LLVM IR is SSA — Phase 3).

Stack Bytecode

Instructions push and pop an operand stack (this roadmap’s Bytecode & Codegen topic); compact and trivial to interpret.

Choosing Your IR

DSL-Scale Guidance

Start with the checked AST (free — you already have it) and add lower IRs only when a need appears: a bytecode target, an optimizer, or a native back-end. The IR ladder is climbed downward one rung at a time, never all at once.

Readability Is a Requirement

Every IR worth shipping has a textual dump format: --dump-ir or a dot renderer. If you cannot print the IR, you cannot debug the compiler.

Example: Lowering an Expression

The same expression in two worlds: the AST from Parsing & Trees and the three-address code that will feed Bytecode & Codegen.

AST -> TAC (commented)

# three-address: one operation per line, fresh temporaries
t1 = 2 * 3          ; the multiplication binds tighter
t2 = 1 + t1         ; addition last, by precedence
store result, t2    ; or: PUSH 1 PUSH 2 PUSH 3 MUL ADD

How to Read It

Notice how 1 + 2 * 3 becomes a sequence of two tiny statements: one operator each, no nesting. That flatness is what every later pass (emit, optimize, register-allocate) wants to consume.

Next Steps

Continue Phase 2

With an IR, pick a target: Interpreters walk trees, Bytecode & Codegen flatten to instruction lists, and Code Generation reaches native code.

Resources