Graph algorithms · which statements are in which loop, including the nested ones

DJ graphs and natural loop bodies

Reducibility answers yes or no, and roughly where. It does not say which statements belong to which loop when there is more than one, nested or side by side. One structure — the DJ graph — answers that, by processing a method's loops from the inside out, one dominator level at a time.

Where this shows up in reverse engineering

Knowing a method is reducible is not the same as knowing which statements belong to which loop — and once there is more than one loop, nested or side by side, that distinction is exactly what the next tool in the pipeline needs.

The problem

Reducibility and loop detection settles whether a flowgraph is well-behaved at all, and the strongly-connected-components method there points at roughly where the trouble is. Neither one hands back the thing a structuring tool actually needs: for this specific loop, the exact set of statements inside it — and when one loop sits inside another, the inner one's statements need to be told apart from the outer one's before either body means anything.

The structure below, the DJ graph, was introduced by Sreedhar and Gao for a different problem — placing φ-nodes in static single assignment form.[1] Sreedhar, Gao and Lee then reused it for this one: identifying every loop's exact body, nested ones included, in one bottom-up pass.[2] The dominator-tree reducibility test on the reducibility page is a piece of that second paper too. The rest of it, covered here: process a method's dominator tree from its deepest level up, and at every level, either collapse a loop that was just found into a single stand-in statement, or discover that no single statement can be called that level's loop header at all.

The DJ graph

A DJ graph is two kinds of edge sharing one graph: every dominator-tree edge (a "D-edge," unchanged from the dominator tree itself), plus every edge from the original flowgraph that is not a dominator-tree edge, reclassified exactly as reducibility and loop detection's third method already does: a back-join edge if its target dominates its source, a cross-join edge otherwise. Nothing new in the classification rule — what is new is doing it once, over the whole method, and keeping each statement's dominator-tree depth alongside it, so a bottom-up pass has an order to follow.

A small nested example — an inner loop (B, B1, B2) sitting inside an outer one (A through D):

flowchart TD
    Entry["Entry · d0"] --> A["A · d1"]
    A --> B["B · d2"]
    B --> B1["B1 · d3"]
    B1 --> B2["B2 · d4"]
    B2 -.->|back edge| B
    B2 --> C["C · d5"]
    C --> D["D · d6"]
    D -.->|back edge| A
    D --> Exit["Exit · d7"]

Every solid edge above is already a dominator-tree edge — this particular shape happens to be a straight chain of immediate dominators, same as the single-loop graph on the reducibility page, just longer. Classifying the two dashed edges the usual way: does B dominate B2? Yes — back-join. Does A dominate D? Yes — back-join. Both land inside the loop they belong to, not sideways into one; this graph has no cross-join edges at all, which is exactly what "fully reducible, nested two deep" looks like on a DJ graph.

Collapsing the nested loop, bottom-up

Walk the dominator tree's levels from deepest to shallowest. At each level, look for a back-join edge landing on a statement at that level; if one exists, that statement is a loop header, and its body is found exactly the way dominance and reachability's loop section already finds a single loop's body: walk backward from the back edge's source, stopping at the header. Collapse the whole body into the header — one statement standing in for all of them — and continue up a level, with that collapse already done.

Collapsing both loops, deepest level first (6 steps)
  1. Depths 7 down to 3 — Exit, D, C, B2, B1 — pass with nothing to do: no back-join edge lands on any of them. The first one that does is at depth 2.

    flowchart TD
        Entry["Entry · d0"] --> A["A · d1"]
        A --> B["B · d2"]
        B --> B1["B1 · d3"]
        B1 --> B2["B2 · d4"]
        B2 -.->|back edge| B
        B2 --> C["C · d5"]
        C --> D["D · d6"]
        D -.->|back edge| A
        D --> Exit["Exit · d7"]
    
  2. Depth 2 — B: the back-join edge B2 → B lands here. Walking backward from B2, stopping at B: B2, then its predecessor B1, then B1's predecessor B — already the stopping point. Loop body: {B, B1, B2}.

    flowchart TD
        Entry["Entry · d0"] --> A["A · d1"]
        A --> B["B · d2"]
        B --> B1["B1 · d3"]
        B1 --> B2["B2 · d4"]
        B2 -.->|back edge| B
        B2 --> C["C · d5"]
        C --> D["D · d6"]
        D -.->|back edge| A
        D --> Exit["Exit · d7"]
        class B,B1 member
        class B2 changed
        classDef member stroke:#4ade80,stroke-width:3px,fill:transparent
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
        linkStyle 4 stroke:#f5b041,stroke-width:3px
    

    Collapsing

    header B ← {B, B1, B2}
  3. Collapse: B1 and B2 disappear into B. The one edge that left the loop body, B2 → C, is redirected to start at B instead, keeping its original type. D's back edge to A is untouched — it never touched this loop at all.

    flowchart TD
        Entry["Entry · d0"] --> A["A · d1"]
        A --> B["B · d2 (= B, B1, B2)"]
        B --> C["C · d5"]
        C --> D["D · d6"]
        D -.->|back edge| A
        D --> Exit["Exit · d7"]
        class B changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    
  4. Depth 1 — A: the back-join edge D → A lands here. Walking backward from D, stopping at A: D, then C, then B — now standing in for the whole inner loop — then B's predecessor A, the stopping point. Loop body: {A, B, C, D}.

    flowchart TD
        Entry["Entry · d0"] --> A["A · d1"]
        A --> B["B · d2 (= B, B1, B2)"]
        B --> C["C · d5"]
        C --> D["D · d6"]
        D -.->|back edge| A
        D --> Exit["Exit · d7"]
        class A,B,C member
        class D changed
        classDef member stroke:#4ade80,stroke-width:3px,fill:transparent
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
        linkStyle 4 stroke:#f5b041,stroke-width:3px
    

    Collapsing

    header A ← {A, B, C, D}
  5. Collapse: B, C, D disappear into A. The one edge leaving the loop body, D → Exit, redirects to start at A.

    flowchart TD
        Entry["Entry · d0"] --> A["A · d1 (= A, B, C, D)"]
        A --> Exit["Exit · d7"]
        class A changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    
  6. Depth 0 — Entry: nothing left to find. Done.

    flowchart TD
        Entry["Entry · d0"] --> A["A · d1 (= A, B, C, D)"]
        A --> Exit["Exit · d7"]
    

    Loop bodies found

    B → {B, B1, B2}A → {A, B, C, D}

