A stream of
A basic block is a maximal run of straight-line instructions with one entry and one exit: control enters only at the top and leaves only at the bottom, with no jumps into the middle and no jumps out of the middle. That single property is enormously freeing. Inside a block there are no surprises — every instruction runs, exactly once, in order, every time the block is entered. So the block is the natural unit of local analysis: whatever we prove about the top of a block still holds all the way down, because nothing can sneak in or out along the way.
To carve a TAC listing into blocks we first find the leaders — the instructions that begin a block. There are exactly three ways an instruction earns leader status:
Once the leaders are marked, each basic block is a leader together with all instructions up to — but not including — the next leader (or the end of the program). Every instruction lands in exactly one block, and the blocks tile the listing with no gaps and no overlaps. The rule is purely syntactic: you can run it with a highlighter and never think about what the code means.
The control-flow graph (CFG) has one node per basic block and a directed edge
A block ending in a conditional branch has two successors — the branch target and the
fall-through — so it fans out. Two distinguished nodes are usually added: an
entry feeding the first block, and an exit that every block with
no other successor flows into. The result below is built one block at a time from a little program
that computes
Notice the shape: block B1 ends in if t < 0 goto B3, so it has two
out-edges — a branch edge to B3 and a fall-through edge to B2. B2 ends in an
unconditional goto B4, so it has no fall-through — only the branch edge. B2
and B3 both funnel into B4, which is therefore said to have two
predecessors. This diamond — a split at B1, a merge at B4 — is the CFG fingerprint
of an if/else.
The CFG is just a directed graph, so it borrows all the usual graph vocabulary — but a few terms do the heavy lifting in code generation:
| Term | Meaning |
|---|---|
| successors of | the blocks reachable from |
| predecessors of | the blocks with an edge into |
| merge point | a block with two or more predecessors (a join in control flow) |
| branch / split | a block with two or more successors (ends in a conditional jump) |
| back edge | an edge |
A dataflow value that must be true "on entry to
The reason we bother building a graph at all is that loops are a property of the edges, not
of the source syntax. A while, a for, a goto that
jumps backward, even a recursive tangle of hand-written jumps — all of them show up in the CFG as
the same thing: a cycle. Formally, the strongly connected components of the CFG
capture exactly the sets of blocks that can repeatedly reach one another, and a
back edge (an edge whose head dominates its tail) is what closes such a
cycle. The blocks a back edge can reach form the loop's body — its
This matters because loops are where programs spend their time: a body executed
The trick is dominance. Block if that happens to
branch to an earlier label fails this test, because you can reach the branch without
passing through its target. Dominance is what lets the compiler distinguish "spaghetti that loops"
from "spaghetti that merely re-joins".
Here is the whole pipeline — leaders, then blocks, then edges — run on the Dragon Book's array-init
loop. Each TAC line is an object; a goto field marks a jump target and
cond marks it conditional (so the block also falls through). Watch which lines become
leaders and how the two conditional branches each spawn a back edge and a fall-through.
The output shows leaders 1, 2, 3, 10, four blocks, and — crucially — that the
conditional branches at lines 9 and 11 each produce two edges: a back edge that closes the
loop and a fall-through that escapes it. That pair of edges per conditional branch is the single most
important thing to get right.
The classic partitioning bug is to mark only one instruction around a jump. But a
conditional jump if c goto L creates two leaders at once: the
target L begins a block, and the instruction
immediately after the jump begins another block (because control can fall through when
c is false). Miss either one and your "basic block" secretly has two entry points or
two exit points — it is no longer basic, and every analysis built on it silently breaks.
The mirror-image slip is on the edges: people draw the branch edge to the target and forget
the fall-through edge. A block ending in a conditional branch always has two
successors; only an unconditional goto (or the exit) has a single
successor and no fall-through. When in doubt, ask: "if the condition is false, where does control
go?" — that answer is the fall-through edge.
With the program reshaped into a flow graph, the compiler finally has something to compute
over. The next lesson uses exactly this structure — one block at a time, with its successors
and predecessors — to