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 branch
  • br label %dest: Unconditional branch
  • ret <ty> <value> / ret void: Return
  • switch i32 %val, label %default [...]: Multi-way branch
  • indirectbr: 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 register
  • call: 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