Skip to content

Lesson 9.3 — Control flow: terminators, phi and select, and the verifier's invariants

Techniques: terminators (ret, br, switch, indirectbr, invoke/callbr/resume, unreachable); phi and select; the verifier's invariants (well-formedness and the SSA dominance property) · Pebble uses: br, switch, ret, unreachable, phi, select · Lab: E1, E4, E7, E8, F1–F7 · Prerequisites: Lesson 9.1, Ch 15 (dominance; you only need Definition 9.3.2 below) · Time: 4 hours

The running example's if (a[i] > 0) s += a[i]; became two blocks and a phi (Lesson 9.1):

if.then:
  %add = add nsw i64 %s.0, %1
  br label %if.end
if.end:                                   ; preds = %if.then, %for.body
  %s.1 = phi i64 [ %add, %if.then ], [ %s.0, %for.body ]

Replace %s.1's use of %add by a direct use in %if.end (%s.1 = add i64 %add, 0) and the verifier refuses the function: "Instruction does not dominate all uses!". On the path for.body → if.end, %add was never computed. This lesson makes that rule and its companions precise: which instructions may end a block, what a phi means, why select exists next to it, and which invariants every function must satisfy. They are the rules you break in the lab's F-tasks.

1. Problem and motivation

A CFG-based IR has to say how a block hands control to the next (terminators), how values merge where paths join (phi), and which programs are meaningful at all (well-formedness). LLVM's choices follow the SSA literature: phi nodes at join points [CFRWZ91], one terminator per block, and a verifier that checks the SSA dominance property after every pass in debug builds [LA04, §2.1; LLVM-LangRef, §Well-Formedness].

Terminators

Every basic block ends with exactly one instruction that transfers control: return, branch, multiway branch, computed jump, a call that can unwind, or "this point is never reached". Making these explicit instructions, rather than implicit fall-through, is what lets block order be arbitrary: LLVM can move blocks around without changing semantics. Exception handling was the hard case: a call that may throw has two successors, so it must be a terminator (invoke), which is why LLVM has both call and invoke [LLVM-EH].

Phi and select

At a join, SSA needs to name "the value of s that arrived along whichever edge was taken". Cytron et al. introduced the \(\phi\)-function for this [CFRWZ91]; LLVM's phi instruction lists one incoming value per predecessor edge. select is the data-flow counterpart: the same choice without control flow, which lets the optimizer turn small diamonds into straight-line code (and the back end into cmov/csel).

The verifier's invariants

An IR that is transformed by hundreds of passes needs a precise notion of "valid", checked mechanically, or bugs surface far from their cause. LLVM's Verifier pass checks well-formedness; the parser runs it on every .ll file, and opt runs it before writing output. The central invariant is the SSA dominance property: every use is dominated by its definition, so that every value a use reads has actually been computed [CFRWZ91; ZNMZ12; SSAB, Ch. 2].

2. Definitions and algorithms

Definition 9.3.1 (Terminators and successors)

The terminators of LLVM 23 and their successor lists \(\mathrm{succs}(t)\) are:

terminator successors meaning
ret [τ v] none return (the value, if any) to the caller
br label %d \(\langle d \rangle\) jump
br i1 %c, label %t, label %f \(\langle t, f \rangle\) to \(t\) if \(c = 1\), to \(f\) if \(c = 0\)
switch iN %v, label %d [ iN c1, label %b1 … ] \(\langle d, b_1, \dots, b_k \rangle\) to \(b_j\) if \(v = c_j\) (the \(c_j\) distinct constants), else to \(d\)
indirectbr ptr %a, [label %b1, …] \(\langle b_1, \dots, b_k \rangle\) to the block whose address (blockaddress) \(a\) is; UB if it is not listed
invoke … to label %n unwind label %u \(\langle n, u \rangle\) call; to \(n\) on return, to \(u\) if the callee unwinds
callbr … to label %d [label %i1, …] \(\langle d, i_1, \dots \rangle\) inline asm goto: fall through or jump to an indirect target
resume {ptr, i32} %e none continue unwinding with exception \(e\)
catchswitch, catchret, cleanupret listed labels funclet-based exception handling (Windows)
unreachable none executing it is undefined behavior

Successors are counted with multiplicity: switch i32 %x, label %d [i32 1, label %j i32 2, label %j] gives \(j\) twice. The CFG of Definition 9.1.5 uses these edges.

Definition 9.3.2 (Dominance)

In the CFG \(G_f = (B, E, b_0)\), block \(d\) dominates block \(b\), written \(d \mathrel{\mathrm{dom}} b\), if every path from \(b_0\) to \(b\) contains \(d\); \(d \mathrel{\mathrm{sdom}} b\) if moreover \(d \ne b\) (as in Ch 15). A block not reachable from \(b_0\) is dominated by every block (the convention LLVM uses).

