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

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:

  1. Build your own AST and type checker — LLVM doesn't help here.
  2. Emit IR with IRBuilder, using alloca for locals rather than constructing φ nodes.
  3. Run verifyModule after every function. Malformed IR otherwise crashes deep inside an optimization pass.
  4. Run the standard pipeline via PassBuilder.
  5. 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:

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

References