For decades every serious compiler was a monolith: a front end, optimiser and back end welded together, sharing private data structures, impossible to reuse. Writing a compiler for a new language meant rebuilding the optimiser and every code generator from scratch. LLVM — begun by Chris Lattner in the early 2000s — dissolved that monolith by insisting on one radical discipline: a single, well-specified intermediate representation that every front end targets and every back end consumes. Get your language down to LLVM IR and you inherit, for free, a world-class optimiser and code generation for x86, ARM, RISC-V, GPUs and more.
The payoff is the "hourglass" architecture. Many source languages funnel down through
their front ends into the narrow waist — LLVM IR — and then fan back out to many machine targets. The
waist is the whole trick: because every optimisation and every back end speaks exactly one IR, the
LLVM IR is not bytecode and not source — it is a low-level, RISC-like virtual instruction set with two defining properties that make it a superb optimisation substrate:
i32, double, i8*, [4 x i32] — so passes can reason about sizes, alignment and legality without guessing.phi nodes merge values at control-flow joins. Unlimited virtual registers, so def-use chains are explicit and dataflow is trivial to follow..ll), a compact binary bitcode (.bc), and an in-memory C++ data structure — you can round-trip between them losslessly.
Here is what a small function looks like in the textual IR. Note the explicit types on every operand,
the single-assignment virtual registers %…, and the phi node that reconciles
the two predecessors of the loop-free branch:
Every front end — no matter how different C, Rust and Swift are on the surface — lowers to exactly this kind of typed SSA. From here on, the optimiser neither knows nor cares which language it came from.
LLVM's optimiser is a pipeline of passes, each a self-contained transformation or analysis over the IR, sequenced and scheduled by the pass manager. The crucial distinction:
Passes have dependencies: a transform declares which analyses it needs, and the pass
manager makes sure those are available and — critically — tracks which analyses a transform
invalidates, so stale information is recomputed rather than trusted. A transform runs, possibly
invalidating the dominator tree; the next pass that needs it triggers a fresh computation. This
bookkeeping is exactly what lets dozens of passes compose safely into the pipelines behind
-O2 and -O3. (LLVM's modern "new pass manager" makes this dependency and
invalidation tracking explicit and fast.)
Ordinary compilation optimises one translation unit at a time; a call across a file boundary is opaque, so no inlining or interprocedural reasoning can cross it. Link-time optimisation (LTO) breaks that wall. Each source file is compiled to LLVM bitcode rather than final machine code; at link time the bitcode modules are combined and the optimiser runs across the whole program — now able to inline across files, propagate constants globally, and prune truly-dead functions.
Full LTO merges everything into one giant module (maximally precise, but memory-hungry and serial). ThinLTO is the scalable compromise: it builds a lightweight cross-module summary index, then optimises each module in parallel while importing just the functions worth inlining from others — most of LTO's benefit at a fraction of the cost, which is why it is the production default.
LLVM IR sits at one level of abstraction — roughly, typed scalar assembly. But a compiler for machine
learning, or for a high-level DSL, wants to optimise at higher levels too: tensor operations,
loop nests, dataflow graphs, whole domain-specific constructs that flatten badly into LLVM IR's low-level
world. MLIR (Multi-Level IR), the modern evolution from the LLVM project, answers this
with dialects: a common IR infrastructure in which many co-existing sub-languages —
a tensor dialect, an affine dialect (loops as polyhedra), a gpu
dialect, and ultimately the llvm dialect — live in one framework and are
progressively lowered from high abstraction to low.
The philosophy is the same one that made LLVM win, applied one level up: don't force everything into a single fixed abstraction; provide shared infrastructure (the pass manager, the printer, the verifier, the rewrite engine) that many IRs can reuse, and lower between them in steps. MLIR is the backbone of modern ML compilers (TensorFlow, IREE, the Torch stack) for exactly this reason.
The economic argument for the shared-IR model is worth making concrete. Without a common waist, every
front end needs its own back end for every target — a full
Five languages and six targets: 30 bespoke back ends collapse to 11 reusable pieces. That multiplicative-to-additive collapse is why Clang, rustc, the Swift compiler, Julia, and dozens of research languages all chose to emit LLVM IR rather than build their own back ends.
Two engineering decisions, mostly. First, library design: LLVM was built as a set of modular C++ libraries with clean APIs, not a take-it-or-leave-it monolithic tool — you could embed the optimiser in a JIT, a shader compiler, or a database query engine and use only the parts you needed. Second, a stable, well-documented, textual IR that was pleasant to read, easy to emit, and durable across versions, so front-end authors could target it confidently. Add a permissive licence (which let companies adopt it without friction) and Clang proving the whole stack could match GCC on a real C/C++ compiler, and you get a virtuous cycle: more front ends justified more back-end investment, which attracted more front ends. GCC had long had an internal IR (GIMPLE/RTL), but it was deliberately not exposed as a reusable substrate — LLVM's bet that the IR itself should be a public product is what made the ecosystem.
A tempting misconception is to treat LLVM IR (or its bitcode) like Java bytecode or WebAssembly — a
universal, target-independent artifact you distribute and run anywhere. It is not. LLVM IR carries
target-specific assumptions almost from the start: data-layout strings fix pointer
size, endianness and alignment; the ABI, calling conventions, and even which operations are legal depend
on the target the front end had in mind. As the pipeline lowers toward machine code it becomes
progressively more target-committed. So a .bc file produced for 64-bit ARM is not a clean
drop-in for 32-bit x86. LLVM IR is a compiler's internal waist for optimisation and code
generation — brilliant at that job — not a portable shipping format. When people want genuine "compile
once, run anywhere", they reach for WebAssembly or a VM bytecode, not raw LLVM IR.