Lab 10.1 · Building IR with IRBuilder¶
Chapter: 10 · The LLVM C++ API · Lessons: 10.1, 10.2, 10.3, 10.8 · Time: 3–5 hours · Tests: ./course test 10 (labels ch10)
Goal¶
Write three functions that each construct a module in memory with llvm::IRBuilder, specified only by what the generated function computes. You decide the control-flow graph, where the phis go and how to name things. The tests verify each module, check its shape, JIT-compile it with ORC LLJIT and call it on hundreds of inputs against a C++ reference.
The point is the object model of Lessons 10.1–10.3. Which object owns which? When may a phi receive its back-edge operand? What is the insertion point when you call CreateXxx? What does the default folder do to constant operands?
Requirements¶
- R1 (all builders). Each builder returns a new
llvm::Modulethat lives in the context you are given, and the caller owns it. The module defines exactly one function, with external linkage and the name and signature from the contract, and it passesllvm::verifyModule. The arguments have the names given in the contract (%n,%coeffs, …); the tests matchdefinelines by those names. - R2 (
@fib).define i64 @fib(i64 %n)returns \(F(n) \bmod 2^{64}\), where \(F(0) = 0\), \(F(1) = 1\) and \(F(k) = F(k-1) + F(k-2)\). For \(n < 0\) it returns \(0\). It must be iterative: one loop whose state is carried inphinodes, with noallocaand nocall. It must handle \(n = 10^5\) without recursion. - R3 (
@horner).define i64 @horner(ptr %coeffs, i64 %n, i64 %x)returns \(\sum_{i=0}^{n-1} c_i x^i \bmod 2^{64}\), where%coeffspoints to \(n\) consecutivei64values \(c_0, \dots, c_{n-1}\). For \(n \le 0\) it returns \(0\) and reads no memory. It must use Horner's rule, \((\dots((c_{n-1})x + c_{n-2})x + \dots)x + c_0\): the function contains exactly onemulinstruction, inside a loop, and noallocaorcall. Coefficients are read with a GEP and aload. - R4 (
@dispatch).define i64 @dispatch(i32 %op, i64 %a, i64 %b)contains exactly oneswitchon%opwith nine cases, 0 to 8. Case 0 isa + b, 1 isa - b, 2 isa * b, 3 isa & b, 4 isa | band 5 isa ^ b(all wrapping). Case 6 isa << (b & 63)and case 7 isa >> (b & 63)(arithmetic). Case 8 is the signed minimum ofaandb. Any other%opreturns 0. - R5 (style). Use
IRBuilder<>(the defaultConstantFolder) or a builder with another folder of your choice. Do not write textual IR and parse it: the lab is about the C++ API.
The contract¶
// labs/ch10-irbuild/include/irbuild/Build.h (provided; do not change)
namespace irbuild {
std::unique_ptr<llvm::Module> buildFib(llvm::LLVMContext &Ctx);
std::unique_ptr<llvm::Module> buildHorner(llvm::LLVMContext &Ctx);
std::unique_ptr<llvm::Module> buildDispatch(llvm::LLVMContext &Ctx);
}
Ownership. The returned unique_ptr<Module> owns the module. The module owns its functions, the functions own their blocks, and the blocks own their instructions (Lesson 10.1). The LLVMContext owns every type and constant you create, so it must outlive the module. The tests create the context, and after verification they move module and context together into an orc::ThreadSafeModule.
Input and output formats¶
There is no file input. The provided driver ch10-irbuild-dump fib|horner|dispatch (tools/DumpMain.cpp) prints your module as textual IR and exits with status 1 if it does not verify. The lit tests check that output with FileCheck. You can use it to look at your own IR:
build/<preset>/bin/ch10-irbuild-dump horner
build/<preset>/bin/ch10-irbuild-dump horner | opt -passes=verify -disable-output
Provided infrastructure¶
| File | What it gives you |
|---|---|
include/irbuild/Build.h |
the contract |
tools/DumpMain.cpp |
ch10-irbuild-dump: prints a module and runs the verifier |
src/Stub.cpp |
stubs that stop with TODO(ch10); delete it once your own files define the three functions |
What the tests check¶
| Test | Checks |
|---|---|
ch10.IrBuild.FibStructure |
R1 (one definition named @fib, argument %n); R2: at least one phi, no alloca, no call |
ch10.IrBuild.FibValues |
JIT via LLJIT: fib(n) for \(n = -5 \dots 200\), INT64_MIN, \(10^5\), against a C++ reference with wrapping |
ch10.IrBuild.HornerStructure |
R1; R3: argument names, exactly one mul, no alloca or call, at least one load |
ch10.IrBuild.HornerValues |
edge cases (\(n = 0\), \(n < 0\), \(n = 1\), \(x = 0\), \(x = -1\)) and 500 random coefficient arrays (up to 11 coefficients, some \(x\) large enough to wrap) |
ch10.IrBuild.DispatchStructure |
R1; R4: %op is i32, exactly one switch on %op with 9 cases |
ch10.IrBuild.DispatchValues |
every op in \(\{-1, 0, \dots, 9, 1000\}\) on 121 edge-value pairs (0, ±1, 63, 64, 65, INT64_MIN/MAX, …) plus 2000 random triples |
tests/ch10/lit/irbuild-{fib,horner,dispatch}.test |
the printed module verifies (opt -passes=verify), and FileCheck finds the define line with the argument names, the phis, the GEP and load, the single mul, the nine switch cases; --implicit-check-not rejects any alloca or call where R2 and R3 forbid them |
Milestones¶
buildDispatch: the easiest, because it has no loop. Runctest --test-dir build/<preset> -R 'ch10.IrBuild.Dispatch'.buildFib: a loop with three phis. Runctest --test-dir build/<preset> -R 'ch10.IrBuild.Fib'.buildHorner: a loop over memory. Runctest --test-dir build/<preset> -R 'ch10.IrBuild.Horner'.ctest --test-dir build/<preset> -R ch10.litfor the shapes.
Hints¶
Hint 1 — where to start
Draw the CFG first: which blocks exist, which edges, and which values flow along each edge. Then create all the blocks with BasicBlock::Create before you emit code into any of them, so you can branch forward. Lesson 10.3 §2 shows how the insertion point moves.
Hint 2 — the key idea
A phi's operands are (value, predecessor) pairs, and the back-edge value does not exist yet when you create the phi. Create the phi with CreatePHI(Ty, 2), build the loop body, and only then call addIncoming for both edges. The verifier checks that each phi has exactly one entry per predecessor (Ch 9), and a phi operand only has to dominate the end of its incoming edge's block, which is why the back-edge value may be defined later in the loop (Lesson 10.2, Theorem 10.2.13).
Hint 3 — a design sketch
@fib: blocks entry → {exit, loop}, loop → {loop, exit}, with phis i, a, b in loop and a result phi in exit. Keep the invariant \(a = F(i-1)\), \(b = F(i)\). @horner: iterate k from n down to 1 with an accumulator phi, load coeffs[k-1], and compute acc * x + c. @dispatch: CreateSwitch(op, default, 9), one block per case, all branching to a join block whose phi collects the nine results plus the default's 0. For case 8, CreateBinaryIntrinsic(Intrinsic::smin, …) or an icmp + select both work. The bug the tests catch most often is an off-by-one in @fib's exit condition (check \(n = 1\) and \(n = 2\)).
Stretch goals ★¶
- Add
buildFibRec(a recursive@fib), JIT both versions and time them. Then runopt -passes='default<O2>'on the recursive one and look at what LLVM does with the recursion. - Rebuild
@dispatchwithIRBuilder<InstSimplifyFolder>and constant%opvalues in a caller, and see which cases fold away.