LLVM
LLVM solved an N×M problem. Before it, supporting M languages on N architectures meant writing N×M back ends, and every new language reimplemented optimization from scratch. LLVM defines one well-specified intermediate representation in the middle: a front end lowers a language to LLVM IR, and LLVM handles optimization and code generation for every supported target.
The result is that Rust, Swift, Julia, Zig, Clang, Haskell (via an LLVM backend), and dozens of research languages all get a mature optimizer and backends for x86, ARM, RISC-V, WebAssembly, PowerPC, and more, for the cost of emitting a well-documented IR. That's an enormous amount of leverage for a language implementer — arguably the single largest reason new systems languages have become viable to build.
The costs are real too: LLVM is a large dependency, compilation is slow relative to a purpose-built backend, and the IR has semantics (undefined behavior, poison values, attribute-driven optimization) subtle enough that getting them wrong produces miscompilation rather than an error.
TL;DR
- LLVM is compiler infrastructure, not a compiler — a well-specified IR plus optimization and backends.
- LLVM IR is typed, SSA-form, and target-independent-ish: modules, functions, basic blocks, instructions.
- Write a front end that emits IR; you inherit optimization and every target.
- The pass pipeline runs analysis and transform passes;
-O2/-O3are curated pipelines. - Clang is LLVM's C/C++/Objective-C front end;
lldthe linker,libc++the standard library. - ORC JIT exposes the same infrastructure for runtime compilation.
- LLVM is slow to compile with — which is why Go, Zig's self-hosted backend, and Cranelift exist.
- IR undefined behavior and poison semantics are subtle; violating them causes miscompilation.
Quick Example
The same function in C, in LLVM IR, and emitted programmatically:
Note nsw ("no signed wrap"). It tells LLVM that signed overflow is undefined, which licenses optimizations like assuming a + 1 > a. Emitting it when your language defines wrapping behavior is a miscompilation waiting to happen — this class of mistake is the main hazard of writing an LLVM front end.
Core Concepts
The structure
Everything is in SSA form — each value assigned exactly once, with φ nodes at control-flow merges. See ASTs & IRs.
A front end usually doesn't construct φ nodes directly. The standard trick is to emit alloca stack slots with loads and stores, and let the mem2reg pass promote them to SSA registers. It's dramatically easier and produces the same result.
The type system
The move to opaque pointers was a significant change: ptr carries no pointee type, so getelementptr and load state the type explicitly. Older tutorials and code using i32* are pre-LLVM-15 and won't compile against a current release.
Key instructions
Attributes are how a front end communicates guarantees. readnone (no memory access), nounwind (cannot throw), noalias on a parameter (Rust's &mut maps to this), and nonnull all unlock optimizations LLVM otherwise cannot prove. Much of Rust's performance advantage over equivalent C comes from being able to emit noalias where C cannot.
The pass pipeline
Order matters enormously. inline before gvn exposes cross-function redundancy; mem2reg before anything else is what makes the rest effective. The -O2 and -O3 pipelines are curated orderings refined over many years, and hand-assembling a sequence rarely beats them.
Writing a front end
The practical sequence:
- Build your own AST and type checker — LLVM doesn't help here.
- Emit IR with
IRBuilder, usingallocafor locals rather than constructing φ nodes. - Run
verifyModuleafter every function. Malformed IR otherwise crashes deep inside an optimization pass. - Run the standard pipeline via
PassBuilder. - Emit an object file with
TargetMachine, then link.
Bindings exist for most languages — the C API (stable across versions), inkwell for Rust, llvmlite for Python, and llvm-hs for Haskell. The C++ API is the most complete and changes between major releases.
Debug information
Emitting DWARF metadata is what makes a debugger show your source rather than disassembly, and it's what makes profilers attribute samples to source lines. It's fiddly, it's usually deferred, and deferring it means every user of your language debugs at the assembly level. Worth doing earlier than feels necessary.
The Toolchain
The sanitizers deserve separate mention as the most widely used part of LLVM outside of compilation itself: AddressSanitizer, UndefinedBehaviorSanitizer, ThreadSanitizer, and MemorySanitizer instrument code to catch memory errors, UB, and data races at runtime. They're standard practice in C and C++ development and available through GCC as well.
ORC JIT
ORC (On-Request Compilation) is LLVM's JIT API, supporting lazy compilation, concurrent compilation, and cross-process execution. Julia uses it as its primary execution model, and it's how a REPL for a compiled language works. See JIT Compilation.
When Not to Use LLVM
Alternatives that exist for good reasons:
- Cranelift — designed for JIT and fast compilation. Used by Wasmtime and available as a Rust debug-build backend. Far faster to compile, less optimization.
- QBE — a small backend with roughly 70% of LLVM's optimization in a tiny fraction of the code.
- A custom backend — Go's and Zig's self-hosted backends exist primarily because compile speed was a first-class goal.
- Transpiling to C — surprisingly effective: you inherit every C compiler's optimization and every platform, at the cost of debugging through generated code.
The pattern in newer projects is increasingly both: a fast backend for debug builds and LLVM for release. Zig and Rust have both moved this direction.
Best Practices
Verify after every function you emit
verifyFunction and verifyModule catch malformed IR immediately. Without them, a mistake surfaces as a crash inside an optimization pass with a stack trace that tells you nothing about your front end.
Use alloca plus mem2reg rather than constructing φ nodes
Emit a stack slot per local with loads and stores, then let mem2reg build SSA. It's vastly simpler than tracking dominance frontiers yourself and produces the same result. This is the approach the official Kaleidoscope tutorial takes for exactly this reason.
Only emit attributes and flags you can guarantee
nsw means signed overflow is undefined. noalias means the pointer genuinely doesn't alias. readnone means no memory access at all. Emitting one that your language doesn't guarantee produces miscompilation that appears only at -O2, only sometimes, and is extremely hard to diagnose.
Read the IR at every optimization level
clang -S -emit-llvm -O0 and -O2, side by side, is the fastest way to understand both LLVM and your own front end. Compiler Explorer makes this instant and is genuinely one of the best learning tools available.
Emit debug info earlier than feels necessary
Retrofitting DWARF metadata means touching every emission site. Without it, every user of your language debugs at the assembly level and every profiler reports addresses.
Pin the LLVM version
The C++ API changes meaningfully between major releases; opaque pointers alone broke a great deal of existing code. Pin a version, upgrade deliberately, and use the C API if long-term stability matters more than completeness.
Use the standard pipeline
default<O2> encodes years of tuning across an enormous range of code. Hand-assembling a pass sequence to beat it is possible for a narrow workload and usually a waste of effort.
Consider a fast path for debug builds
If compile time matters — and for developer experience it does — a simple direct backend or Cranelift for unoptimized builds, with LLVM for release, is a well-trodden approach.
Common Mistakes
Emitting nsw/nuw without the guarantee
Skipping verification
A basic block without a terminator
Using typed pointers on a current LLVM
Reimplementing optimizations in the front end
Ignoring compile time until it's a crisis
FAQ
Is LLVM a compiler?
No — it's the middle and back end. Clang is the compiler; LLVM is the infrastructure Clang and Rust and Swift and Julia all sit on top of. The name is historical: it originally stood for "Low Level Virtual Machine," which the project explicitly abandoned because it describes neither what it is nor what it does.
How hard is it to write an LLVM front end?
For a toy language, a weekend — the official Kaleidoscope tutorial builds one end to end. For a real language, the LLVM part is genuinely the easy half; your parser, type system, name resolution, error messages, and standard library are far more work. Getting IR semantics right (attributes, UB, poison) is the part that takes longest to learn.
Why is LLVM slow?
It does a great deal of work: dozens of passes, each traversing the IR, plus register allocation and scheduling in the back end. The design prioritizes generated code quality over compile speed, which is the right call for release builds and the wrong one for iteration. This is precisely why Cranelift, QBE, and language-specific backends exist.
LLVM or GCC?
For consuming a compiler, both are excellent and produce comparable code; the differences are in specific target support, license (Apache 2.0 with LLVM exceptions vs. GPL), and diagnostic quality (Clang's error messages set the standard and GCC has closed much of the gap). For building a compiler, LLVM — its API is designed to be used as a library, which GCC's historically was not.
What does Rust get from LLVM specifically?
Optimization, every target, and one thing worth naming: Rust's ownership rules let it emit noalias on &mut references, telling LLVM that a mutable reference cannot alias anything else. C compilers cannot generally prove this, which is why restrict exists and is rarely used. Where it applies, this enables optimizations unavailable to equivalent C — one of the concrete ways the borrow checker pays for itself in performance rather than only in safety.
Can I use LLVM from a language other than C++?
Yes. The C API is stable across versions and complete enough for most front ends. Idiomatic bindings exist for Rust (inkwell), Python (llvmlite, used by Numba), Haskell (llvm-hs), Go, and others. The C++ API is the most complete and the least stable — the tradeoff is capability against churn at every major release.
Related Topics
- Code Generation — What LLVM does after your IR
- ASTs & Intermediate Representations — SSA form and the IR you emit
- JIT Compilation — ORC and runtime compilation
- Type Systems — The front-end work LLVM doesn't do
- Lexers & Parsers — The other half of a front end
- Rust — An LLVM front end, and
noaliasin practice - C · C++ — Clang's languages
- WebAssembly — An LLVM target
- Systems Programming — Where reading IR is useful