LLVM IR in Practice

On this page

LLVM IR is a typed intermediate language processed by both the optimizer and the backend. It contains SSA values, explicit control flow, and explicit memory operations, but it does not imply that all values are eventually placed in physical registers, nor is it equivalent to the full semantics of any source language.

The following examples use modern ptr opaque pointer syntax and can be verified using LLVM tools of the same version that support this syntax. The language reference specifies the exact semantics of each instruction; do not infer semantics solely from names. LLVM Language Reference

Three Representations and Tool Responsibilities

Textual .ll files are convenient for reading, bitcode .bc is used for serialization, and memory IR is manipulated via the compiler API. llvm-as assembles text into bitcode, while llvm-dis displays it in reverse; opt analyzes and transforms IR, and llc generates target code. Not all tools can convert between all three representations.

You can first observe the IR of C code using clang -S -emit-llvm, and then use small, hand-written modules to understand specific instructions. Different optimization levels will significantly change the output structure.

Names, Types, and Basic Blocks

@name typically refers to global variables or functions, while %name can refer to local values, arguments, and other local identifiers. The integer type i32 represents 32 bits and does not inherently carry a "signed" attribute; icmp slt, icmp ult, and different division instructions select signed or unsigned interpretations respectively.

Basic blocks end with a terminator instruction, such as br, ret, or switch. Ordinary instructions cannot be placed after a terminator to continue execution; phi nodes are located at the beginning of the block and provide inputs from predecessors. Indirect function calls use the function pointer target of the call instruction; do not treat indirectbr as a standard virtual function call instruction.

An Executable Module

Save the following as example.ll:

%Pair = type { i32, double }

define i32 @choose_add(i1 %take_a, i32 %a, i32 %b) {
entry:
  br i1 %take_a, label %left, label %right
left:
  %a1 = add i32 %a, 1
  br label %merge
right:
  %b1 = add i32 %b, 1
  br label %merge
merge:
  %result = phi i32 [ %a1, %left ], [ %b1, %right ]
  ret i32 %result
}

define double @read_weight(ptr %p) {
entry:
  %field = getelementptr %Pair, ptr %p, i64 0, i32 1
  %value = load double, ptr %field
  ret double %value
}

define i32 @main() {
entry:
  %pair = alloca %Pair
  %weight = getelementptr %Pair, ptr %pair, i64 0, i32 1
  store double 2.500000e+00, ptr %weight
  %got = call double @read_weight(ptr %pair)
  %a = call i32 @choose_add(i1 true, i32 40, i32 9)
  %b = call i32 @choose_add(i1 false, i32 40, i32 9)
  %ok_a = icmp eq i32 %a, 41
  %ok_b = icmp eq i32 %b, 10
  %ok_w = fcmp oeq double %got, 2.500000e+00
  %ok_ab = and i1 %ok_a, %ok_b
  %ok = and i1 %ok_ab, %ok_w
  %status = select i1 %ok, i32 0, i32 1
  ret i32 %status
}
llvm-as example.ll -o example.bc
opt -passes=verify -disable-output example.bc
lli example.bc
opt -passes='default<O2>' -S example.bc -o optimized.ll

main constructs the exit status using the actual results of two branches and the result of reading a struct field; success is indicated by 0. The verifier checks IR structure and some semantic conditions, but it does not prove that any pointer is valid at runtime.

Opaque Pointers: Pointer Types Do Not Carry Pointee Type

Modern ptr does not encode whether it points to an i32 or a double; the read/write width is provided by the load/store instructions, and the address calculation layout is provided by the source element type of the GEP. Address space remains an important part of the pointer type; do not treat all ptrs as having the same physical representation. Opaque Pointers

Therefore, load i32, ptr %p is not a runtime check that p actually points to an i32, but rather a frontend declaration that this read uses i32 semantics. Requirements such as valid objects, alignment, lifetime, and aliasing still must be satisfied.

GEP: The First Index Is Not "Dereference"

GEP calculates the location; load reads the content at that location ptr %base i32 element sequence GEP i32, index 3 Advance by 3 elements ptr %element Still an address Subsequently, load i32, ptr %element produces the read i32 value.

For %Pair, getelementptr %Pair, ptr %p, i64 0, i32 1 first selects the 0th Pair, then selects field 1. Field offsets include the alignment and padding of the target DataLayout; they are not simply the sum of the sizes of preceding fields.

The first index can be non-zero: when treating %p as the base address of a continuous sequence of Pairs, index 2 means advancing by two elements according to the Pair's allocation size. Ordinary array element accesses also frequently use non-zero indices; opaque pointers do not turn this into a no-op.

GEP itself does not perform memory access; this does not mean that any calculated address can be read. inbounds adds stronger object and calculation constraints; violating them may produce poison, so do not add it without justification "to make optimization better."

Pair Array: Element Stride and Field Offsets Must Be Calculated Together

Under the common x86-64 System V layout, field 0 of { i32, double } occupies bytes [0,4), followed by 4 bytes of padding, and field 1 occupies [8,16), with the entire object size being 16. If accessing the weight of the 2nd element in a Pair array, the offset is 2*16+8=40, not 2*(4+8)+4=28.

Calculation PartBytes on this TargetSource
Allocation size of one Pair16Field layout and overall alignment
Start of 2nd element32Skip two elements by allocation size
Offset of weight within element8Padding generated by double field alignment
Final relative address40Element start plus field offset

You can cross-check with the C layout of the same target. The static assertions here explicitly fix expected values and do not claim these numbers apply to any DataLayout:

