Graph algorithms · how did I get here

Reaching conditions

Not "can execution reach this statement" but "what has to be true about the data for it to." Built up one statement at a time, with the two spots where the real implementation's shortcuts show.

Where this shows up in reverse engineering

Reachability answers "can execution get here at all." This page answers the sharper question underneath almost every real investigation into an unfamiliar program: under what condition on the data does it.

The problem

Reachability answers yes or no: can execution get from here to there at all. A sharper question, for a statement guarded by one or more IFs: not just that it can be reached, but under what condition on the data — spelled out as a boolean expression over the program's own fields, true exactly when some path to the statement is the one actually taken.

Two kinds of edge matter to this question. Leaving an ordinary statement, control always goes the one way it can — call that edge always taken. Leaving an IF, there are two edges: one taken exactly when the condition holds, one taken exactly when it does not. Tag each edge with which of the three it is, and the reaching condition of any statement becomes: for each way in, the edge's own condition combined with however that predecessor was reached — and if there is more than one way in, any one of them being true is enough.

First, slice away the dead ends

A real flowgraph has branches that never lead to the statement being asked about at all — error handlers, early STOP RUNs, unrelated sections. None of that is needed to answer the question, so the first step throws it away: keep only the statements and edges that lie on some path from the start to the target, nothing else. What is left is called a slice, and it comes with the start-to-target paths already topologically ordered — every statement after everything that can lead into it, the same order visiting a graph builds for exactly this reason.

Take a start statement A, two nested IFs, and a DISPLAY guarded by both — reusing the shape of a real example from this technique's own source project, with the field conditions written as C1 and C2:

flowchart TD
    A -->|always| B["B: IF C1"]
    B -->|C1| C
    B -->|"not C1"| Stop1["STOP RUN"]
    C -->|always| B2["B2: IF C2"]
    B2 -->|C2| D["D: DISPLAY"]
    B2 -->|"not C2"| Stop2["STOP RUN"]

Both STOP RUN arms never reach D. Sliced from A to D, they disappear entirely:

flowchart TD
    A -->|always| B
    B -->|"then: C1"| C
    C -->|"then: C2"| D

Four statements, three edges, nothing left that doesn't lead somewhere useful.

Building the condition, one statement at a time

Walking the slice in topological order, each statement's condition is built from its predecessors', already known by the time it is needed:

condition(v) = OR, over every incoming edge (u → v):
  AND(that edge's own condition, condition(u))

Diagrams below mark the statement whose condition a step is computing in amber.

Computing the condition at each of A, B, C, D (4 steps)
  1. Nothing computed yet.

    flowchart TD
        A -->|always| B
        B -->|"then: C1"| C
        C -->|"then: C2"| D
    
  2. A has no incoming edge in the slice at all — it is the start. With nothing to combine, its condition is the empty case.

    flowchart TD
        A -->|always| B
        B -->|"then: C1"| C
        C -->|"then: C2"| D
        class A changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    

    condition(A)

    FALSE
  3. B's only way in: the always-taken edge from A. Combine that edge's own condition (always true) with A's condition.

    flowchart TD
        A -->|always| B
        B -->|"then: C1"| C
        C -->|"then: C2"| D
        class B changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    

    condition(B)

    TRUE
  4. C's only way in: the then-edge from B, carrying C1. Combined with B's (true) condition, C1 survives unchanged.

    flowchart TD
        A -->|always| B
        B -->|"then: C1"| C
        C -->|"then: C2"| D
        class C changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    

    condition(C)

    C1
  5. D's only way in: the then-edge from C, carrying C2. Combined with C's condition, C1: both now have to hold.

    flowchart TD
        A -->|always| B
        B -->|"then: C1"| C
        C -->|"then: C2"| D
        class D changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    

    condition(D)

    AND(C2, C1)

AND(C2, C1) — reach D exactly when both guards hold. That is the literal answer this technique's own source project documents for the real COBOL this shape is drawn from.

A branch that merges back

One statement can have more than one way in. Change the shape so both of B's arms lead to D instead of one of them being a dead end:

flowchart TD
    A -->|always| B["B: IF C1"]
    B -->|C1| C
    B -->|"not C1"| X
    C -->|always| D
    X -->|always| D
Computing the condition with two ways into D (5 steps)
  1. Nothing computed yet.

    flowchart TD
        A -->|always| B
        B -->|C1| C
        B -->|"not C1"| X
        C -->|always| D
        X -->|always| D
    
  2. A: no incoming edge. Same empty case as before.

    flowchart TD
        A -->|always| B
        B -->|C1| C
        B -->|"not C1"| X
        C -->|always| D
        X -->|always| D
        class A changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    

    condition(A)

    FALSE
  3. B: always-taken from A. Same as before.

    flowchart TD
        A -->|always| B
        B -->|C1| C
        B -->|"not C1"| X
        C -->|always| D
        X -->|always| D
        class B changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    

    condition(B)

    TRUE
  4. C: then-edge from B, carrying C1. X: else-edge from B, carrying NOT C1.

    flowchart TD
        A -->|always| B
        B -->|C1| C
        B -->|"not C1"| X
        C -->|always| D
        X -->|always| D
        class C changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
    

    condition(C)

    C1

    condition(X)

    NOT C1
  5. D: two ways in, both always-taken, from C and from X. Combine each edge with its source's condition, then take either being enough.

    flowchart TD
        A -->|always| B
        B -->|C1| C
        B -->|"not C1"| X
        C -->|always| D
        X -->|always| D
        class D changed
        classDef changed fill:#f5b041,stroke:#f5b041,color:#000
        linkStyle 3 stroke:#f5b041,stroke-width:3px
        linkStyle 4 stroke:#f5b041,stroke-width:3px
    

    condition(D)

    OR(C1, NOT C1)

Reach D when C1 holds, or when it does not — which is every possible value C1 could have. D is unconditionally reached once B is, which is exactly what the diagram shows by inspection: both of B's only two arms lead to D. The answer the walk produces, though, is the literal expression OR(C1, NOT C1), not the word "always" — see below.

What goes wrong

Where this is used

Answering "what has to be true for this line to run at all" for a specific statement someone has pointed at — a crash report, a line a debugger stopped on, a DISPLAY whose output someone is trying to explain. The slice step already does most of the narrowing: everything not on some path to the statement in question is gone before the condition work even starts, so the condition itself only ever has to describe the guards that are actually relevant.