Skip to content

Lab 0.L1 · Your first LLVM IR file

Chapter: 0 · Compiler Architectures & Your Toolchain · Lessons: 0.4 (LLVM IR as a shared IR), 0.6 (calling libc) · Time: 1–2 hours · Tests: ./course test 0 (the first-ir-* lit tests)

Goal

Write one LLVM IR module by hand — no C, no clang — that defines a constant function, a loop, and a main that prints through libc's printf, and run it with lli. You will meet the parts of LLVM IR every later chapter uses: functions, basic blocks, terminators, phi nodes, integer types and globals. The LLVM Language Reference is the specification; Chapter 9 teaches IR properly.

Requirements

Your file is labs/ch00-first-ir/first.ll (replace the placeholder).

  • R1. define i32 @answer() returns the constant 42.
  • R2. define i64 @sum_to(i64 %n) returns \(0 + 1 + \dots + n\) for \(n \ge 0\) and 0 for \(n < 0\), computed by a loop (LLVM's LoopInfo must find a natural loop in @sum_to); sum_to(100000) = 5000050000 does not fit in 32 bits.
  • R3. define i32 @main() prints exactly these two lines to standard output with printf, and returns 0:

answer = 42
sum_to(10) = 55
- R4. The module is valid IR: opt -passes=verify accepts it, and lli first.ll runs it. Use opaque pointers (ptr), no target triple is needed.

The contract

Black-box: the three function signatures above, the output of @main, and "contains a loop". The tests never look inside your functions otherwise.

lli labs/ch00-first-ir/first.ll                       # R3
opt -passes=verify -disable-output labs/ch00-first-ir/first.ll   # R4
opt -passes='print<loops>' -disable-output labs/ch00-first-ir/first.ll   # R2: "Loop at depth 1"

What the tests check

Test (tests/ch00/lit/) Checks
first-ir-verify.test R4: lli runs the file; opt -passes=verify accepts it
first-ir-main.test R3: the output is exactly the two lines (FileCheck --match-full-lines, nothing else printed)
first-ir-functions.test R1, R2: llvm-links your file with tests/ch00/lit/Inputs/first-ir-driver.ll and runs lli --entry-function=ch00_driver, which calls @answer() and @sum_to on 0, 1, 10, 100000 and −5
first-ir-loop.test R2: print<loops> reports Loop at depth 1 in @sum_to

Until you replace the placeholder, every test fails with TODO(ch00): L1: write labs/ch00-first-ir/first.ll. ./course test 0 --solution (or -DPEBBLE_USE_SOLUTION=first-ir) runs the same tests on the reference solution.

Milestones

  1. @answer and a @main that just ret i32 0 — lli first.ll; echo $? prints 0.
  2. @main calls printf with a global format string — first-ir-main.test passes for the first line.
  3. @sum_to with a loop — ctest --test-dir build/<preset> -R ch00.lit passes.

Hints

Hint 1 — where to start

Declare printf as declare i32 @printf(ptr, ...) and call it with the variadic syntax call i32 (ptr, ...) @printf(ptr @fmt, i32 %x). A string constant is a global array: @fmt = private constant [N x i8] c"...\0A\00" — N must count every byte including \0A and the final \00. If you get it wrong, the parser tells you ("got type '[14 x i8]' but expected '[13 x i8]'").

Hint 2 — the key idea

A loop in SSA form needs a header block with one phi per loop-carried value (%i and %s), each with one incoming value from the entry block and one from the block that jumps back (the latch). Compare %i with %n using icmp sgt or icmp slt (signed) so that negative %n exits immediately. Lesson 0.1's MLIR box and Lesson 0.4's sumloop.ll show the shape.

Hint 3 — a design sketch

Blocks entry → loop (two phis, compare, conditional branch) → body (two adds, branch back to loop) and exit (ret the sum phi). Use i64 throughout @sum_to; print it with %lld. The most common bug: a phi whose incoming block label is not an actual predecessor ("PHI node entries do not match predecessors" from the verifier).

Stretch goals ★

  • Add -O2: opt -passes='default<O2>' -S first.ll — what happens to your loop? (Compare with Lesson 0.3's closed-form box.)
  • Compile your file for three targets with llc -mtriple=... (Lesson 0.4) and link the x86-64 one with clang first.s -o first.