1. Overview
There are several reasons you might want to learn about LLVM. Maybe you’re a tech enthusiast who wants to create a new programming language, or a university student stuck in the quagmire of a compilers course. Whatever the case, you likely want LLVM for just one thing: generating target code.
But it’s not an easy journey. You’ve probably already struggled through the compiler frontend, and now another mountain stands before you: LLVM IR. Right from the start, concepts like basic blocks, virtual registers, and phi nodes make your head spin. Code examples are full of irrelevant details that obscure the key points. It’s overwhelming…
Perhaps a better approach is to treat LLVM IR as a special programming language rather than a compiler’s “intermediate representation.” This language sits between high-level and low-level languages — it has some abstraction capabilities while also reflecting how low-level assembly works.
In this article, we’ll take a brief tour of LLVM IR from the perspective of a programming language. By writing LLVM IR by hand, we’ll gradually analyze its features and principles. I hope it helps.
(1) Preparation
Although LLVM IR is intermediate code, LLVM also provides the lli tool for interpreting / JIT-compiling and executing LLVM IR files. This makes LLVM IR somewhat similar to Java bytecode. Using lli, we can directly run our hand-written IR — very convenient. This requires installing LLVM.
sudo apt-get install llvm
Additionally, LLVM has no built-in I/O, because LLVM operates below the operating system. This creates a problem: we can’t see the results of our program. One workaround is to use the function’s return value as output, but that only gives us a single number. For convenience, we can first write an output library in C and compile it to LLVM IR with clang. This requires installing clang:
sudo apt-get install clang
For the output library, we’ll simply define a putint function:
// lib.c
#include <stdio.h>
void putint(int i) { printf("%d\n", i); }
Use clang to translate it into IR. Don’t worry — we don’t need to understand anything in the generated lib.ll.
clang -emit-llvm -S ./lib.c -o lib.ll
Now the preparation is complete. Let’s start with our first program.
(2) First Program — but No “Hello World”
Since I’m too lazy to define character output, our first program won’t print “hello world.” Let’s write a simple addition program instead — outputting 1, 2, and 1 + 2.
; first.ll
declare void @putint(i32)
define i32 @main() {
call void @putint(i32 1)
call void @putint(i32 2)
%result = add i32 1, 2
call void @putint(i32 %result)
ret i32 0
}
LLVM IR uses
;for comments.
After writing the code, link first.ll with the lib.ll we generated via clang using llvm-link:
llvm-link lib.ll first.ll -o out.ll
Then execute the linked out.ll with lli:
lli out.ll
Output:
1
2
3
From this first program, do you get a strong C-like vibe from LLVM IR? First there’s the declare statement, which clearly corresponds to a C header declaration:
declare void @putint(i32)
Function definitions also retain a structure similar to C:
define i32 @main() {
; omit...
}
Function calls are essentially identical to C:
call void @putint(i32 1)
The biggest difference is in the computation part — it seems to assign a result to a “variable” without any declaration. In reality, %result represents a register, not a variable in memory:
%result = add i32 1, 2
Actually, you shouldn’t call something like
%resulta “register.” It’s more accurate to think of it as an identifier for the result of an instruction. In LLVM IR, these “registers” don’t correspond to any actual registers in an instruction set — they can be freely defined without register allocation. And within a single function, each instruction’s register name is unique.
Another difference is in types. LLVM IR retains types, but unlike high-level languages where types only appear at declaration, LLVM IR requires the type to be specified every time a value is used. In our first program, the type i32 is explicitly written for the add instruction, function call arguments, and return value.
Regardless, LLVM IR shares enough similarities with C that we can directly translate this LLVM IR program into C:
// lib.h
void putint(int);
// first.c
#include "lib.h"
int main() {
putint(1);
putint(2);
putint(1 + 2);
return 0;
}
So far, LLVM IR doesn’t seem too hard to understand. From here, we’ll gradually explore each part of LLVM IR and write corresponding code.
2. Types
LLVM IR is strongly typed, and every “value” or “variable” must specify its type when used. This makes types a very important part of LLVM IR. So let’s start with types. However, LLVM IR’s type system is quite complex, so we’ll only cover the parts most relevant to programming.
For our purposes, LLVM IR types are divided into primitive types and aggregate types, similar to C. Primitive types are the basic units for computation instructions — they include integers, floating-point numbers, and void. Aggregate types are combinations of primitive types, including arrays and structs. There are also pointer types — every primitive type, aggregate type, and pointer itself has a corresponding pointer type.
(1) Primitive Types
For integers, LLVM only cares about the bit width, not signedness. Signedness is distinguished by the instruction used. For example, unsigned and signed integer division are udiv and sdiv respectively.
LLVM doesn’t restrict integer widths to 32 and 64. You can choose any number of bits from $1$ to $2^{23} - 1$.
In LLVM IR, an integer is represented by i followed by its bit width, so i1, i2, i32, i64, etc., are all valid. Notably, i1 explicitly represents the bool type in LLVM IR, with constant values true and false rather than 0 and 1.
; int.ll
; i2 is still usable, return value should be 2
define i2 @main() {
%t1 = add i2 1, 1
ret i2 %t1
}
Floating-point types include float, double, and others. The specific types vary by standard. Operations are similar to integers. For example, the result should be 7.555000:
// lib.c
#include <stdio.h>
void putint(int i) { printf("%d\n", i); }
void putfloat(float f) { printf("%f\n", f); } // new
; float.ll
declare void @putfloat(float)
define i32 @main() {
%t0 = fadd float 2.0, 0x40163851E0000000
call void @putfloat(float %t0)
ret i32 0
}
Note that not all decimals can be represented as floating-point numbers, so many floating-point constants can’t be used directly. For instance, 2.0 can be represented, so it works as a constant; but 0.2 can’t, so it can’t be a constant. A better approach is to use hexadecimal representation. For example, 0x40163851E0000000 above actually represents 0.555.
(2) Aggregate Types
Arrays represent data of the same type stored in a contiguous address range. In LLVM IR, they’re written as [N x T], where N is the length and T is the element type — which can be a primitive type, struct, pointer, or even another array. So LLVM IR can represent multi-dimensional arrays. For example, a 2D array like int arr[2][3] would have type [2 x [3 x i32]].
Array constant notation deserves special mention, as it’s important for initializing global variables. In LLVM IR, array constants are written as [T V, T V, ..., T V]. A 1D array like {1, 2, 3} becomes [i32 1, i32 2, i32 3]. A 2D array like {{1, 2}, {3, 4}} becomes [[2 x i32] [i32 1, i32 2], [2 x i32] [i32 3, i32 4]] — higher dimensions follow the same pattern. In LLVM IR, you generally need the type before the value, so a constant like [i32 1, i32 2, i32 3] is typically used as [3 x i32] [i32 1, i32 2, i32 3].
Here’s an example. Since arrays involve address computation and loading from addresses, the main function uses instructions we haven’t covered yet — you can ignore those for now. Just look at the initializer [3 x i32] [i32 1, i32 2, i32 3] used for the global variable.
; array.ll
declare void @putint(i32)
; Define global variable @arr, initialized with an array constant
@arr = global [3 x i32] [i32 1, i32 2, i32 3]
define i32 @main() {
; Output arr[0]
%arr0 = getelementptr [3 x i32], [3 x i32]* @arr, i32 0, i32 0
%arr0val = load i32, i32* %arr0
call void @putint(i32 %arr0val)
; Output arr[1]
%arr1 = getelementptr [3 x i32], [3 x i32]* @arr, i32 0, i32 1
%arr1val = load i32, i32* %arr1
call void @putint(i32 %arr1val)
; Output arr[2]
%arr2 = getelementptr [3 x i32], [3 x i32]* @arr, i32 0, i32 2
%arr2val = load i32, i32* %arr2
call void @putint(i32 %arr2val)
ret i32 0
}
Running this program should produce:
1
2
3
Structs are similar to arrays but represent a group of differently-typed data in contiguous memory. Structs must be defined before use, similar to C, and must be defined outside function bodies. Here’s an example defining a struct named MyStruct containing an integer, a float, and an integer array of size 3. Note that % is required:
%MyStruct = type {
i32,
float,
[3 x i32]
}
Once defined, using the struct type is much like using any other type — just use the defined struct name. For the example above, use %MyStruct.
Struct constants are similar to array constants and are used when defining global variables. In LLVM IR, they’re written as {T V, T V, ..., T V}, typically with the type prefix. For %MyStruct, a constant would be %MyStruct { i32 10, float 2.0, [3 x i32] [i32 3, i32 2, i32 1]}.
Here’s a struct example. Again, ignore main for now — just look at the struct definition and the global variable @mystruct.
; struct.ll
declare void @putint(i32)
declare void @putfloat(float)
%MyStruct = type {
i32,
float,
[3 x i32]
}
@mystruct = global %MyStruct { i32 10, float 2.0, [3 x i32] [i32 3, i32 2, i32 1]}
define i32 @main() {
; Get i32
%mystruct0 = getelementptr %MyStruct, %MyStruct* @mystruct, i32 0, i32 0
%mystruct0val = load i32, i32* %mystruct0
call void @putint(i32 %mystruct0val)
; Get float
%mystruct1 = getelementptr %MyStruct, %MyStruct* @mystruct, i32 0, i32 1
%mystruct1val = load float, float* %mystruct1
call void @putfloat(float %mystruct1val)
; Get [3 x i32] elements
; Get [0]
%mystruct2 = getelementptr %MyStruct, %MyStruct* @mystruct, i32 0, i32 2
%arr0 = getelementptr [3 x i32], [3 x i32]* %mystruct2, i32 0, i32 0
%arr0val = load i32, i32* %arr0
call void @putint(i32 %arr0val)
; Get [1]
%arr1 = getelementptr [3 x i32], [3 x i32]* %mystruct2, i32 0, i32 1
%arr1val = load i32, i32* %arr1
call void @putint(i32 %arr1val)
; Get [2]
%arr2 = getelementptr [3 x i32], [3 x i32]* %mystruct2, i32 0, i32 2
%arr2val = load i32, i32* %arr2
call void @putint(i32 %arr2val)
ret i32 0
}
Output should be:
10
2.000000
3
2
1
One last small note: you can use
zeroinitializerto uniformly zero-initialize the memory region of a global variable. For an all-zero integer array of size 3:[3 x i32] zeroinitializer. For a 2D array like{{0, 0}, {0, 1}}:[2 x [2 x i32]] [[2 x i32] zeroinitializer, [2 x i32] [i32 0, i32 1]]. Structs work similarly.
(3) Pointers
A pointer is an address. Every type has a corresponding pointer type, just like in C. Addresses are mainly used for data access, but sometimes you also need to perform arithmetic on addresses directly. The former is covered in the next chapter; the latter is done via ptrtoint .. to and inttoptr .. to to convert between pointers and integers. For example, %addr_int = ptrtoint i32* %some_addr to i64 converts a pointer to a 64-bit integer, enabling integer arithmetic on the pointer.
Pointers are often passed as function parameters. For instance, when passing an array, we don’t copy its entire contents onto the stack — we just pass a pointer to it, which makes sense. Similar to C, a special case is multi-dimensional arrays: dimensions beyond the first must have explicit sizes. In C this looks like:
void func(int arr[][10]) { }
In LLVM IR:
define void @func([10 x i32]* %arr) {
ret void
}
Here’s a pointer example. Pay attention to ptrtoint usage and pointer parameters. The comments should clarify how array pointers are passed between functions:
// lib.c
#include <stdio.h>
void putint(int i) { printf("%d\n", i); }
void putfloat(float f) { printf("%f\n", f); }
void putaddr(size_t addr) { printf("%#zx\n", addr); } // new
; pointer.ll
declare void @putaddr(i64)
@arr = global [2 x [2 x i32]] [[2 x i32] [i32 1, i32 2], [2 x i32] [i32 3, i32 4]]
define void @func1([2 x i32]* %arr) {
%addr = ptrtoint [2 x i32]* %arr to i64
call void @putaddr(i64 %addr)
; arr[1]
%arr1addr = getelementptr [2 x i32], [2 x i32]* %arr, i32 0, i32 1
; func2(arr[1])
call void @func2(i32* %arr1addr)
ret void
}
define void @func2(i32* %arr) {
%addr = ptrtoint i32* %arr to i64
call void @putaddr(i64 %addr)
ret void
}
define i32 @main() {
%addr = ptrtoint [2 x [2 x i32]]* @arr to i64
call void @putaddr(i64 %addr)
; arr[1]
%arr1addr = getelementptr [2 x [2 x i32]], [2 x [2 x i32]]* @arr, i32 0, i32 1
; func1(arr[1])
call void @func1([2 x i32]* %arr1addr)
ret i32 0
}
On my machine, the output is:
0x7f03162ff000
0x7f03162ff008
0x7f03162ff00c
The base address of global array @arr is 0x7f381bd79000. Then func1(arr[1]) receives 0x7f03162ff008 — an offset of 8 bytes, i.e., two i32s. Inside func1, func2(arr[1]) receives 0x7f03162ff00c — offset by 4 bytes from 0x7f03162ff008, exactly one i32.
3. Variables
Types define how data is represented; variables define where data lives. Now let’s understand variables in LLVM IR.
As we know, variables fall into three categories based on storage: stack variables, heap variables, and static storage variables. However, heap variables are managed by the OS (or the programmer) and aren’t considered at compile time, so LLVM IR only has the other two kinds.
(1) Globals
We’ve already seen static storage variables in previous sections — those global variables defined outside function bodies. These are defined with global or constant. For example, @a = global i32 1 or @two = constant i32 2. Globals defined with constant are read-only. Both require an initializer; if you don’t want to set one (or want it zeroed by default), use zeroinitializer as explained earlier.
One more thing about globals: they are given a name like
@xxxat definition. This label is different from the “virtual registers” we use inside function bodies. It’s more like an assembly “label” — essentially an address; or rather, a pointer to the defined global. This is why something like%t = add i32 @a, 1is wrong —@ahas typei32*.
(2) Locals
That leaves only stack variables, or local variables. Stack space grows and shrinks dynamically. In LLVM, the alloca instruction represents a stack allocation. When the function returns, the stack space is automatically freed.
Note that locals can’t truly be
constant. For globals, variables and constants can be placed in different memory regions — variables in a read-write region, constants in a read-only region. This relates to OS page table management and program segment loading.
The alloca instruction looks like %xxx = alloca T, meaning “allocate stack space for type T, storing its address in virtual register %xxx.” Note that %xxx has type T*, not T — similar to globals.
allocadiffers from global definitions: it carries no initial value. To set an initial value, usestoreafteralloca.
Important:
allocashould only be used at the very beginning of a function. Sinceallocaessentially moves the stack pointer down to allocate space, using it inside a loop would continuously allocate stack space, potentially causing a stack overflow.The following code proves that loop-local variables occupy the same stack space:
#include <stdio.h> int main() { for (int i = 0; i < 10; i++) { int a; printf("%d ", a); a++; } return 0; }Output:
0 1 2 3 4 5 6 7 8 9
Now that we know the address of allocated memory, we need store and load to access it. (Always remember: LLVM IR operates on registers, not memory. This is where LLVM IR resembles assembly and differs from high-level languages.)
store writes a register value to a memory location. Its format is store T %val, P %ptr — %val is the value register with type T; %ptr is the address register (or global @ptr) with type P where T* = P. Notably, store has no “return value.”
load is the inverse of store. The only difference is that load has a “return value.” Its format is %val = load T, P %ptr.
Here’s an example combining the above:
; val.ll
declare void @putint(i32)
@a = global i32 1
@two = constant i32 2
define i32 @main() {
; alloca must be used at the beginning
%var = alloca i32
; Wrong: call void @putint(@a)
%a_val = load i32, i32* @a
call void @putint(i32 %a_val)
; Wrong: store i32 3, i32* @two
store i32 100, i32* @a
%a_val2 = load i32, i32* @a
call void @putint(i32 %a_val2)
; Uninitialized, value is undefined
%var_val = load i32, i32* %var
call void @putint(i32 %var_val)
; Set initial value to 123
store i32 123, i32* %var
%var_val2 = load i32, i32* %var
call void @putint(i32 %var_val2)
ret i32 0
}
Output (the third line’s value is actually indeterminate):
1
100
0
123
(3) The Quirky getelementptr
Now we know how to define variables and access their values. Ready for the next chapter? Not quite. What if I want to define an array and access each element, or define a struct and access its fields?
Defining them is easy — just @arr = global [3 x i32] [i32 1, i32 2, i32 3] or %arr = alloca [3 x i32]. The problem is: how do we get the address of each element, given only the array’s base address?
You might think of pointer arithmetic — adding 4 to get the next element’s address. But that requires ptrtoint, and after getting the address, since load and store only accept properly-typed pointers, you’d need inttoptr to convert back. That’s way too cumbersome and clearly not the intended solution.
The real solution is getelementptr. Under LLVM IR’s strict type system, this instruction is incredibly useful — it can easily compute addresses of aggregate type elements while also performing pointer type conversions.
I see three forms of getelementptr. Let’s go through them one by one.
Array Pointer getelementptr
The simplest form: %ptr = getelementptr T, T* %baseptr, V %idx. This is equivalent to C pointer arithmetic baseptr + idx. It computes the address offset from %base by %idx units of type T’s size. The pointer type remains unchanged.
Suppose we have a function whose C signature is void func(int arr[]). We can now use getelementptr to access its elements:
define void @func(i32* %arr) {
; putint(arr[0])
%arr0 = getelementptr i32, i32* %arr, i32 0
%arr0val = load i32, i32* %arr0
call void @putint(i32 %arr0val)
; putint(arr[1])
%arr1 = getelementptr i32, i32* %arr, i32 1
%arr1val = load i32, i32* %arr1
call void @putint(i32 %arr1val)
ret void
}
So can we now access elements of an array variable? Not yet. For a variable like %arr = alloca [3 x i32], %arr has type [3 x i32]*, not i32*. So this form of getelementptr still can’t access individual array elements.
Member Address getelementptr
We want address computation that goes “deep inside” rather than “staying on the surface.” This requires the second form: %ptr = getelementptr T, T* %baseptr, V %idx1, V %idx2. This is just the first form with one more offset. %idx1 means the same thing; %idx2 means “get the address of the %idx2-th member of aggregate type T.” The return type is a pointer to that member’s type.
For example, getelementptr [2 x [2 x i32]], [2 x [2 x i32]]* @arr, i32 0, i32 1 in C is (arr+0)[1], with return type [2 x i32]*. Structs work similarly, though each field’s pointer type may differ.
This form appears frequently in the previous example code. Go back and look at the parts we skipped — do they make sense now?
Multi-level Nested Member getelementptr
Suppose we have this struct and a global variable of that type:
%MyStruct = type {
i32,
[2 x [2 x i32]]
}
@a = global %MyStruct {i32 10, [2 x [2 x i32]] [[2 x i32] [i32 1, i32 2], [2 x i32] [i32 3, i32 4]]}
To get the value at row 1, column 0 of the struct’s inner array, what’s the getelementptr?
You can certainly do it by chaining getelementptr calls. So you write:
define i32 @main() {
%t0 = getelementptr %MyStruct, %MyStruct* @a, i32 0, i32 1
%t1 = getelementptr [2 x [2 x i32]], [2 x [2 x i32]]* %t0, i32 0, i32 1
%t2 = getelementptr [2 x i32], [2 x i32]* %t1, i32 0, i32 0
%val = load i32, i32* %t2
call void @putint(i32 %val)
ret i32 0
}
This is correct, but rather verbose. Is there a more concise way? Yes — just keep appending indices after the first getelementptr. The above is equivalent to:
define i32 @main() {
%t = getelementptr %MyStruct, %MyStruct* @a, i32 0, i32 1, i32 1, i32 0
%val2 = load i32, i32* %t
call void @putint(i32 %val2)
ret i32 0
}
Of course, you could say getelementptr really only has one form to begin with. I just think presenting it this way makes it easier to understand.
4. Computation
Computation is the CPU’s fundamental purpose. There’s not much to elaborate on here, so we’ll just give a brief overview. One reminder: both operands of any computation instruction must have the same type. The general format is %xxx = <op> T %yyy, %zzz. For example, a simple integer addition: %t2 = add i32 %t0, %t1.
(1) Arithmetic
Arithmetic includes both numeric and bitwise operations. For integers, the mapping from C to LLVM IR is:
| C | LLVM IR |
|---|---|
+ |
add |
- |
sub |
* |
mul |
/ |
sdiv or udiv |
% |
srem or urem |
| |
or |
& |
and |
<< |
shl |
>> |
ashr |
For operations other than division, signedness doesn’t matter. But division and remainder require specifying signed vs. unsigned, hence
sdiv/udivandsrem/urem. Also, C’s>>is arithmetic right shift, so the corresponding LLVM IR isashr(Arithmetic Shift Right), notlshr(Logical Shift Right).
For floats, LLVM provides corresponding instructions with an f prefix:
| C | LLVM IR |
|---|---|
+ |
fadd |
- |
fsub |
* |
fmul |
/ |
fdiv |
% |
frem |
Since floats are always signed, there’s no
sdiv/udivdistinction.
Here’s an integer example. Floats work similarly:
; calc.ll
declare void @putint(i32)
define i32 @main() {
%t0 = add i32 5, 8
call void @putint(i32 %t0) ; 13
%t1 = sub i32 5, 8
call void @putint(i32 %t1) ; -3
%t2 = mul i32 5, 8
call void @putint(i32 %t2) ; 40
%t3 = sdiv i32 8, -5
call void @putint(i32 %t3) ; -1
; Note: treating -5 as unsigned makes it much larger than 8, so result is 0
%t4 = udiv i32 8, -5
call void @putint(i32 %t4) ; 0
%t5 = udiv i32 8, 5
call void @putint(i32 %t5) ; 1
%t6 = srem i32 8, -5
call void @putint(i32 %t6) ; 3
%t7 = urem i32 8, -5
call void @putint(i32 %t7) ; 8
%t8 = urem i32 8, 5
call void @putint(i32 %t8) ; 3
; 0101 | 1000 = 1101
%t9 = or i32 5, 8
call void @putint(i32 %t9) ; 13
; 0101 & 0001 = 0001
%t10 = and i32 5, 1
call void @putint(i32 %t10) ; 1
; 0101 << 1 = 1010
%t11 = shl i32 5, 1
call void @putint(i32 %t11) ; 10
; 0101 >> 1 = 0010
%t12 = lshr i32 5, 1
call void @putint(i32 %t12) ; 2
; Arithmetic right shift pads with 1 for negative numbers
%t13 = ashr i32 -5, 1
call void @putint(i32 %t13) ; -3
ret i32 0
}
(2) Comparison
Comparisons use icmp for integers and fcmp for floats. Both require a condition code specifying the relationship. The format is %xxx = (icmp|fcmp) <cond> T %yyy, %zzz. For example, checking integer equality: %t2 = icmp eq i32 %t0, %t1.
Condition codes mapped from C:
| C | LLVM IR |
|---|---|
== |
eq |
!= |
ne |
> |
gt |
>= |
ge |
< |
lt |
<= |
le |
Note:
icmpandfcmpresults have typei1, i.e.,bool.
(3) Type Conversion
Although LLVM IR requires operand types to match, real code doesn’t always satisfy this. We perform implicit or explicit type conversions. LLVM IR provides a variety of conversion instructions — too many to cover fully. Here are some simpler ones.
For integers, conversion is just extending or truncating bits. LLVM IR provides zext (Zero Extend), sext (Sign Extend), and trunc (Truncate). Usage: %xxx = zext T %yyy to Y, where Y is the target type.
zext fills high bits with 0 — for extending unsigned integers. sext fills high bits based on the sign bit — for extending signed integers. trunc simply drops high bits.
Floats have similar instructions: fpext, fptrunc, plus fptoui, fptosi, uitofp, sitofp for converting between signed/unsigned integers and floats. We won’t go into detail.
5. Function Calls
We’ve already seen function calls in many examples. The format is %xxx = call T @fff(T1 %p1, T2 %p2). When the return type is void, omit the “assignment.”
What’s worth mentioning is function definitions. If you’ve been paying attention, you’ll notice that function parameters are represented as virtual registers. In LLVM IR, function parameters are stored in registers, not on the stack.
; From earlier: parameter arr is actually register %arr
define void @func2(i32* %arr) {
%addr = ptrtoint i32* %arr to i64
call void @putaddr(i64 %addr)
ret void
}
Of course, “parameters on the stack” isn’t really a hard requirement — there just aren’t enough registers to store a variable number of parameters.
For consistency, you can use alloca inside the function body to allocate stack space for parameters, then store the register values onto the stack.
6. Control Flow
Finally, let’s discuss the most important topic: control flow. In LLVM IR (and lower-level languages), control flow boils down to jump instructions. In LLVM IR, the jump instruction is br.
br has two forms. The first is a conditional branch: it takes a bool (i1) value and jumps to one of two labels based on its truth value. Format: br i1 %cond, label %iftrue, label %iffalse. If %cond is true, jump to %iftrue; if false, jump to %iffalse.
The second form is an unconditional branch: br label %target. This simply jumps to %target.
A label is just a name followed by a colon, like labelname:. In assembly terms, it refers to the address of the first instruction after the colon.
LLVM imposes constraints on branches and labels. Specifically, the last instruction between two labels must be br or ret, and there must be at least one instruction between labels. Here’s an incorrect example:
; errbr.ll
define i32 @main() {
one:
%t0 = add i32 1, 2
; Error: needs br or ret
two:
; Error: need instructions between two and three
three:
ret i32 %t0
}
Why these constraints? This relates to basic blocks. A basic block is a sequence of instructions with a single entry point (the first instruction) and a single exit point (the last instruction). Dividing programs into basic blocks facilitates compiler optimizations. LLVM IR is designed to generate code in basic block form. Since a basic block has one entry, it can only have one label at the beginning; since it has one exit, no instructions can follow a branch. This explains the constraints: instructions between two labels form a basic block’s body — the starting label is the sole entry, the ending branch is the sole exit.
With labels and branches, we can implement any program flow. But no matter how complex the combination, all control flow reduces to three patterns: sequence, selection, and iteration. (There seems to be a mathematical proof of this.)
(1) Selection
Sequence needs no explanation — instructions within a basic block are executed in order. For selection (branching), using familiar C terms, there are two types:
if type:
if (cond) {
// do something if cond is true
}
if-else type:
if (cond) {
// do something if cond is true
} else {
// do something if cond is false
}
“There’s a third type —
if else if.” Actually, that’s not quite right.else ifis just nested branching. Consider these two forms:if (cond1) { // do something if cond1 is true } else if (cond2) { // do something if cond1 is false and cond2 is true } if (cond1) { // do something if cond1 is true } else if (cond2) { // do something if cond1 is false and cond2 is true } else { // do something if cond1 and cond2 are false }They’re really:
if (cond1) { // do something if cond1 is true } else { if (cond2) { // do something if cond2 is true } } if (cond1) { // do something if cond1 is true } else { if (cond2) { // do something if cond2 is true } else { // do something if cond2 is false } }This is also why C allows single statements after
if/elsewithout braces.
Both branching forms can be expressed with branches. Here are the LLVM IR equivalents. They do nothing, but they compile fine:
; ifelse.ll
declare void @dosomething() {
ret void
}
define void @if(i1 %cond) {
br i1 %cond, label %true, label %fi
true:
call void @dosomething()
br label %fi
fi:
ret void
}
define void @if_else(i1 %cond) {
br i1 %cond, label %true, label %false
true:
call void @dosomething()
br label %fi
false:
call void @dosomething()
br label %fi
fi:
ret void
}