Two natural loops, correctly told apart, in one bottom-up pass — the inner one collapsed before the outer one's own back edge was even inspected, so the outer loop's walk never had to know B1 or B2 existed. The redirected edges keep their original type for exactly this reason: a back-join or cross-join edge that happens to skip past this loop's own header, straight to an outer ancestor, carries that fact up to whichever shallower level needs it next — the same one-level-at-a-time climb eliminating GOTOs uses for a jump nested several scopes deep.

When a level is irreducible instead

Not every level finds a back-join edge. Back on the two-entry cycle from the reducibility page — the smallest irreducible shape there is:

flowchart TD
    Entry --> A
    Entry --> B
    A -->|cross-join| B
    B -->|cross-join| A

A DFS over this DJ graph from Entry, visiting A before B, finds exactly one back edge: B → A, already classified cross-join. No back-join edge landed on this level at all — the signal to try the quicker check instead.

Finding the irreducible region at depth 1 (3 steps)
  1. The DFS back edge found, B → A, is cross-join. There is no back-join edge at this level — nothing qualifies for the backward-walk shortcut above.

    flowchart TD
        Entry --> A
        Entry --> B
        A -->|cross-join| B
        B -.->|"cross-join, back edge"| A
        class A,B member
        classDef member stroke:#4ade80,stroke-width:3px,fill:transparent
    
  2. Fall back to the quicker check: take every statement still at this dominator depth or deeper that is still in the graph — here, just {A, B} — and find its strongly connected components. One, with more than one member: {A, B} itself.

    flowchart TD
        Entry --> A
        Entry --> B
        A -->|cross-join| B
        B -.->|"cross-join, back edge"| A
        class A,B changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
        linkStyle 2 stroke:#f5b041,stroke-width:3px
        linkStyle 3 stroke:#f5b041,stroke-width:3px
    

    Collapsing

    header A ← {A, B}
  3. Collapsed the same way as a reducible loop — merge the whole component into one stand-in statement — just with no single header found by the walk itself. The one assigned here, A, is which statement the cross-join back edge happened to target, not a property of the region.

    flowchart TD
        Entry --> A["A (= A, B)"]
        class A changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    

Same collapse step either way — a reducible loop's body or an irreducible region's component both disappear into one statement before the level above ever sees them. What differs is how the body was found: a bounded backward walk from one edge for a reducible loop, a full strongly-connected-components pass for an irreducible region, because an irreducible region has no single back edge whose header could have been walked back from in the first place.

What goes wrong

Where this is used

The three edge types this page builds — dominator, back-join, cross-join — are not thrown away once reducibility and loop bodies are known. The transpiler's own tree-cloning step still switches on exactly these three classes when copying an edge into its output graph, alongside the same plain and annotated edge types eliminating GOTOs rewrites: the classification done here survives into code generation, not just into an analysis report.

References

  1. V. C. Sreedhar and G. R. Gao. "A Linear Time Algorithm for Placing φ-Nodes." Proceedings of the 22nd ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages (POPL '95), 1995, pp. 62–73. Introduces the DJ graph.
  2. V. C. Sreedhar, G. R. Gao, and Y.-F. Lee. "Identifying Loops Using DJ Graphs." ACM Transactions on Programming Languages and Systems, 18(6), November 1996, pp. 649–658. The bottom-up collapsing algorithm this page walks through.