Graph algorithms · is this flowgraph well-behaved

Reducibility and loop detection

Read first: Dominance and reachability, Visiting a graph, Strongly connected components

A WHILE loop has one head: control enters at the top and nowhere else. To print a tangle of GO TOs as one, a structuring tool must find, for each cycle, the one statement every path into it passes through. When a cycle can be entered at two statements there is none, and no nesting of IF and WHILE describes it without first rewriting the code.

On this page's three-node counter-example, where Entry branches straight into both A and B of a two-node cycle, three methods all say irreducible: collapsing gets stuck at three statements, the cycle {A, B} is entered at two places, and the dominator test finds a sideways edge closing a cycle. On a five-node one-entry loop, all three say reducible.

Terms are pinned down in Why one way in matters.

The example: one way in, or two

The counter-example, the smallest graph with a loop that has two ways in: a branch straight into both nodes of a two-node cycle, so neither node is reachable only through the other.

flowchart TD
    Entry --> A
    Entry --> B
    A --> B
    B --> A

Below it, a loop with one entry, reducible by inspection, used to confirm each method says yes. T is the method's entry; the dashed edge goes back to it.

flowchart TD
    T --> X1
    T --> L1
    X1 --> X2
    L1 --> L2
    L2 -.->|back edge| T

The answer, for both graphs, from each of the three methods on this page. Every cell is worked out in the section named in its row:

methodtwo-entry cycleone-entry loop
T1/T2 collapsingstuck at 3 statements: irreducibledown to 1 statement: reducible
strongly connected components{A, B}, entered at A and at B: irreducible{T, L1, L2}, one way in, T: reducible
dominancea cross-join edge is a DFS back edge: irreducibleits one DFS back edge, L2 → T, is a back-join: reducible

One way COBOL ends up with the counter-example's shape, in a fragment constructed for this page. Take the paragraphs as the nodes: ENTRY-PARA is Entry, A-PARA is A, B-PARA is B.

ENTRY-PARA.
    IF WS-RESTART = 'Y'
        GO TO B-PARA.
A-PARA.
    ADD 1 TO WS-COUNT.
B-PARA.
    IF WS-COUNT < 10
        GO TO A-PARA.

Falling through gives Entry → A and A → B; GO TO A-PARA gives B → A. The loop A-PARA…B-PARA would have one way in, at A, but GO TO B-PARA jumps into its middle: that is Entry → B. Leave out B-PARA's fall-through out of the loop and these are exactly the four edges of the graph above.

Collapsing the one-entry loop

The first method, due to Hecht and Ullman,[1] shrinks the graph with two rules, repeated until nothing shrinks any further: T1 deletes an edge from a statement to itself; T2 folds a statement with exactly one predecessor into that predecessor. In the walk, diagrams mark the statement a step just folded into, or the self-loop it just removed, in amber.

Collapsing the one-entry loop (5 steps, 3 rounds)
  1. Starting shape: five statements.

    flowchart TD
        T --> X1
        T --> L1
        X1 --> X2
        L1 --> L2
        L2 -.->|back edge| T
    
  2. Round 1 — X2 has exactly one predecessor, X1. Fold it in: X1 inherits X2's outgoing edges (none), X2 disappears.

    flowchart TD
        T --> X1
        T --> L1
        L1 --> L2
        L2 -.->|back edge| T
        class X1 changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    
  3. Round 1 — X1 now has exactly one predecessor, T. Fold it in: T inherits nothing new, X1 disappears.

    flowchart TD
        T --> L1
        L1 --> L2
        L2 -.->|back edge| T
        class T changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    
  4. Round 1 — L2 has exactly one predecessor, L1. Fold it in: L1 inherits L2's edge to T, L2 disappears.

    flowchart TD
        T --> L1
        L1 -.->|back edge| T
        class L1 changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    
  5. Round 1 — L1 now has exactly one predecessor, T. Fold it in: T inherits L1's edge to T — which is an edge from T to itself. L1 disappears.

    flowchart TD
        T -.->|self-loop| T
        class T changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    
  6. Round 2 — T1 fires on the self-loop just created: remove the edge. One statement left, no edges. Round 3 confirms: nothing left to fold, nothing left to remove. Done.

    flowchart TD
        T["T (no edges)"]
        class T changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    

Five statements down to one, in the last diagram of the walk. Reducible.

Collapsing the two-entry cycle

