Middle-end

Intermediate Representation

The representation the middle-end is written against. Why an IR exists at all, how many levels of it there are, and what lowering means at each step.

What an Intermediate Representation Is
▶ lab

Between the type checker and the code generator sits a third representation that belongs to neither: a flat sequence of simple instructions over an unlimited supply of virtual registers, grouped into blocks. It is not source, it is not machine code, and almost every interesting thing a compiler does happens there.

Q · What is an IR, and why is there a whole extra representation between my typed program and the machine code?
Why an IR Exists: M x N Becomes M + N
▶ lab

Six languages and five targets is thirty compilers if every frontend talks to every backend directly. Put one representation in the middle and it is eleven components. That arithmetic is the entire argument, and it is why the middle of a compiler is a public interface.

Q · Why not just generate machine code straight from the typed tree, and skip a whole representation?
Levels of IR: High, Mid and Low
▶ lab

Rust has HIR, THIR, MIR and then LLVM IR. That is not indecision. Each level answers a question the level below it can no longer phrase, and each lowering discards something on purpose — which is precisely why the earlier level had to exist.

Q · Why do real compilers have three or four IRs instead of one, and how do I know which one a pass should run on?
Three-Address Code
▶ lab

`x = a + b * c` becomes `t1 = b * c; t2 = a + t1; x = t2`. The rewrite looks like busywork until you notice that `t1` is a *name* — and that every analysis in the middle-end is a statement about names.

Q · Why does the compiler invent temporary names for values I never named, and what would break without them?
Lowering
▶ lab

Lowering is the verb the whole middle of a compiler runs on: replace a construct with a simpler one that has the same defined behavior, and repeat until nothing is left but jumps, arithmetic and memory. A `for` loop, a closure, a `match` and an `await` are all the same kind of problem.

Q · What does "lowering" actually mean, and how does a high-level feature like `match` or `async` become ordinary jumps?
Designing an IR: The Four Decisions
▶ lab

SSA or not, typed or untyped, how much target detail to admit, and linear or graph. LLVM IR, Cranelift CLIF, GCC GIMPLE and V8 TurboFan answer those four differently and all four are correct — because they were built to be fast at different things.

Q · If I were designing an IR, what are the decisions, and what does each one actually cost me?
IR Verification
▶ lab

A verifier is a function that rejects malformed IR. Its value is not that it finds bugs — it is that it finds them at the pass that caused them, instead of three passes later in a code generator that had every right to assume otherwise.

Q · Why does a compiler check its own intermediate representation, and what exactly is it checking?
Many Frontends, One Backend
▶ lab

Clang, rustc, swiftc, flang, Julia and Zig do not share a parser, a type system or an opinion about memory. They share an optimizer and a set of code generators, because all six agreed to emit the same instruction set — and that agreement is what LLVM actually sells.

Q · How do six languages with nothing in common end up sharing an optimizer, and what does each of them give up to get it?