On this page
LLVM IR in Practice
LLVM IR is not just a textbook example of SSA—it is the common "target language" for dozens of compiler frontends such as Clang, Rustc, and Swiftc. It is fully SSA, strongly typed, and uses three-address code. Understanding the syntax and memory model of IR is essential to implementing your own codegen from AST to LLVM.
Overview
SSA Form covers the mathematical foundations of SSA—dominator trees, φ functions, and the Cytron algorithm. This article puts it into practice with LLVM IR: it is LLVM's intermediate representation, fully SSA, strongly typed, and in three-address code form. It serves as the common target for dozens of compiler frontends like Clang, Rustc, and Swiftc, and is the input for LLVM's optimization pipeline. This article does not discuss "which passes LLVM has," but rather focuses on the syntax, semantics, types, and memory model of LLVM IR as a language. Understanding these concepts enables you to implement "lowering from AST to LLVM IR" codegen yourself.
Three Forms of LLVM IR
The same LLVM module has three representations, which are equivalent to each other:
.ll (human-readable text): Defined in documentation and seen during debugging
%result = add i32 %a, %b
ret i32 %result
.bc (bitcode binary): Compact format used for linking and LTO
(binary sequence)
Memory IR (C++ API): Constructed in memory by compiler frontends
auto *add = builder.CreateAdd(a, b, "result");
The three forms are converted into one another using llvm-as / llvm-dis / opt / llc. Understanding the .ll text format is a prerequisite for reading and debugging LLVM IR.
Core Syntax: Virtual Registers + Types + Instructions
Virtual Registers and Assignment
LLVM IR is fully SSA—each virtual register (%1, %2, ... or %name) is assigned exactly once:
%x = add i32 1, 2 ; %x is defined
%y = mul i32 %x, 3 ; %x is used, %y is defined
; Cannot assign to %x again — violates SSA; use φ or a different version
Type System
LLVM IR's type system is much richer and more explicit than C's; every value has a type:
i1 ; 1-bit integer (boolean)
i32 ; 32-bit integer
i64 ; 64-bit integer
float ; 32-bit IEEE floating-point
double ; 64-bit IEEE floating-point
ptr ; Opaque pointer (LLVM 15+)
; Legacy: i32* (pointer to i32)
[10 x i32] ; Array of 10 i32s
{ i32, float, i8 } ; Structure (anonymous)
<4 x float> ; Vector of 4 floats (SIMD)
Opaque pointers (ptr) represent a significant simplification. Before LLVM 15, pointers were i32*, float**, etc.—the type and the bit-width of the pointer were orthogonal. Opaque pointers eliminate the encoding of "what type the pointer points to," making the IR more concise (reducing the need for bitcast), but frontends must maintain type information themselves for gep.
Control Flow Instructions
Control flow in LLVM IR is based on basic blocks and terminator instructions:
define i32 @max(i32 %a, i32 %b) {
entry:
%cond = icmp sgt i32 %a, %b ; signed greater than → i1
br i1 %cond, label %then, label %else
then:
ret i32 %a ; Terminator: return
else:
ret i32 %b
}
br i1 %cond, label %true, label %false: Conditional branchbr label %dest: Unconditional branchret <ty> <value>/ret void: Returnswitch i32 %val, label %default [...]: Multi-way branchindirectbr: Computed branch (e.g., virtual table dispatch)
Phi Instruction: Value Selection at Control Flow Merges
define i32 @abs(i32 %x) {
entry:
%ge = icmp sge i32 %x, 0
br i1 %ge, label %pos, label %neg
pos:
br label %merge
neg:
%negx = sub i32 0, %x
br label %merge
merge:
%result = phi i32 [ %x, %pos ], [ %negx, %neg ]
; result = %x if coming from pos, %negx if coming from neg
ret i32 %result
}
The arguments of the phi instruction [value, predecessor_block] are precisely bound—the first value corresponds to the case where control comes from the first predecessor. Phi instructions are located at the beginning of a basic block, and multiple phis are ordered by their declaration order—they do not represent actual computation, but rather value selection at control flow merges, all referencing variable versions that existed before entering the block (i.e., in the predecessor blocks).
Memory Model: alloca / load / store / gep
LLVM IR distinguishes between virtual registers (SSA values, in registers) and memory (on stack/heap, accessed via pointers):
%ptr = alloca i32 ; Allocate an i32 on the stack (returns ptr)
store i32 42, ptr %ptr ; Write 42 into memory pointed to by ptr
%val = load i32, ptr %ptr ; Read from ptr → %val (i32)
%x = add i32 %val, 1 ; Operate directly on SSA value
store i32 %x, ptr %ptr
The result of alloca is a ptr; subsequent load/store explicitly read/write memory. LLVM's mem2reg pass converts "promotable alloca + load/store pairs" into SSA virtual registers + phi—this is the first step before entering the optimization pipeline. Those that cannot be promoted (e.g., whose address is taken or passed across functions) remain in memory.
GEP (GetElementPtr): Address Calculation for Structs/Arrays
getelementptr is the most confusing instruction in LLVM IR; its semantics are pure address calculation, with no memory access:
%struct = type { i32, float, i8 } ; Struct definition
%p = alloca %struct
%field1_addr = getelementptr %struct, ptr %p, i32 0, i32 1
; ^^ ^^ ^^
; type ptr indices:
; [0]=do not dereference, [1]=1st field (float)
store float 3.14, ptr %field1_addr
GEP calculation is entirely based on type layout—[i32 0, i32 1] means "starting from %p, for the %struct type, take the 0th element (pointer unchanged), take the 1st field." GEP returns a pointer offset relative to the base address (determined by the struct field type layout), rather than accessing memory—i32 takes 4 bytes, float is aligned to 4 bytes, i8 takes 1 byte.
In modern LLVM with opaque pointers, the first index of GEP is usually always i32 0 (historical pointer dereferencing becomes a no-op).
Function and Module Structure
; External declaration
declare i32 @printf(ptr, ...)
; Function definition
define i32 @main(i32 %argc, ptr %argv) {
...
%fmt = ... ; Pointer to "Hello %s\n"
call i32 (ptr, ...) @printf(ptr %fmt, ptr %str)
ret i32 0
}
@name: Global symbol (function, global variable)%name: Local virtual registercall: Call—can be direct call, indirect call (call i32 %fp(...)), or tail call (musttail call)
From AST to LLVM IR: A Lowering Path
Using a + b * c as an example, show the process of lowering from AST to LLVM IR:
AST: Binary(+, Variable("a"), Binary(*, Variable("b"), Variable("c")))
Codegen (recursive):
codegen(Binary(+, lhs, rhs)):
left_val = codegen(lhs) → %a = load i32, ptr %a_addr
right_val = codegen(rhs) → recursive:
b_val = codegen(Var("b")) → %b = load...
c_val = codegen(Var("c")) → %c = load...
→ %tmp = mul i32 %b, %c
→ %add = add i32 %a, %tmp
return %add
Output LLVM IR:
%a = load i32, ptr %a_addr
%b = load i32, ptr %b_addr
%c = load i32, ptr %c_addr
%tmp = mul i32 %b, %c
%add = add i32 %a, %tmp
Real-world codegen also needs to handle:
- Short-circuit evaluation (
a && b): Convert to control flow + phi:if a { if b { true } else { false } } else { false }. - Variable addresses (
&x): If x was previously allocated with alloca, use its ptr directly; if x has already been promoted to a virtual register by mem2reg, you need to re-allocate with alloca and store. - Aggregate types: Assignments to structs and arrays become memcpy or field-by-field load/store.
References
- LLVM Language Reference Manual: https://llvm.org/docs/LangRef.html — Complete syntax and semantics of IR; refer to the official documentation
- LLVM Kaleidoscope Tutorial: https://llvm.org/docs/tutorial/ — Implement a DSL to LLVM IR from scratch
- "Learn LLVM 17" (Kai Nacke) — Engineering guide to LLVM internals, including pass manager, backend, and JIT
Keywords: LLVM IR, SSA, virtual register, i1, i32, i64, ptr, opaque pointer, struct, array, vector, phi, br, switch, alloca, load, store, gep, getelementptr, bitcast, function, module, intrinsic, lowering, mem2reg, Kaleidoscope