Trying to collapse the two-entry cycle (2 steps, 1 round, nothing moves)
  1. Starting shape: three statements.

    flowchart TD
        Entry --> A
        Entry --> B
        A --> B
        B --> A
    
  2. T1: no statement has an edge to itself. Nothing to do.

    flowchart TD
        Entry --> A
        Entry --> B
        A --> B
        B --> A
    

    (unchanged)

  3. T2: A's predecessors are {Entry, B} — two, not one. B's predecessors are {Entry, A} — also two. Neither qualifies. Entry has no predecessor at all, and is the root besides. Nothing folds.

    flowchart TD
        Entry --> A
        Entry --> B
        A --> B
        B --> A
        class A,B stuck
        classDef stuck stroke:#f87171,stroke-width:3px,fill:transparent
    

    (round produced no change — stuck)

Stuck at three statements on the very first round: the red-outlined A and B in step 2 each have two predecessors. Irreducible — and this method says so without having to point at which two nodes are the problem, only that something, somewhere, is.

The collapsing rules: T1 and T2

The two rules the walks above applied, in full:

T1: a statement with an edge to itself — remove the edge.
T2: a statement with exactly one predecessor — fold it into that predecessor
  (the predecessor inherits its outgoing edges; the statement disappears).
Repeat T1 then T2, over every statement, until a full round changes nothing.

In the one-entry walk, T2 is steps 1 to 4, each folding one statement, and T1 is step 5, removing the self-loop step 4 made on T. In the two-entry walk, step 1 is T1 and step 2 is T2, and neither changes anything. If the whole graph has shrunk to one statement, as in the first walk, every loop in it had exactly one entry: reducible. If it gets stuck above one statement, as in the second, some region had more than one way in: irreducible.

A quicker check for where: strongly connected components

A cheaper question, answerable without folding anything: find every maximal set of statements that can all reach each other — a strongly connected component (its own page shows how to find them) — and look at how it is entered from outside. A loop with one entry has every outside edge landing on the same statement. A loop with two or more has outside edges landing on different statements inside the same component. On the two-entry cycle:

Checking the two-entry cycle's one strongly connected component (1 step)
  1. A and B reach each other (A → B → A); Entry reaches both but neither reaches Entry. One strongly connected component: {A, B}.

    flowchart TD
        Entry --> A
        Entry --> B
        A --> B
        B --> A
        class A,B changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    
  2. Edges entering {A, B} from outside: Entry → A and Entry → B — two edges, landing on two different statements inside the component.

    flowchart TD
        Entry --> A
        Entry --> B
        A --> B
        B --> A
        class A,B changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
        linkStyle 0 stroke:#f5b041,stroke-width:3px
        linkStyle 1 stroke:#f5b041,stroke-width:3px
    

    Entries into {A, B}

    Entry → AEntry → B

Two distinct entry points into one component, the two amber edges in step 1: irreducible, by inspection, with no folding at all. Checked the same way on the good loop, below, the component is {T, L1, L2} (amber) and no edge enters it from outside at all — T is the method's entry, and X1 and X2 are only reached from it — so its one way in is T: reducible.

flowchart TD
    T --> X1
    T --> L1
    X1 --> X2
    L1 --> L2
    L2 -.->|back edge| T
    class T,L1,L2 changed
    classDef changed fill:#f5b041,stroke:#f5b041,color:#000
SCC {T, L1, L2}edges in from outside: none

A third way: reusing dominance

A third method, from Sreedhar, Gao and Lee's paper on identifying loops with DJ graphs,[2] answers the same question using the machinery dominance and reachability already built, rather than folding the graph or hunting components. Compute dominators for the two-entry cycle:

statementdominated by
Entry{Entry}
A{Entry, A}
B{Entry, B}

The dominator tree is just Entry → A and Entry → B — two children, nothing nested. Every edge in the original graph that is not a dominator-tree edge gets classified by one question: does the edge's target dominate its source?

extra edgedoes target dominate source?classified as
A → Bdoes B dominate A? dom(A) = {Entry, A} — nocross-join
B → Adoes A dominate B? dom(B) = {Entry, B} — nocross-join

Both rows of that table apply the second line of this rule:

target dominates source → back-join: a back edge in the dominator sense, climbing to a real header
target does not dominate source → cross-join: reaching sideways, into a region
  this edge's source was never shown to be inside
  (harmless — every if/else merge makes one — unless it is also a DFS back edge)