#include <stddef.h>
struct Pair { int tag; double weight; };
_Static_assert(sizeof(int) == 4, "requires 32-bit int");
_Static_assert(sizeof(struct Pair) == 16, "different target layout");
_Static_assert(offsetof(struct Pair, weight) == 8, "different field layout");
int main(void) {
    struct Pair pairs[3] = {{0, 0.0}, {0, 0.0}, {7, 2.5}};
    return pairs[2].weight != 2.5;
}

Do not derive the entire aggregate layout solely from "pointers are 64-bit." DataLayout also specifies the size and alignment of different types, and packed structures change padding between fields; even if you calculate the address, the alignment promise of the load must match the actual object. GEP Layout Explanation

How do element stride and field offset compose?

The address multiplication skips i whole objects; the addition enters a field in the selected object. Byte rows expose padding and packed stride. The control changes an example DataLayout and performs no load. An unaligned field address does not justify a stronger alignment promise on a later access.

Preparing the visual
How do element stride and field offset compose?

GEP advances by allocation size, then field offset. Padding affects ordinary layout; packed layout can yield unaligned fields. Address calculation does not load memory.

Arithmetic Flags and Source Language Semantics

Ordinary integer add operates on bit width; nsw (no signed wrap) and nuw (no unsigned wrap) express corresponding no-overflow promises, they do not automatically check for overflow and throw source language exceptions. If the source language requires checked addition, the frontend must generate checks, branches, or appropriate intrinsics.

Similarly, you cannot simply replace all short-circuit && operations with unconditionally evaluated select. If the right-hand side calls functions, accesses memory, or throws errors, you must preserve the source language's conditional execution. Choosing between two pre-calculated values is different from controlling whether evaluation occurs.

Checked Addition: Turning Overflow Flags into Control Flow

Assume the interface convention: adding two signed i32s, writing the output slot and returning 1 on success, not modifying the output slot and returning 0 on overflow. Ordinary add does not have this checking behavior, and add nsw will not automatically jump to the failure branch for us. Save as checked.ll:

declare { i32, i1 } @llvm.sadd.with.overflow.i32(i32, i32)

define i32 @checked_add(i32 %a, i32 %b, ptr %out) {
entry:
  %pair = call { i32, i1 } @llvm.sadd.with.overflow.i32(i32 %a, i32 %b)
  %sum = extractvalue { i32, i1 } %pair, 0
  %overflow = extractvalue { i32, i1 } %pair, 1
  br i1 %overflow, label %failure, label %success
success:
  store i32 %sum, ptr %out, align 4
  ret i32 1
failure:
  ret i32 0
}

This intrinsic simultaneously produces the truncated bit pattern and the signed overflow flag. Even if the failure branch does not use %sum, the flag calculation does not disappear; only the success branch exposes the result to the caller. The interface here requires out to point to a valid, at least 4-byte aligned i32 output object; C drivers use ordinary int objects to satisfy this target requirement. Addition with Overflow Detection

/* checked-main.c: clang checked.ll checked-main.c -o checked-demo */
#include <limits.h>
_Static_assert(INT_MAX == 2147483647, "requires 32-bit int");
extern int checked_add(int, int, int *);
int main(void) {
    int out = 99;
    if (checked_add(20, 22, &out) != 1 || out != 42) return 1;
    if (checked_add(INT_MAX, 0, &out) != 1 || out != INT_MAX) return 2;
    out = 99;
    if (checked_add(INT_MAX, 1, &out) != 0 || out != 99) return 3;
    if (checked_add(INT_MIN, -1, &out) != 0 || out != 99) return 4;
    if (checked_add(INT_MIN, 0, &out) != 1 || out != INT_MIN) return 5;
    return 0;
}

For inputs INT_MAX and 1, the mathematical sum exceeds the signed range, returning 0 and preserving the original out=99; for INT_MAX and 0, the boundary value itself is valid, returning 1. The difference between the two branches is defined explicitly in IR; the optimizer may replace it with a machine implementation, but must preserve this interface behavior.

Detect overflow, then branch according to the contract sadd.with.overflow Produces both sum and overflow Check overflow Choose successor using i1 No Overflow Store sum; return 1 Overflow Do not write out; return 0 nsw is a promise of no signed overflow; it is not a substitute for the above check and error return flow.

Why does the same bit pattern not imply the same contract?

Separate the bit pattern from the interface contract. i8 makes the sign bit visible: modular addition truncates, nsw produces poison when its signed-overflow promise fails, and the checked API explicitly decides whether to write out. The poison label denotes a result category, not a simulation of every later use.

Preparing the visual
Why does the same bit pattern not imply the same contract?

Plain add keeps modulo-256 bits. Signed overflow under nsw produces poison, not an exception. The checked interface reports failure and preserves output99.

Mutable Local Variables and Address Identity

Frontends new to this can create appropriate allocas for mutable local variables, use load/store, and then let mem2reg or SROA promote optimizable parts. It is not that "taking the address of an alloca" itself prevents promotion; alloca inherently returns an address; the key is whether the specific use of the address meets the promotion conditions.

If the source program allows object address escaping or observable identity, the frontend should model this correctly from the start. You cannot see &x in the source code after optimization and arbitrarily create a new alloca, claiming to have restored the original object identity.

Module and ABI Boundaries

Target triple and DataLayout affect layout and generated targets. Calling external functions also requires correct signatures, calling conventions, argument attributes, and ABI lowering. The way an aggregate parameter is written in IR does not guarantee it naturally conforms to all platform C ABIs.

musttail, exception calls, atomic operations, volatile, and various attributes have independent constraints; first generate valid IR with minimal trusted semantics, then add optimization information based on proofs. Incorrect attributes may cause the optimizer to legally produce programs that do not match the original intent.

References

Keywords: LLVM IR, bitcode, ptr, opaque pointer, phi, GEP, load, store, DataLayout, poison, mem2reg, verifier