From an idea to a language you can read
In the previous guide, SSA form and the phi node lived as ideas inside the middle end — a clean way for the compiler to talk to itself. LLVM IR is what happens when you take that idea and give it an actual written grammar: a real, typed, SSA-based intermediate representation you can print to a file, read with your eyes, and even hand-write. That is the thing that makes LLVM so good to learn from. Most compilers keep their IR locked away in memory as data structures you can only inspect through a debugger; LLVM lets you dump it as text, where each line is one small, regular operation, exactly the three-address shape from before — but now concrete enough to point at.
A few facts about the language anchor everything else. Every value in LLVM IR has a type written right on it — i32 is a 32-bit integer, i8 a byte, i8* a pointer to a byte, and so on — so the IR knows the width and shape of everything it touches, which the back end will need later. Values come in two flavors of name: a percent sign starting a virtual register, like %1 or %sum, of which there is an unlimited supply (you never run out, because mapping them onto real registers is a separate job for the back end), and an at-sign for globals and functions, like @main or @counter. And the IR obeys SSA: each %name is the result of exactly one instruction, defined once and used freely thereafter.
Reading a tiny function in IR
The fastest way to learn IR is to read some. Take a function that adds two integers and returns the sum, compiled with no optimization so the IR stays close to the source. The body is divided into basic blocks — straight-line runs of instructions with one entry at the top and one exit at the bottom, labelled like entry: — and control flows between blocks only by explicit branch instructions (br) at a block's end. At -O0 the compiler is deliberately literal: it allocates stack slots with alloca, store-s the arguments into them, and load-s them back, so the IR mirrors the C almost line for line. It is verbose, but every line is honest about what the unoptimized program does.
C source:
int add(int a, int b) { return a + b; }
LLVM IR at -O0 (verbose: everything goes through the stack):
define i32 @add(i32 %a, i32 %b) {
entry:
%a.addr = alloca i32 ; reserve a stack slot for a
%b.addr = alloca i32 ; reserve a stack slot for b
store i32 %a, i32* %a.addr ; spill argument a to memory
store i32 %b, i32* %b.addr
%1 = load i32, i32* %a.addr ; read a back
%2 = load i32, i32* %b.addr ; read b back
%3 = add i32 %1, %2 ; the actual addition
ret i32 %3 ; return the sum
}
LLVM IR at -O2 (the stack traffic is gone):
define i32 @add(i32 %a, i32 %b) {
%1 = add i32 %a, %b
ret i32 %1
}Look closely at what changed between the two versions, because it is the heart of how optimization begins. The -O0 IR puts a and b in memory and shuttles them through load and store; the -O2 IR has no memory traffic at all. A single early pass called mem2reg (memory-to-register) is responsible: it notices those alloca slots are only ever stored to and loaded from locally, and it promotes them into pure SSA values, inserting phi nodes wherever branches would have required them. This one pass is why the rest of the optimizer can be so clean — almost every later optimization assumes its values live in SSA virtual registers, not in memory, and mem2reg is what gets them there.
What a pass actually is
The optimizer is not one big clever procedure. It is a long line of small, single-minded transformations called passes, and the discipline that holds LLVM together is that each pass takes IR in and produces equivalent IR out. Because the input and the output are the same language, passes compose like beads on a string: you can run them in almost any order, run one twice, drop one, or insert a new one, and the IR is still valid IR at every step. Each pass also has a tidy scope — a function pass sees one function at a time, a module pass sees the whole compilation unit (useful when one function needs to be inlined into another) — which keeps each transformation small enough to be correct and testable on its own.
The ordering matters as much as the passes themselves, because each one sets the table for the next. Run constant propagation and a folded constant turns a conditional branch into an always-taken one; run a simplification pass and the now-unreachable block is deleted; run dead-code elimination and the instructions that fed only that block disappear too. None of these passes is individually dramatic, but the pipeline of them, applied in a deliberate order and often repeated, is what compounds a literal -O0 translation into tight -O2 code. The exact passes and exact order are guides 4 and 5; here the point is the mechanism — a pipeline of equivalence-preserving rewrites.
The pipeline as a whole, and what governs it
Step back from individual passes and the pass pipeline is the named, ordered recipe of which passes run and how many times. LLVM ships standard pipelines keyed to the optimization level you ask for, and they differ enormously. At -O0 the pipeline is nearly empty — barely more than mem2reg — precisely so the IR stays close to your source and a debugger can map machine code back to your lines; that is the whole reason a debug build is built at -O0. At -O2 the pipeline is a long, carefully tuned sequence built up over decades, with whole groups of passes run more than once because one round exposes opportunities for another. -O3 turns a few more aggressive (and code-size-growing) passes loose; -Os and -Oz bias the same machinery toward smaller code instead of faster.
It is worth being honest about how this pipeline is actually built, because it is less magical than it sounds. The order of passes is largely a human-engineered, heuristically tuned schedule — compiler engineers measured what helps on real programs and froze a good order — not a provably optimal plan, and you can construct programs where running the passes in a different order would produce better code. The optimizer is also not guaranteed to find the best possible machine code; it is a set of pragmatic rewrites that usually improve things, run within a time budget. Knowing this keeps your expectations calibrated: -O2 is excellent and safe to trust, but it is engineering, not an oracle.
Why this all stays correct: the as-if rule
A reasonable worry hangs over the whole pipeline: if pass after pass keeps rewriting your code, how do we know the program still does what you wrote? The answer is the same license you met in guide 1, the as-if rule. Every pass is required to preserve the program's observable behavior — its I/O, its writes to volatile objects, its final output — while it is free to change anything unobservable: the order of internal arithmetic, whether a value ever touches memory, whether a temporary exists at all. mem2reg deleting your stack slots, dead-code elimination removing a computation, the whole stack-traffic-vanishing trick from earlier — all of it is legal precisely because none of it is observable. The IR being typed and in SSA is what lets each pass check that its rewrite preserves meaning.
But the as-if rule has the dark twin you were warned about, and it is sharpest exactly here in the pass pipeline. A pass reasons about the abstract machine, which has no defined behavior the moment your program triggers undefined behavior — so a pass is entitled to assume UB simply never happens and to rewrite on that assumption. That is the real mechanism behind "it worked at -O0 and broke at -O2": the extra passes in the -O2 pipeline lean on an assumption your buggy code violates (a signed overflow that can't happen, a null pointer that is never null), and the rewrite they make is valid for every UB-free program but wrong for yours. Guide 5 takes this apart in full as UB-driven optimization; for now, hold that the very rule that makes the pipeline safe for correct programs is what makes it merciless to incorrect ones.