The dominator-tree edges plus this reclassified set, together, are called a DJ graph — the "D" for dominator, the "J" for the join edges just classified. Here it is for the two-entry cycle: solid edges are the dominator tree, dashed ones the two cross-joins from the table.

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

A graph is reducible exactly when none of its back edges — found by a fresh depth-first walk over this reclassified graph, not the original — turn out to be cross-join edges. Walking from Entry and visiting A first (Entry → A → B, the amber path below), B → A is found going to a statement still on the current path: a back edge, drawn in red.

flowchart TD
    Entry --> A
    Entry --> B
    A -.->|cross-join| B
    B -.->|"cross-join, DFS back edge"| A
    linkStyle 0 stroke:#f5b041,stroke-width:3px
    linkStyle 2 stroke:#f5b041,stroke-width:3px
    linkStyle 3 stroke:#f87171,stroke-width:3px

(Visit B first and A → B is the one found — the same verdict either way.) It was classified cross-join in the table above. One cross-join back edge is enough: irreducible, the same answer as the other two methods, from a different question — "does this edge ever reach into a region sideways, rather than climbing to something that was shown to dominate it" — asked once per edge instead of once per fold or once per component.

The same test on the good loop. Its dominator tree is T → X1 → X2 and T → L1 → L2, which is every edge but one; that one extra edge is a back-join:

statementdominated by
T{T}
X1{T, X1}
X2{T, X1, X2}
L1{T, L1}
L2{T, L1, L2}
extra edgedoes target dominate source?classified asDFS back edge?
L2 → Tdoes T dominate L2? dom(L2) = {T, L1, L2} — yesback-joinyes: T is on the path T → L1 → L2

That last row is where the two senses of back edge meet. In the DFS sense, a back edge is one a depth-first walk finds going to a statement still on its current path; in the dominator sense, it is one whose target dominates its source — a back-join edge. L2 → T is both. On a reducible graph the two pick out exactly the same edges; on the two-entry cycle, B → A is a DFS back edge but not a back-join.

A cross-join edge that is not a DFS back edge closes no cycle and says nothing about reducibility. Either arm of an if/else landing on the statement where they merge is one. Below, M's dominators are {C, M}, so neither edge into M is a dominator-tree edge; and M dominates neither Then (dominated by {C, Then}) nor Else ({C, Else}), so both are cross-joins. A depth-first walk C → Then → M finishes M before it visits Else, so Else → M goes to a finished statement, not one on the path: no back edges at all.

flowchart TD
    C --> Then
    C --> Else
    Then -.->|cross-join| M
    Else -.->|cross-join| M
cross-joins: Then → M, Else → MDFS back edges: none

This page only asks the DJ graph yes-or-no questions; DJ graphs and natural loop bodies uses the same graph to find the exact statement set of every loop's body, nested ones included.

Why one way in matters

A loop with one entry — one statement execution always passes through to get into it — is a natural loop: its header dominates every node in its body, meaning every path into the loop must pass through that one entry point. The two dominator tables above show the difference. Intersect the dominator sets of each loop's statements:

loopits statementsdominates them allinside the loop?
one-entry loopT, L1, L2{T}yes: T is the header
two-entry cycleA, B{Entry}no: no header

This is the kind of loop dominance and reachability already finds the body of. In the second row, nothing inside the cycle dominates both A and B; only Entry, outside it, does. A flowgraph built entirely out of natural loops, nested or side by side, is reducible. One with a multi-entry loop anywhere inside it is irreducible.

Reducibility matters because most of what a structuring tool wants to say — "this is a loop, here is its body, here is what comes after it" — assumes a single entry, the T in the first row. The second row has none to name, so that cycle cannot be described this way without first rewriting it, a much bigger job than detecting it.

What goes wrong

Where this shows up in reverse engineering

Where this is used

Before translating a flowgraph with arbitrary jumps into structured code, it has to be known which parts, if any, do not reduce — those are the parts a straight translation into IF/WHILE cannot express without first restructuring them. The three methods above answer "is it reducible at all" and "roughly where" cheaply, as the example's answer table shows. Turning that into the exact statement set of every loop body, including loops nested inside other loops, is the DJ graph's other job — see DJ graphs and natural loop bodies.

References

  1. M. S. Hecht and J. D. Ullman. "Flow Graph Reducibility." SIAM Journal on Computing, 1(2), 1972, pp. 188–202. The T1/T2 collapsing rules.
  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 dominator/back-join/cross-join reducibility test; see also DJ graphs and natural loop bodies.