Irreducible loops | MaskRay
H whose head H dominates its tail T defines a loop with the single entry H. This works only for reducible control flow graphs. Opti">
H whose head H dominates its tail T defines a loop with the single entry H. This works only for reducible control flow graphs. Opti">
2026-07-12
The dominator tree lets<br>us identify natural loops:<br>a back edge T->H whose head H dominates its<br>tail T defines a loop with the single entry H.<br>This works only for reducible control flow graphs. Optimized<br>machine code and decompiler output routinely contain<br>irreducible loops, which have more than one entry and thus no<br>dominating header, so the dominator-based method cannot see them.
This post builds a loop-nesting forest for an arbitrary CFG with the<br>single-pass depth-first search of 韦韬、毛剑、邹维、陈宇(Tao Wei, Jian<br>Mao, Wei Zou & Yu Chen) A New Algorithm for Identifying Loops in<br>Decompilation, SAS 2007 (The 14th International Static Analysis<br>Symposium).
Reducibility
A depth-first search from the entry classifies every non-tree edge<br>relative to the spanning tree. A retreating edge<br>u->v goes to an ancestor v of<br>u on the DFS path. A cycle is a closed path in the<br>CFG, a graph-theoretic object with no distinguished entry; a<br>loop is the control-flow structure built on top — a set of<br>nodes with a header, nested into a forest. (LLVM draws the same line:<br>LoopInfo represents natural loops, while the<br>GenericCycleInfo used below generalizes them to irreducible<br>control flow.) A CFG is reducible when, in every DFS, each<br>retreating edge is a back edge — its head v<br>dominates its tail u. Then every cycle lies in a natural<br>loop whose header dominates it, so control enters the loop only through<br>that header.
A CFG is irreducible when some cycle has two or more<br>entries: nodes with a predecessor outside the cycle. No single node<br>dominates the cycle, so there is no natural header. The smallest example<br>is the irreducible core:
The irreducible core: 0 enters the 1↔︎2<br>cycle at both nodes. Double circle = header (1, visited first); dashed =<br>the re-entry edge.
Node 0 branches to both 1 and 2, and 1 -> 2 -> 1<br>is a cycle entered at 1 (through 0->1) and at 2 (through<br>0->2). M.S. Hecht and J.D. Ullman proved that a CFG is<br>irreducible if and only if it contains this three-node pattern as a<br>subgraph, allowing each edge to be a path through other nodes.
Because no node dominates an irreducible cycle, its header is not<br>intrinsic — we must pick one of the entries. The standard choice<br>(Havlak, LLVM, and the algorithm below) is the node the DFS reaches<br>first, i.e. the loop member with the smallest preorder number. So the<br>loop-nesting forest of an irreducible CFG depends on the DFS order.
Loop-nesting forest
There is no agreed definition of the loop-nesting forest for<br>irreducible CFGs. Steensgaard, Sreedhar–Gao–Lee, Havlak, and Ramalingam<br>each give a different one; for the CFG in Wei et al.'s Fig. 3 they<br>report two, one, three, and one loops respectively. We adopt Havlak's,<br>the finest, because it gives each loop a single header and the fewest<br>gotos when re-structuring, and it is what LLVM's<br>GenericCycleInfo computes:
The outermost loops are the maximal strongly connected regions (with<br>at least one internal edge).
A loop's header is its minimum-preorder node.
Its inner loops are the loops of the subgraph induced on (loop nodes<br>− header), found recursively.
The forest has a compact encoding. For each node record its<br>innermost loop header iloop_header: the header of<br>the smallest loop containing it, or none. A header's own<br>iloop_header is the header of its parent loop. Following<br>the iloop_header links from a node lists its enclosing<br>loops innermost-first — the "loop header list" of the paper. Which nodes<br>are headers, plus every node's innermost header, determines the whole<br>forest; that is what the program below prints.
Representing and querying<br>the forest
Building the forest is half the job; every consumer then asks two<br>questions repeatedly: is a node inside loop L, and is loop<br>L2 nested in L1? How the forest is stored<br>decides whether these are O(1).
The obvious layout gives each loop the set of all its nodes — its own<br>plus every descendant's. A membership test is one hash probe, but a node<br>nested d loops deep is stored d times, so<br>memory and build cost are O(N * depth). This is what<br>GenericLoopInfo and GenericCycleInfo did: a<br>set per cycle.
An Euler tour of the forest removes the duplication. Lay every node<br>into one shared array so each loop owns a contiguous slice<br>[begin, end) nested inside its parent's. Each node then<br>appears once, memory is O(N), and both queries become<br>interval tests:
is a node inside L: map the node to its innermost loop,<br>then test that loop's index against L's slice;
is L2 nested in L1:<br>L1.begin .
No per-loop set survives — the same trick DominatorTree<br>uses for O(1) dominance (DFSNumIn/DFSNumOut),<br>and the direction GenericCycleInfo took.
The contiguous slices suit a build-once analysis...