Lab 10.2 · An IR inspection and rewriting tool¶
Chapter: 10 · The LLVM C++ API · Lessons: 10.1, 10.2, 10.4, 10.7 · Time: 4–6 hours · Tests: ./course test 10 (labels ch10)
Goal¶
Write ch10-tool, a small command-line tool in the style of LLVM's own llvm-* utilities. It reads a textual .ll file and does one of three things:
--statsprints counts per function;--def-useprints every value's def-use chain in a canonical order;--rewrite-pow2replaces multiplications, unsigned divisions and unsigned remainders by powers of two with shifts and masks, then prints the module.
You will practise loading IR with error handling, walking the ownership tree, following use lists, casting with dyn_cast, and rewriting while iterating without invalidating your iterator. The rewrite must also keep the program's meaning, and the tests run the program before and after under lli to check that.
Requirements¶
- R1 (CLI and errors). Usage is
ch10-tool --stats|--def-use|--rewrite-pow2 <file.ll>. - Wrong argument count or unknown mode: print a line containing
usage: ch10-tool --stats|--def-use|--rewrite-pow2 <file.ll>to stderr and exit 2. - Unreadable file, parse error, or a module that fails
verifyModule: print a diagnostic containingerror:to stderr and exit 1. - Success: exit 0.
- On failure, nothing goes to stdout.
- R2 (
--stats). Print exactly the format in "Output formats": one header line, then for every defined function in module order its block count, instruction count, phi count and call count (isa<CallBase>: call, invoke, callbr), followed by one line per opcode present (Instruction::getOpcodeName()), sorted by opcode name. - R3 (
--def-use). For every defined function in module order, print its name, then one line per argument (in order) and per non-void instruction (in program order). Each line gives the value and its uses: - The value is printed as
printAsOperandprints it without the type, so unnamed values appear as%0,%1, … - Each use is written
<block>:<index>#<operand>, where<block>is the user's block asprintAsOperandprints it,<index>is the user's 0-based position in that block, and<operand>isUse::getOperandNo(). - Uses are sorted by (block position in the function, index, operand number). Do not print them in use-list order: that order is an implementation detail (Lesson 10.2).
- R4 (
--rewrite-pow2: what). In every defined function, rewrite every scalar integer instruction of these forms, where \(C = 2^k\) is aConstantIntwhose value is a power of two as an unsigned number: mul X, Candmul C, Xbecomeshl X, k;udiv X, C(divisor on the right only) becomeslshr X, k;urem X, Cbecomesand X, C-1.
Everything else stays as it is, including vectors, sdiv/srem and non-power-of-two constants.
- R5 (flags). mul nuw becomes shl nuw. mul nsw becomes shl nsw only when \(k < w - 1\) (\(w\) = bit width): multiplying by \(2^{w-1}\) is not the same as shifting by \(w-1\) under nsw: as a signed number the constant is \(-2^{w-1}\), so mul nsw %x, C is defined for \(x = 1\) while shl nsw 1, w-1 is poison (Ch 9, poison, Ch 13). udiv exact becomes lshr exact.
- R6 (identity). The new instruction takes the old one's name (takeName) and debug location. All uses are redirected to it (RAUW), and the old instruction is erased.
- R7 (output). Print the rewritten module to stdout with Module::print and a line rewrote <N> instruction(s) to stderr: instruction for \(N = 1\), instructions otherwise. The printed module must verify.
- R8 (safety). Rewriting happens while iterating over the function. Your iteration must not touch an erased instruction; use make_early_inc_range (Lesson 10.1, Theorem on early increment) or collect first. The input tests/ch10/lit/tool-rewrite-iterate.ll has rewrites at the first and last position of blocks and in blocks that contain nothing else.
- R9 (semantics). The rewritten program must behave exactly like the input. The tests run both under lli and compare stdout.
The contract¶
A black-box CLI (LABS.md): the lit tests run the executable and check its output. The provided tools/Main.cpp forwards argv to one function you implement:
// labs/ch10-irtool/include/irtool/Tool.h (provided; do not change)
namespace irtool {
/// Runs `ch10-tool <Args...>`; returns the exit code (0, 1 or 2 as in R1).
int run(llvm::ArrayRef<llvm::StringRef> Args, llvm::raw_ostream &Out, llvm::raw_ostream &Err);
}
Output formats¶
For labs/ch10-irtool/inputs/small.ll:
$ ch10-tool --stats inputs/small.ll
functions: 2 defined, 1 declared
@f: blocks=3 instructions=9 phis=1 calls=1
add: 1
br: 2
call: 1
icmp: 1
mul: 1
phi: 1
ret: 1
sub: 1
@g: blocks=1 instructions=4 phis=0 calls=0
ret: 1
shl: 1
store: 2
$ ch10-tool --def-use inputs/small.ll
@f
%x: 2 uses: %entry:0#0 %entry:2#1
%y: 1 use: %entry:0#1
%a: 4 uses: %entry:1#0 %entry:1#1 %then:0#0 %join:1#1
%b: 2 uses: %entry:2#0 %join:0#1
%c: 1 use: %entry:3#0
%0: 1 use: %join:0#0
%p: 1 use: %join:1#0
%q: 1 use: %join:2#0
@g
%out: 2 uses: %0:1#1 %0:2#1
%v: 2 uses: %0:0#0 %0:2#0
%1: 1 use: %0:1#0
A value with no uses prints %v: 0 uses. Note 1 use (singular) versus 2 uses, and that a use line ends right after the last use (no trailing space). In @g the entry block is unnamed and gets slot %0, so the first unnamed instruction is %1.
For --rewrite-pow2, stdout is the whole module as Module::print writes it, and stderr is the summary line (see tests/ch10/lit/tool-rewrite.test for the exact expected IR).
Provided infrastructure¶
| File | What it gives you |
|---|---|
include/irtool/Tool.h |
the contract |
tools/Main.cpp |
main(), forwarding to irtool::run |
inputs/small.ll, inputs/pow2.ll |
sample inputs (the tests use them) |
src/Stub.cpp |
a stub that stops with TODO(ch10) |
What the tests check¶
| Test | Checks |
|---|---|
ch10.IrTool.ExitCodes |
R1 through irtool::run: 0 on success, 2 for four kinds of usage error (with usage: on stderr), 1 for a missing file and for a module that fails the verifier; nothing on stdout on failure |
tests/ch10/lit/tool-errors.test |
R1 through the executable: usage message and error: diagnostics for a missing file, a parse error and Inputs/invalid.ll |
tests/ch10/lit/tool-stats.test |
R2, whole output line by line (--match-full-lines) |
tests/ch10/lit/tool-def-use.test, tool-def-use-unused.ll |
R3, whole output; values without uses, a value used twice by one instruction, void instructions omitted |
tests/ch10/lit/tool-rewrite.test |
R4–R7 and R9 on inputs/pow2.ll: the summary line (14 rewrites), the verifier, the exact rewritten instructions including flags, untouched sdiv/udiv C, X/vector/non-power-of-two cases, and identical lli output before and after |
tests/ch10/lit/tool-rewrite-iterate.ll |
R8 and R9: rewrites at block boundaries, across a loop and in a phi-carried chain; lli equivalence |
Milestones¶
- Parse, verify and report errors (R1):
ctest --test-dir build/<preset> -R 'ch10.IrTool'. --stats, then--def-use:ctest --test-dir build/<preset> -R ch10.lit(the stats and def-use tests go green first).--rewrite-pow2: the remaining lit tests.
Hints¶
Hint 1 — where to start
parseIRFile(Path, Diag, Ctx) returns nullptr on failure and fills an SMDiagnostic. SMDiagnostic::print(ProgName, Err) writes a message that already contains error:. A module can parse and still be invalid (a use before its definition), so call verifyModule too.
Hint 2 — the key idea
For --def-use, you need the position of each user before you can sort. Number every instruction once per function (block number, index) in a DenseMap<const Instruction *, …>, then walk V.uses() and sort by that key. ModuleSlotTracker makes repeated printAsOperand calls fast and consistent. For the rewrite, dyn_cast<BinaryOperator>, getOpcode(), and dyn_cast<ConstantInt> on each operand are all you need. Create the replacement before the old instruction (an iterator insert position), then RAUW, then erase.
Hint 3 — a design sketch
One function per mode plus a small pow2Exponent(Value *) -> optional<unsigned>. The rewrite loop is for (Instruction &I : make_early_inc_range(instructions(F))): the iterator already points past I when the body erases I. Common bugs the tests catch: printing uses in use-list order (reversed); keeping nsw for a shift by \(w-1\); rewriting udiv C, X (constant on the left); rewriting vector multiplications; forgetting takeName, which renames the value and breaks the FileCheck lines.
Stretch goals ★¶
- Add
--rewrite-pow2=signed:sdiv X, 2^k→ashrwith the rounding fix-up (\(x + ((x \gg_s (w-1)) \gg_u (w-k))\)), and check it withllion negative inputs. - Add
--dot-def-useto print the def-use graph as Graphviz.