Definition 9.3.3 (A definition dominates a use)

Let value \(v\) be defined by instruction \(i_v\) in block \(b_v\) (a parameter counts as defined before the first instruction of \(b_0\)). A use of \(v\) as operand of instruction \(i\) in block \(b\) is dominated by its definition if

  • \(i\) is not a phi, and either \(b_v = b\) and \(i_v\) comes before \(i\) in \(b\), or \(b_v \mathrel{\mathrm{sdom}} b\); or
  • \(i\) is a phi and \(v\) is its incoming value for the edge \(p \to b\), and \(b_v \mathrel{\mathrm{dom}} p\) (the definition is available at the end of the predecessor, where the value is read); or
  • \(b\) is unreachable from \(b_0\).

If \(i_v\) is an invoke, its result exists only after a normal return, so "\(b_v \mathrel{\mathrm{sdom}} b\)" (for a phi, "\(b_v \mathrel{\mathrm{dom}} p\)") is strengthened to: every path from \(b_0\) to \(b\) (for a phi: every path to \(p\) followed by the edge \(p \to b\)) passes through the edge from \(b_v\) to the invoke's normal destination. This is the edge-dominance test DominatorTree::dominates(const BasicBlockEdge &, const Use &) that the verifier applies.

Definition 9.3.4 (Well-formed function)

