Reaching conditions
Read first: Visiting a graph
Whether a statement runs is decided by every IF on every path into it: along a path all its guards must hold, where paths merge any one will do, and a branch that never reaches the statement must add nothing. Reachability only says yes or no, and reading guards off by eye across GO TOs misses one or counts a branch that goes elsewhere.
In this page's example, from cobol-rekt's README, an IF WS-NUM1 > 10 jumps to an IF WS-NUM1 = 201 that guards a DISPLAY. The technique slices away the two STOP RUN arms, walks the four statements left in topological order, and gives the DISPLAY the condition AND(C2, C1): WS-NUM1 = 201 and WS-NUM1 > 10. It works on acyclic slices only.
Topological order is explained in Visiting a graph.
The example: a DISPLAY behind two IFs
The page's model of the README program: start A, the IFs as B and C, the DISPLAY as D, a STOP RUN on each arm not taken. The conditions are written C1 and C2:
flowchart TD
A -->|always| B["B: IF C1"]
B -->|C1| C["C: IF C2"]
B -->|"not C1"| Stop1["STOP RUN"]
C -->|C2| D["D: DISPLAY"]
C -->|"not C2"| Stop2["STOP RUN"]
Neither STOP RUN in the graph above leads to D, so the first step throws both away. What is left is the slice from A to D: only the statements and edges on some path from the start to the target. Four statements, three edges:
flowchart TD
A -->|always| B
B -->|"then: C1"| C
C -->|"then: C2"| D
The answer for the slice above, one condition per statement, is what the walk below builds. D's is the one the question was about:
| statement | condition |
|---|---|
| A | FALSE (an implementation quirk; see step 1) |
| B | TRUE |
| C | C1 |
| D | AND(C2, C1) |
The lines of the README program that the graph models. GO TO SA2 is the then-edge from B to C; the model leaves out the paragraph labels:
SA1.
IF WS-NUM1 > 10
THEN
GO TO SA2.
STOP RUN.
SA2.
IF WS-NUM1 = 201
THEN
DISPLAY "IT IS DONE".
STOP RUN.
cobol-rekt's README gives this as the reaching condition it prints for the DISPLAY, reformatted here onto two lines. It is AND(C2, C1), with C2 first, as in the table:
and(eq(ref('WS-NUM1'), primitive(201.0)),
gt(ref('WS-NUM1'), primitive(10.0)))
Full listing (the README's STOPRUN program)
IDENTIFICATION DIVISION.
PROGRAM-ID. STOPRUN.
AUTHOR. MOJO
DATE-WRITTEN. SEP 2024.
ENVIRONMENT DIVISION.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-NUM1 PIC 9(9) VALUE 5.
PROCEDURE DIVISION.
S SECTION.
SA1.
IF WS-NUM1 > 10
THEN
GO TO SA2.
STOP RUN.
SA2.
IF WS-NUM1 = 201
THEN
DISPLAY "IT IS DONE".
STOP RUN.
Step by step
The walk visits the slice in topological order, so every statement comes after everything that leads into it, and its predecessors' conditions are already known when it needs them. Each step applies this rule once:
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)
Nothing computed yet.
flowchart TD A -->|always| B B -->|"then: C1"| C C -->|"then: C2"| DAhas 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:#000condition(A)
FALSEFlag:
FALSEis what the real implementation gives here — OR over no incoming edges. The correct value isTRUE: the start is always reached. See What goes wrong.B's only way in: the always-taken edge fromA. Combine that edge's own condition (always true) withA'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:#000condition(B)
TRUEFlag: literally this is
AND(TRUE, FALSE), which in correct boolean logic isFALSE. The real implementation returnsTRUEbecause of a second quirk in itsAND.TRUEis the right answer forB— it is whatAND(TRUE, TRUE)gives withA's correct condition — but only because the two mistakes cancel. See What goes wrong.C's only way in: the then-edge fromB, carryingC1. Combined withB's (true) condition,C1survives unchanged.flowchart TD A -->|always| B B -->|"then: C1"| C C -->|"then: C2"| D class C changed classDef changed fill:#f5b041,stroke:#f5b041,color:#000condition(C)
C1D's only way in: the then-edge fromC, carryingC2. Combined withC'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:#000condition(D)
AND(C2, C1)
Step 4 is the rule box in full: one incoming edge, C → D, whose own condition is C2, ANDed with condition(C), which step 3 left as C1. Each statement in this slice has one way in, so the OR line never has more than one operand here. Step 1 is the OR over no edges at all. AND(C2, C1) means D is reached exactly when both guards hold, and it matches the README output above.
Why each edge carries a condition
Reachability on the first graph says only that D can be reached. To get a condition, every edge is tagged with its own. Leaving an ordinary statement, as at A, control goes the one way it can: always taken, condition TRUE. Leaving an IF, the then-edge carries the IF's condition and the else-edge carries its negation. For the five edges of the first graph:
| edge | kind | its own condition | in the slice? |
|---|---|---|---|
| A → B | always | TRUE | yes |
| B → C | then | C1 | yes |
| B → STOP RUN | else | NOT C1 | no |
| C → D | then | C2 | yes |
| C → STOP RUN | else | NOT C2 | no |
The rule box then reads these off the three rows marked "yes": steps 2, 3 and 4 each took one row's condition and ANDed it with the predecessor's. The two "no" rows would only matter if they led to D; the next example changes the shape so that an else-edge does.
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 (4 steps)
Nothing computed yet.
flowchart TD A -->|always| B B -->|C1| C B -->|"not C1"| X C -->|always| D X -->|always| DA: 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:#000condition(A)
FALSEFlag: as before, the real implementation's value; the correct one is
TRUE.B: always-taken fromA. 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:#000condition(B)
TRUEFlag: as before, the implementation's
AND(TRUE, FALSE)returnsTRUE— wrong boolean logic, right answer, because it cancels the wrongFALSEatA.C: then-edge fromB, carryingC1.X: else-edge fromB, carryingNOT C1.flowchart TD A -->|always| B B -->|C1| C B -->|"not C1"| X C -->|always| D X -->|always| D class C,X changed classDef changed fill:#f5b041,stroke:#f5b041,color:#000condition(C)
C1condition(X)
NOT C1D: two ways in, both always-taken, fromCand fromX. 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:3pxcondition(D)
OR(C1, NOT C1)
Step 4's OR(C1, NOT C1) says reach D when C1 holds, or when it does not: every possible value. The graph above shows the same by inspection, since both of B's arms lead to D. The walk's answer, though, is the literal expression, not the word "always"; the third bullet below shows why.
What goes wrong
Each bullet comes with the small case that shows it. The first three are taken from the two walks above.
- The start statement's own condition reads
FALSE, notTRUE. It genuinely does, in the real implementation. A statement with no incoming edge in the whole flowgraph (in practice the program's first statement, the start both walks use) gets the OR over nothing, which the implementation starts fromFALSE. Step 1 of both walks shows it:The start is reached unconditionally, sostatement incoming edges implementation correct A none FALSE TRUE TRUEis what a person would expect. It never surfaces as a wrong final answer forDin either walk, but only because of the next point. AND(lhs, rhs)returnslhsif either side isFALSE, which is correct only whenlhsis theFALSEone. Also real. In the implementation'sAND, abridged,lhsis the edge's own condition andrhsthe predecessor's:if (isFalse(lhs) || isFalse(rhs)) return lhs; else if (isTrue(lhs)) return rhs; else if (isTrue(rhs)) return lhs; return AND(lhs, rhs);
The three ANDs of the first walk, run through those lines:In step 2,step lhs (edge) rhs (predecessor) returns boolean AND 2, at B TRUE FALSE TRUE FALSE 3, at C C1 TRUE C1 C1 4, at D C2 C1 AND(C2, C1) AND(C2, C1) rhsis the false side:A's wrongly-false condition. The result comes back as the (non-false)lhs, which happens to be the right answer for an always-taken edge out of the start. The two quirks cancel out for every statement directly off the start; nothing here checks that they always will.OR(C1, NOT C1)is never recognised asTRUE. It is a tautology, true for every valueC1could hold:But the rule only simplifies an operand that is already the literal constant true or false, not one that is logically guaranteed to be, so step 4 of the merge walk reports the unsimplified expression. This is a known, stated limitation of the real implementation, not an oversight found by reading between the lines of the code.C1 NOT C1 OR(C1, NOT C1) TRUE FALSE TRUE FALSE TRUE TRUE - None of this runs once the slice has a cycle. The rule box needs every predecessor's condition known before a statement needs it, which only a topological order guarantees. Give the slice a loop, as below, and there is none:
Bhas to come beforeCbecause ofB → C, and after it because ofC → B. Building the order throws an exception.flowchart TD A -->|always| B B -->|always| C["C: IF C1"] C -->|"then: C1"| B C -->|"else: NOT C1"| D linkStyle 1 stroke:#f5b041,stroke-width:3px linkStyle 2 stroke:#f5b041,stroke-width:3px - Predecessors outside the slice count as
TRUE. Even for an acyclic slice, the condition pass reads each statement's incoming edges from the whole flowgraph, not the slice. A predecessor outside the slice has no computed condition and silently defaults toTRUE, understating what is actually required rather than raising an error. Below,Palso leads intoDbut is not reached fromA, so the slice fromAtoDisA,B,D:flowchart TD A -->|always| B["B: IF C1"] B -->|"then: C1"| D B -->|"else: NOT C1"| Stop["STOP RUN"] P["P: not reached from A"] -->|always| D linkStyle 3 stroke:#f5b041,stroke-width:3px,stroke-dasharray:4Worked by the rule and the two quirks above, not run:Fromincoming edge of D AND(edge, predecessor) gives B → D AND(C1, TRUE) C1 P → D AND(TRUE, TRUE: P's default) TRUE OR of both OR(C1, TRUE) TRUE A,Druns only whenC1holds, but the reported condition isTRUE: the one edge fromPwipes out the guard.
Where this shows up in reverse engineering
- Triaging a crash report or a log line that points at one statement, like the
DISPLAYin the listing: the answer becomesWS-NUM1 = 201andWS-NUM1 > 10, checkable against the input that triggered it. - Security review of legacy code: a dangerous statement in the source may be reachable under no condition a real caller can satisfy;
D's row in the table at the top is what to check. - The control-side counterpart to a backward data slice: the data version asks "what value reached this read"; this asks the same backward question about control, with the same slice-then-combine shape.
Where this is used
The slice does most of the narrowing: in the example both STOP RUNs were gone before any condition was built.
The same idea, under the same name, drives control-flow structuring in decompilers: Yakdan et al. compute reaching conditions over acyclic regions to turn a recovered flowgraph back into if/else without pattern matching.[1]
References
- K. Yakdan, S. Eschweiler, E. Gerhards-Padilla, and M. Smith. "No More Gotos: Decompilation Using Pattern-Independent Control-Flow Structuring and Semantics-Preserving Transformations." Network and Distributed System Security Symposium (NDSS 2015), 2015. Reaching conditions over acyclic regions as the basis for pattern-independent structuring.