A function is well formed if:

  • (W1) every block ends in exactly one terminator and contains no other (Definition 9.1.5);
  • (W2) the entry block \(b_0\) has no predecessors (and therefore no phis);
  • (W3) in every block, all phi instructions come before any other instruction (and a landingpad or other EH pad, if present, is the first non-phi);
  • (W4) every phi in block \(b\) has exactly one entry per incoming edge of \(b\), counted with multiplicity, entries for the same predecessor carry the same value, and no entry names a non-predecessor;
  • (W5) operand and result types agree with the instruction (for example, a br condition is i1, ret returns the function's type);
  • (W6) every use is dominated by its definition (Definition 9.3.3); in particular no non-phi instruction uses its own result;
  • (W7) exception handling is consistent: the unwind destination of an invoke starts (after phis) with a landingpad, and a function containing one has a personality.

The parser enforces W1 (it starts a new block after each terminator) and W5; the Verifier enforces W2–W7.

Theorem 9.3.5 (SSA dominance property)

Let \(f\) be well formed and consider any execution of \(f\) that reaches a use of \(v\) in a reachable block. Then \(v\)'s definition was executed on that execution, and the value read is the one produced by the most recent execution of that definition before the use (for a phi use: before leaving the predecessor).

Proof

An execution follows a path \(\pi = b_0 \to b_1 \to \dots \to b_m = b\) of the CFG. Non-phi use, \(b_v \ne b\): by (W6) \(b_v \mathrel{\mathrm{sdom}} b\), so by Definition 9.3.2 the path \(\pi\) contains \(b_v\), and since the path ends at \(b \ne b_v\), it passes through all of \(b_v\), executing \(i_v\) (blocks execute to their terminator, W1). If \(i_v\) is an invoke, the strengthened condition of Definition 9.3.3 says that \(\pi\) uses the invoke's normal edge, so the call returned normally and defined \(v\). Non-phi use, \(b_v = b\): \(i_v\) precedes \(i\) in \(b\), so it executed earlier during this visit of \(b\). Phi use on edge \(p \to b\): the execution reached \(b\) from \(p = b_{m-1}\), so the prefix \(b_0 \to \dots \to p\) is a path to \(p\), which contains \(b_v\) because \(b_v \mathrel{\mathrm{dom}} p\); the definition executed before leaving \(p\). Most recent: each execution of \(i_v\) produces a new value for the single name \(v\) (SSA, Definition 9.1.6), and the use reads the name's current value, i.e. the last one produced. Parameters are defined on entry, before everything.

Algorithm 9.3.6 (Writing a counted loop in SSA form by hand)

  • Input: a loop for (x1 = e1, …, xk = ek; cond(x); ) body whose body updates the loop-carried variables \(x_1, \dots, x_k\), and whose result is some \(x_r\) after the loop.
  • Output: a well-formed CFG with blocks entry, header, body, exit and no memory for the \(x_j\).
  • Precondition: \(e_1, \dots, e_k\) and every value used by cond and body other than the \(x_j\) are defined in or before entry.
  • Postcondition: W1–W6 hold; at the start of each iteration, phi \(x_j\) holds the variable's current value.
  • Invariant: every value used in header or body is either a header phi, a value defined earlier in the same block, or defined in entry (which dominates both).
function WriteLoop(vars x1..xk, inits e1..ek, cond, body, result xr):
    emit entry:    compute e1..ek; "br label %header"
    emit header:   for j in 1..k: "%xj = phi τj [ ej, %entry ], [ %xj.next, %body ]"
                   "%c = <cond on %x1..%xk>"
                   "br i1 %c, label %body, label %exit"
    emit body:     compute %x1.next..%xk.next from %x1..%xk     # new names: no updates in place
                   "br label %header"
    emit exit:     "ret τr %xr"          # header dominates exit: %xr is available

A guarded variant tests the condition once in entry and jumps straight to exit, so that the loop runs at least once when entered; exit then needs a phi with one entry per incoming edge (lab E1, @collatz_steps).

Algorithm 9.3.7 (Checking well-formedness in LLVM's order)

  • Input: the text of one function.
  • Output: "valid", or the first violated rule of Definition 9.3.4.
  • Precondition: the text uses only the constructs of Definition 9.3.1 and ordinary instructions.
  • Postcondition: the reported rule is the one llvm-as/opt -passes=verify reports first.
  • Invariant: when step \(k\) runs, the rules of steps \(1, \dots, k - 1\) hold, so each step may rely on them (for example, dominance is only computed on a CFG whose blocks all have terminators).
function CheckFunction(text):
    # parser (llvm-as)
    1: for each block: if code follows a terminator, or a label follows a non-terminator → W1
    2: for each name: if defined twice → "multiple definition"; if used but never defined → "undefined value"
    3: for each use: if its type ≠ the type of its definition → W5 ("defined with type …")
    4: for each ret: if the returned type ≠ the function's → W5
    # verifier (Verifier::visitFunction, visitBasicBlock, visitPHINode, visitInstruction)
    5: if preds(b0) ≠ ∅ → W2
    6: for each block: if a phi follows a non-phi → W3
    7: for each block b, phi φ: if multiset of φ's blocks ≠ multiset of preds(b) → W4
    8: for each non-phi i: if i uses itself → W6 ("Only PHI nodes may reference their own value!")
    9: compute the dominator tree; for each use: if not dominated (Definition 9.3.3) → W6
    return valid

The drill ir-validity uses exactly this order (tools/course/lib/llvmir.py, check_function).

Phi and select semantics.

Definition 9.3.8 (Phi and select)

Let block \(b\) start with phis \(\phi_1, \dots, \phi_k\). When control transfers along edge \(p \to b\), the phis are assigned simultaneously: each \(\phi_j\) takes the value its entry for \(p\) had at the end of \(p\). A select i1 %c, τ %x, τ %y evaluates to \(x\) if \(c = 1\) and \(y\) if \(c = 0\); it is poison if \(c\) is poison, and otherwise poison only if the chosen operand is (Lesson 9.7).

3. Worked example

Checking the running example (Algorithm 9.3.7). The blocks of @sum_pos in RPO are entry, for.cond, for.body, if.then, if.end, for.inc, for.end. Its dominator tree (Ch 15, computable with opt -passes='print<domtree>'):

flowchart TD
  entry([entry]) --> for.cond[for.cond]
  for.cond --> for.body[for.body]
  for.cond --> for.end[for.end]
  for.body --> if.then[if.then]
  for.body --> if.end[if.end]
  if.end --> for.inc[for.inc]

(if.end is dominated by for.body, not by if.then, because of the edge for.body → if.end.) The trace of the checks:

step rule what is checked on @sum_pos result
1 W1 7 blocks, each ends in br/ret ok
2 names 13 local definitions, each once; every used name is defined ok
3 W5 e.g. %cmp is i1 in br i1 %cmp; %s.0 is i64 in add nsw i64 ok
4 W5 ret i64 %s.0 in an i64 function ok
5 W2 preds(entry) = ∅ ok
6 W3 phis only at the tops of for.cond and if.end ok
7 W4 for.cond: phi blocks {entry, for.inc} = preds; if.end: {if.then, for.body} = preds ok
8 W6 no instruction uses itself ok
9 W6 see the dominance table below ok

Dominance of every non-trivial use (step 9):

use in block definition in rule of Definition 9.3.3 holds?
%s.0 in %add if.then for.cond for.cond sdom if.then yes
%s.0 in ret for.end for.cond for.cond sdom for.end yes
%s.0 entry of %s.1 for edge for.body → if.end if.end (phi) for.cond for.cond dom for.body yes
%add entry of %s.1 for edge if.then → if.end if.end (phi) if.then if.then dom if.then yes
%s.1 entry of %s.0 for edge for.inc → for.cond for.cond (phi) if.end if.end dom for.inc yes
%inc entry of %i.0 for edge for.inc → for.cond for.cond (phi) for.inc for.inc dom for.inc yes
%i.0 in %arrayidx2 if.then for.cond for.cond sdom if.then yes
hypothetical %add in ret for.end if.then if.then sdom for.end? no (path via for.body → if.end) W6 fails

The last row is the mistake from the introduction; it is lab task F1 in another function.

A phi swap. The simultaneity in Definition 9.3.8 matters when phis read each other. The loop header of the Fibonacci function of lab task F3

loop:
  %a = phi i64 [ 0, %entry ], [ %b, %loop ]
  %b = phi i64 [ 1, %entry ], [ %sum, %loop ]
  %sum = add i64 %a, %b

executes, on the back edge, \(a' = b\) and \(b' = \mathit{sum}\) at once:

iteration %a %b %sum
1 (from entry) 0 1 1
2 (back edge) 1 1 2
3 1 2 3
4 2 3 5

If the phis were executed one after the other in the order %b, then %a, the back edge after iteration 2 would first set %b ← %sum = 2 and then %a ← the new %b = 2, giving (2, 2) instead of (1, 2). Textual order (%a first) happens to give the right answer here, but only because %a reads %b before %b is overwritten; the semantics must not depend on the order in which phis are listed, so the reading %a ← %b always sees the old %b. Out-of-SSA translation must implement these parallel copies with care (Ch 16, the "swap problem").

Select from a diamond. simplifycfg turns the running example's if into a select (real-world box in the "Phi and select" section): it speculates %add = add nsw i64 %s.0, %0 into for.body, which now runs even when a[i] ≤ 0, and picks with %spec.select = select i1 %cmp1, i64 %add, i64 %s.0. If the speculated add nsw overflows on a path where the source would not have executed it, it is poison, but the select does not choose it. That is why select's poison rule is "only the chosen operand" (Lesson 9.7, Proposition 9.7.12).

Try it

./course drill ir-validity --seed 11 --difficulty medium --solution shows the step table of Algorithm 9.3.7 on a random function, one rule broken or none.

4. Invariants and correctness

Terminators

Proposition 9.3.9 (Block order does not matter)

Permuting the blocks of a well-formed function other than the entry block yields a well-formed function with the same executions.

Proof

No terminator falls through: every successor is named explicitly (Definition 9.3.1), so the CFG \(G_f\) does not depend on block order. Rules W1–W5 and W7 are local to blocks or instructions. Dominance (Definition 9.3.2) is a property of \(G_f\) and of the positions of instructions within blocks, both unchanged, so W6 is preserved. Executions follow CFG edges from the entry block, which is fixed, so they are unchanged.

unreachable deserves a separate remark: it has no successors, so a block ending in it has no way out, and reaching it is undefined behavior. Optimizers may therefore delete any code that must reach it, and may treat the branch that leads to it as never taken, which is how __builtin_unreachable() in a default: case turns a switch into a table lookup (real-world box below).

Phi and select

Proposition 9.3.10 (Branch to select is a refinement)

Let \(b\) end in br i1 %c, label %t, label %e where \(t\) and \(e\) each contain only speculatable instructions, compute \(x\) and \(y\) respectively, and branch to \(j\), whose phi is %r = phi [ %x, %t ], [ %y, %e ]. Replacing the diamond by the instructions of \(t\) and \(e\) in \(b\) followed by %r = select i1 %c, %x, %y produces a program whose every behavior is a behavior of the original.

Proof

Case \(c \in \{0, 1\}\): the original executes one arm and \(r\) is that arm's value; the new program executes both arms, which is allowed because their instructions are speculatable (no side effects, no immediate UB: e.g. add nsw may yield poison but never traps), and the select returns the same arm's value. If the other arm's value is poison, select ignores it (Definition 9.3.8). Case \(c\) poison: branching on poison is undefined behavior in the original (Lesson 9.7), so any behavior of the new program, which gets \(r\) = poison, is allowed. The converse fails: turning a select on a possibly-poison condition into a branch introduces UB, unless the condition is first freezed.

The verifier's invariants

Proposition 9.3.11 (Why the entry block may not have predecessors)

If (W2) held without its restriction, a phi in the entry block would have no well-defined value on function entry.

Proof

On entry, control arrives at \(b_0\) without traversing any CFG edge, so no phi entry applies (Definition 9.3.8 assigns phis only along edges). A phi in \(b_0\) would therefore be undefined on the first iteration. Forbidding predecessors of \(b_0\) also forbids phis there, since a phi with zero entries in a block with zero predecessors is useless and rejected. The lab's F4 is exactly this: a loop whose header is the entry block must get a new entry block.

The SSA dominance property (Theorem 9.3.5) is the invariant most passes silently rely on: GVN replaces a value by an equivalent one only if the replacement dominates the use; LICM hoists an instruction only to a block that dominates all its uses; SROA and mem2reg place phis precisely so that every new use is dominated. A pass that violates it produces IR that the verifier catches, and that without the verifier would read garbage.

When it breaks. Theorem 9.3.5 needs (W1): a block that could be left before its terminator would break "the path passes through all of \(b_v\)". Unwinding is the exception that proves the rule: an ordinary call that throws leaves the block in the middle, so a definition after the call is not executed. That is why a call that may unwind to a handler in the same function must be the terminator invoke, and why values defined by an invoke are available only in its normal successor: the verifier checks that the invoke's result is used only in blocks dominated by the normal edge.

5. Complexity

Technique Time (worst) Time (typical) Space Variables
Parsing blocks and terminators (W1) \(\Theta(i)\) linear \(O(i)\) \(i\) = instructions
Phi-entry check (W4) \(O(\sum_b \mathrm{phis}(b) \cdot \mathrm{preds}(b))\) linear \(O(\mathrm{preds})\) —
Dominance check (W6), per use \(O(1)\) with DFS intervals, after building the tree — \(O(n)\) \(n\) = blocks
Whole verifier \(O(i + u + T_{\mathrm{dom}}(n, e))\) (Proposition 9.3.12) near-linear \(O(n + u)\) \(u\) = uses, \(e\) = edges

Proposition 9.3.12 (Cost of Algorithm 9.3.7)

Algorithm 9.3.7 runs in \(O(i + u + T_{\mathrm{dom}}(n, e))\) time, where \(T_{\mathrm{dom}}\) is the time to build the dominator tree (Ch 15: near-linear for Lengauer–Tarjan, \(O(n^2)\) worst case for Semi-NCA as used by LLVM).

Proof

Steps 1–8 visit each instruction and each use a constant number of times, with hash lookups for names, \(O(i + u)\). Step 7 compares, per phi, a multiset of incoming blocks with the predecessor multiset; summed over all phis this is \(O(\sum_b \mathrm{phis}(b) \cdot \mathrm{preds}(b))\), bounded by the number of phi operands, i.e. by \(u\). Step 9 builds the dominator tree once, \(T_{\mathrm{dom}}(n, e)\), and then answers each use in \(O(1)\): same-block uses compare instruction positions (LLVM caches an order number per instruction), cross-block uses compare DFS entry/exit numbers of the tree.

Pathological input. A block with \(p\) predecessors and \(k\) phis has \(k \cdot p\) phi operands: a switch with \(10^4\) cases jumping to one join with 100 phis creates \(10^6\) operands, all checked. A same-block dominance query without cached instruction order would cost \(O(\text{block length})\), making a long block with many uses quadratic; LLVM renumbers instructions lazily (Instruction::comesBefore) to keep it \(O(1)\) amortized.

6. Variants and refinements

Terminators

  • Funclet-based EH (catchswitch, catchpad, cleanuppad, catchret, cleanupret) [LLVM-EH]: models Windows SEH/C++ EH, where handlers are separate "funclets", with tokens tying pads to their exits (Lesson 9.2); trade-off: more terminators and rules, but required by the Windows ABI.
  • callbr (asm goto): a call with multiple successors, needed by the Linux kernel's static keys; trade-off: critical edges that cannot be split normally.
  • Switch lowering in the back end (jump tables, bit tests, binary search): the IR keeps one switch; the choice is made late (Ch 11).

Phi and select

  • Block arguments (MLIR, SIL, Cranelift) instead of phis: branches pass values explicitly; no "one entry per predecessor" bookkeeping, and multiple edges from one block to another can carry different values (Lesson 9.8).
  • select with vector conditions chooses per lane; llvm.smax/smin/umax/umin/abs are canonical forms of common selects (the running example's select becomes smax under instcombine).

The verifier's invariants

  • Machine verifier (-verify-machineinstrs): the same idea for Machine IR after instruction selection (Ch 21).
  • Formal verification of the IR's well-formedness and of SSA-preserving transformations: Vellvm formalizes LLVM IR in Coq and proves the dominance property is preserved by mem2reg-like passes [ZNMZ12]; Alive2 validates transformations semantically [LLM+21].

7. In real compilers

Terminators

LLVM

llvm/include/llvm/IR/Instructions.h — classes ReturnInst, BranchInst (split in LLVM 23 into the subclasses UncondBrInst and CondBrInst), SwitchInst, IndirectBrInst, InvokeInst, CallBrInst, ResumeInst, UnreachableInst; llvm/lib/Transforms/Utils/SimplifyCFG.cpp — simplifySwitchLookup turns a switch that selects constants into a table named switch.table.<function> (LLVM 23.1.2).

  • GCC 15: GIMPLE has GIMPLE_COND, GIMPLE_SWITCH, GIMPLE_GOTO, GIMPLE_RETURN (gcc/gimple.def); exceptional edges are CFG edges flagged EDGE_EH, and a throwing call ends its block, the same constraint LLVM expresses with invoke.
  • rustc MIR: terminators are an enum (TerminatorKind::{Goto, SwitchInt, Return, Unreachable, Call { unwind, .. }, Assert, Drop, …}, compiler/rustc_middle/src/mir/syntax.rs); every call has an explicit unwind action.

Find where LLVM does it. In llvm/lib/Transforms/Utils/SimplifyCFG.cpp, find simplifySwitchLookup, which converts a switch into a lookup table. Question: what does the table hold for case values that are not listed when the default is unreachable?

switch, unreachable, and the lookup table they become

Reproduce (clang 23.1.2, opt 23.1.2):

cat > sw.c <<'EOF'
int classify(int op) {
  switch (op) {
  case 0: return 10;
  case 1: return 20;
  case 7: return 30;
  default: __builtin_unreachable();
  }
}
EOF
clang-23 --target=x86_64-unknown-linux-gnu -O0 -Xclang -disable-O0-optnone -S -emit-llvm sw.c -o - \
  | opt -passes=mem2reg -S | sed -n '/^define/,/^}/p'
echo ----
clang-23 --target=x86_64-unknown-linux-gnu -O1 -S -emit-llvm sw.c -o - | grep -E "^@|^define|^  [%r]"

Output (complete):

define dso_local i32 @classify(i32 noundef %0) #0 {
  switch i32 %0, label %5 [
    i32 0, label %2
    i32 1, label %3
    i32 7, label %4
  ]

2:                                                ; preds = %1
  br label %6

3:                                                ; preds = %1
  br label %6

4:                                                ; preds = %1
  br label %6

5:                                                ; preds = %1
  unreachable

6:                                                ; preds = %4, %3, %2
  %.0 = phi i32 [ 10, %2 ], [ 20, %3 ], [ 30, %4 ]
  ret i32 %.0
}
----
@switch.table.classify = private unnamed_addr constant [8 x i8] [i8 10, i8 20, i8 poison, i8 poison, i8 poison, i8 poison, i8 poison, i8 30], align 4
define dso_local range(i32 10, 31) i32 @classify(i32 noundef %0) local_unnamed_addr #0 {
  %2 = zext nneg i32 %0 to i64
  %3 = getelementptr inbounds nuw i8, ptr @switch.table.classify, i64 %2
  %4 = load i8, ptr %3, align 1
  %5 = zext i8 %4 to i32
  ret i32 %5

What to notice: the switch has four successors (Definition 9.3.1) and the join's phi one entry per incoming edge (W4). Because reaching unreachable is UB, -O1 may assume op ∈ {0, 1, 7}: it drops the range check, fills the holes of the table with poison, and even marks the index zext nneg and the result range(i32 10, 31).

invoke, landingpad, resume: a call with two successors

Reproduce (clang++ 23.1.2):

cat > eh.cpp <<'EOF'
struct Guard { ~Guard(); };
int risky(int);
int caller(int x) {
  Guard g;                       // needs a cleanup when risky() throws
  try { return risky(x); }
  catch (int e) { return -e; }
}
EOF
clang++-23 --target=x86_64-unknown-linux-gnu -O1 -fno-discard-value-names -S -emit-llvm eh.cpp -o - \
  | sed -n '/^define/,/^}/p'

Output (complete):

define dso_local noundef i32 @_Z6calleri(i32 noundef %x) local_unnamed_addr #0 personality ptr @__gxx_personality_v0 {
entry:
  %g = alloca %struct.Guard, align 1
  call void @llvm.lifetime.start.p0(ptr nonnull %g) #5
  %call = invoke noundef i32 @_Z5riskyi(i32 noundef %x)
          to label %cleanup unwind label %lpad

lpad:                                             ; preds = %entry
  %0 = landingpad { ptr, i32 }
          cleanup
          catch ptr @_ZTIi
  %1 = extractvalue { ptr, i32 } %0, 1
  %2 = tail call i32 @llvm.eh.typeid.for.p0(ptr nonnull @_ZTIi) #5
  %matches = icmp eq i32 %1, %2
  br i1 %matches, label %catch, label %ehcleanup

catch:                                            ; preds = %lpad
  %3 = extractvalue { ptr, i32 } %0, 0
  %4 = tail call ptr @__cxa_begin_catch(ptr %3) #5
  %5 = load i32, ptr %4, align 4, !tbaa !9
  %sub = sub nsw i32 0, %5
  tail call void @__cxa_end_catch() #5
  br label %cleanup

cleanup:                                          ; preds = %entry, %catch
  %retval.0 = phi i32 [ %sub, %catch ], [ %call, %entry ]
  call void @_ZN5GuardD1Ev(ptr noundef nonnull align 1 dereferenceable(1) %g) #5
  call void @llvm.lifetime.end.p0(ptr nonnull %g) #5
  ret i32 %retval.0

ehcleanup:                                        ; preds = %lpad
  call void @_ZN5GuardD1Ev(ptr noundef nonnull align 1 dereferenceable(1) %g) #5
  call void @llvm.lifetime.end.p0(ptr nonnull %g) #5
  resume { ptr, i32 } %0
}

What to notice: %call is used in the phi only on the edge from %entry, the invoke's normal edge: on the unwind edge it was never computed (the "When it breaks" remark of Section 4). The landing pad says what it handles (cleanup, catch ptr @_ZTIi), dispatches on the selector, and either catches or runs the destructor and resumes unwinding. The function carries a personality (W7). Lab task E8 writes a smaller version by hand.

indirectbr and callbr from computed goto and asm goto

Reproduce (clang 23.1.2):

cat > jumps.c <<'EOF'
int interp(const unsigned char *pc) {
  static void *table[] = { &&op_inc, &&op_halt };
  int acc = 0;
  goto *table[*pc];
op_inc:
  acc++; pc++; goto *table[*pc];
op_halt:
  return acc;
}
int flag(int x) {
  asm goto("testl %0, %0; jz %l[zero]" :: "r"(x) :: zero);
  return 1;
zero:
  return 0;
}
EOF
clang-23 --target=x86_64-unknown-linux-gnu -O1 -S -emit-llvm jumps.c -o - | grep -E "indirectbr|callbr|blockaddress"

Output (complete):

@interp.table = internal unnamed_addr constant [2 x ptr] [ptr blockaddress(@interp, %2), ptr blockaddress(@interp, %5)], align 16
  indirectbr ptr %12, [label %2, label %5]
  callbr void asm sideeffect "testl $0, $0; jz ${1:l}", "r,!i,~{dirflag},~{fpsr},~{flags}"(i32 %0) #2

What to notice: indirectbr lists every block it may reach, so the CFG stays exact even for a computed jump; the targets' addresses are blockaddress constants. callbr makes inline assembly a terminator with an extra (!i) successor.

Phi and select

LLVM

llvm/include/llvm/IR/Instructions.h — PHINode (incoming values and blocks stored in parallel arrays, getIncomingValueForBlock) and SelectInst; llvm/lib/Transforms/Utils/SimplifyCFG.cpp — foldTwoEntryPHINode and SimplifyCFGOpt::speculativelyExecuteBB turn small diamonds and triangles into select (Proposition 9.3.10) (LLVM 23.1.2).

  • GCC 15: GIMPLE_PHI nodes (# s_5 = PHI <s_15(4), s_11(5)> in Lesson 9.8's dump) and COND_EXPR for selects; the if-conversion pass is tree-ssa-phiopt.cc.
  • Cranelift 37: no phis; select exists as an instruction, and block parameters carry merges (cranelift/codegen/src/ir/dfg.rs).

Find where LLVM does it. In llvm/lib/Transforms/Utils/SimplifyCFG.cpp, find SimplifyCFGOpt::speculativelyExecuteBB. Question: which name does it give the select instructions it creates (visible in the box below)?

simplifycfg speculates an add nsw and merges with select

Reproduce (clang 23.1.2, opt 23.1.2):

cat > sum_pos.c <<'EOF'
long long sum_pos(const long long *a, long long n) {
  long long s = 0;
  for (long long i = 0; i < n; i++)
    if (a[i] > 0)
      s += a[i];
  return s;
}
EOF
clang-23 --target=x86_64-unknown-linux-gnu -O0 -Xclang -disable-O0-optnone -fno-discard-value-names \
  -S -emit-llvm sum_pos.c -o sum_pos.ll
opt -passes='mem2reg,early-cse,simplifycfg' -S sum_pos.ll | sed -n '/^for.body/,/^for.end/p'
opt -passes='mem2reg,early-cse,simplifycfg,instcombine' -S sum_pos.ll | grep -E "call i64 @llvm.smax|spec.select ="

Output (complete):

for.body:                                         ; preds = %for.cond
  %arrayidx = getelementptr inbounds i64, ptr %a, i64 %i.0
  %0 = load i64, ptr %arrayidx, align 8
  %cmp1 = icmp sgt i64 %0, 0
  %add = add nsw i64 %s.0, %0
  %spec.select = select i1 %cmp1, i64 %add, i64 %s.0
  %inc = add nsw i64 %i.0, 1
  br label %for.cond, !llvm.loop !5

for.end:                                          ; preds = %for.cond
  %add = call i64 @llvm.smax.i64(i64 %0, i64 0)
  %spec.select = add nuw nsw i64 %s.0, %add

What to notice: early-cse removed the second load of a[i], which made if.then speculatable; simplifycfg then hoisted %add = add nsw … (executed now even when a[i] ≤ 0, where it may be poison) and replaced the phi by a select that never chooses the poison (Proposition 9.3.10). instcombine rewrote s + (a[i] > 0 ? a[i] : 0) as s + smax(a[i], 0), the form in clang's -O2 output.

The verifier's invariants

LLVM

llvm/lib/IR/Verifier.cpp — Verifier::visitFunction (W2: "Entry block to function must not have predecessors!"), Verifier::visitBasicBlock (W4: "PHINode should have one entry for each predecessor…", "PHI node entries do not match predecessors!"), Verifier::visitPHINode (W3), Verifier::visitInstruction (self-reference), Verifier::verifyDominatesUse (W6: "Instruction does not dominate all uses!") (LLVM 23.1.2) [LLVM-Verifier].

  • GCC 15: verify_ssa (gcc/tree-ssa.cc) checks that every SSA name's definition dominates its uses and that PHI arguments match the incoming edges; verify_gimple_in_cfg (gcc/tree-cfg.cc) checks statement types, run with -fchecking.
  • Swift SIL: SILVerifier (lib/SIL/Verifier/SILVerifier.cpp) checks the same dominance rule for block arguments plus ownership rules.

Find where LLVM does it. In llvm/lib/IR/Verifier.cpp, find the message "PHI node has multiple entries for the same basic block with different incoming values!". Question: which W-rule of Definition 9.3.4 does it enforce?

Phi entries are per edge, not per block

Reproduce (opt 23.1.2):

cat > dup.ll <<'EOF'
define i32 @f(i32 %x) {
entry:
  switch i32 %x, label %other [ i32 1, label %join
                                i32 2, label %join ]
other:
  br label %join
join:
  %r = phi i32 [ 10, %entry ], [ 20, %other ]
  ret i32 %r
}
EOF
opt -passes=verify -disable-output dup.ll
sed 's/\[ 10, %entry \], \[ 20, %other \]/[ 10, %entry ], [ 11, %entry ], [ 20, %other ]/' dup.ll > dup2.ll
opt -passes=verify -disable-output dup2.ll
sed 's/\[ 10, %entry \], \[ 20, %other \]/[ 10, %entry ], [ 10, %entry ], [ 20, %other ]/' dup.ll > dup3.ll
opt -passes=verify -disable-output dup3.ll && echo "dup3.ll verifies"

Output (complete):

PHINode should have one entry for each predecessor of its parent basic block!
  %r = phi i32 [ 10, %entry ], [ 20, %other ]
opt: dup.ll: error: input module is broken!
PHI node has multiple entries for the same basic block with different incoming values!
  %r = phi i32 [ 10, %entry ], [ 11, %entry ], [ 20, %other ]
label %entry
i32 11
i32 10
opt: dup2.ll: error: input module is broken!
dup3.ll verifies

What to notice: the switch reaches %join along two edges, so the phi needs two entries for %entry (W4 counts with multiplicity), and they must agree because one block cannot pass two different values to the same phi. Block arguments (Lesson 9.8) lift exactly this restriction.

8. Comparison

Technique Power / precision Speed (asymptotic · practical) Output / error quality Implementation effort Typical use
Terminators exact CFG, including computed jumps (indirectbr), unwinding (invoke) and asm goto (callbr) successor lists \(O(1)\) per block explicit edges; unreachable enables deletion moderate; EH adds several instructions every function; invoke for C++/Rust unwinding
Phi and select phi: merges at joins, one entry per edge; select: branch-free choice that ignores unchosen poison phi check \(O(\text{operands})\); select is one instruction phi entries must match predecessors exactly phi bookkeeping on every CFG edit phis at joins; selects after if-conversion, cmov
The verifier's invariants W1–W7, with the SSA dominance property \(O(i + u + T_{\mathrm{dom}})\) the offending instruction printed with a message ~8 000 lines (Verifier.cpp) parser, opt, every pass in debug builds

Choose invoke for any call that may unwind to a handler in the same function; a plain call in a function with no handler just unwinds through. Choose select when both values are cheap and speculatable; keep the branch when one arm is expensive or has side effects. Run the verifier after every transformation you write (Chapter 12's test harness does it for you): its messages point at the rule (W1–W7) you broke.

9. Assessment

Technique Quiz ids (solutions/quizzes/ch09.yaml) Drill Flashcard tag Exercises
Terminators switch-successors, invoke-normal-edge, find-switch-table ./course drill ir-validity (missing terminators) terminators E4, E7, E8
Phi and select phi-parallel, select-poison, branch-to-select ./course drill poison-propagation (select) phi-select E1, E2, F2, F3
The verifier's invariants verifier-rule-of-error, dominance-use, find-phi-dup-check ./course drill ir-validity --difficulty hard verifier F1–F7

A phi reads its operand at the end of the predecessor, not at the phi

In %x = phi i64 [ %v, %p ], %v need not dominate the phi's block, only the end of %p. A value defined in the loop body can feed the header's phi on the back edge; the same value cannot be used by a non-phi instruction in the header.

References

See the chapter references.