Skip to content

Commit 6af65e0

Browse files
committed
feat(dataflow): stage 2 — post-dominators and control dependence
Cooper–Harper–Kennedy iterative post-dominators over the reverse CFG (unique root EXIT, guaranteed by stage 1's synthetic escape edges) and Ferrante–Ottenstein–Warren control dependence with ENTRY as the region root. Gate tests pin exact hand-computed CDG sets for the fixture's if/loop/early-return functions. (#67)
1 parent 2c08649 commit 6af65e0

2 files changed

Lines changed: 244 additions & 0 deletions

File tree

codeanalyzer/dataflow/dominance.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
################################################################################
2+
# Copyright IBM Corporation 2025
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
################################################################################
16+
17+
"""Stage 2 of the level-3 dataflow ladder: dominance and control dependence.
18+
19+
Post-dominators are computed with the Cooper–Harper–Kennedy iterative
20+
algorithm over the reverse CFG. Infinite loops are already normalized by the
21+
CFG builder (synthetic escape edge to EXIT), so the post-dominator tree always
22+
has the unique root EXIT.
23+
24+
Control dependence follows Ferrante–Ottenstein–Warren: for each CFG edge
25+
``(a, b)`` where ``b`` does not post-dominate ``a``, every node on the
26+
post-dominator-tree path from ``b`` up to (but not including) ``a``'s
27+
immediate post-dominator is control-dependent on ``a``.
28+
29+
Nodes with no branch-node control dependence are control-dependent on ENTRY —
30+
the conventional region root, which keeps every statement anchored in the PDG
31+
and gives interprocedural traversals a path from a callee's ENTRY to its
32+
unconditional statements.
33+
"""
34+
35+
from __future__ import annotations
36+
37+
from typing import Dict, List, Set, Tuple
38+
39+
from codeanalyzer.dataflow.cfg import ControlFlowGraph
40+
41+
42+
def _postorder(adj: Dict[int, List[int]], root: int) -> List[int]:
43+
"""Iterative DFS postorder over ``adj`` from ``root``."""
44+
order: List[int] = []
45+
visited: Set[int] = set()
46+
stack: List[Tuple[int, int]] = [(root, 0)]
47+
visited.add(root)
48+
while stack:
49+
node, i = stack.pop()
50+
children = adj.get(node, [])
51+
if i < len(children):
52+
stack.append((node, i + 1))
53+
child = children[i]
54+
if child not in visited:
55+
visited.add(child)
56+
stack.append((child, 0))
57+
else:
58+
order.append(node)
59+
return order
60+
61+
62+
def post_dominators(cfg: ControlFlowGraph) -> Dict[int, int]:
63+
"""Immediate post-dominator of every node, as ``{node: ipdom}``.
64+
65+
EXIT is its own post-dominator (the tree root). Cooper–Harper–Kennedy
66+
("A Simple, Fast Dominance Algorithm") run on the reverse CFG.
67+
"""
68+
# Reverse CFG: successors of n are the CFG predecessors of n.
69+
radj: Dict[int, List[int]] = {n.id: [] for n in cfg.nodes}
70+
rpred: Dict[int, List[int]] = {n.id: [] for n in cfg.nodes}
71+
for e in cfg.edges:
72+
if e.source == e.target:
73+
continue # self-loops carry no dominance information
74+
radj[e.target].append(e.source)
75+
rpred[e.source].append(e.target)
76+
77+
root = cfg.exit_id
78+
post = _postorder(radj, root)
79+
number = {n: i for i, n in enumerate(post)} # postorder number
80+
rpo = list(reversed(post)) # reverse postorder: root first
81+
82+
ipdom: Dict[int, int] = {root: root}
83+
84+
def intersect(a: int, b: int) -> int:
85+
while a != b:
86+
while number[a] < number[b]:
87+
a = ipdom[a]
88+
while number[b] < number[a]:
89+
b = ipdom[b]
90+
return a
91+
92+
changed = True
93+
while changed:
94+
changed = False
95+
for node in rpo:
96+
if node == root:
97+
continue
98+
preds = [p for p in rpred[node] if p in ipdom]
99+
if not preds:
100+
continue
101+
new = preds[0]
102+
for p in preds[1:]:
103+
new = intersect(new, p)
104+
if ipdom.get(node) != new:
105+
ipdom[node] = new
106+
changed = True
107+
108+
return ipdom
109+
110+
111+
def control_dependence(cfg: ControlFlowGraph) -> List[Tuple[int, int]]:
112+
"""CDG edges ``(branch_node, dependent_node)`` per Ferrante–Ottenstein–
113+
Warren, plus ENTRY-region edges for nodes with no other controller."""
114+
ipdom = post_dominators(cfg)
115+
116+
deps: Set[Tuple[int, int]] = set()
117+
for e in cfg.edges:
118+
a, b = e.source, e.target
119+
if a == b:
120+
continue
121+
# b post-dominates a iff b is an ancestor of a in the pdom tree.
122+
runner = b
123+
stop = ipdom.get(a)
124+
# Walk from b up the post-dominator tree to (not including) ipdom(a).
125+
while runner != stop and runner != a:
126+
deps.add((a, runner))
127+
nxt = ipdom.get(runner)
128+
if nxt is None or nxt == runner:
129+
break
130+
runner = nxt
131+
132+
# ENTRY as the region root for otherwise-uncontrolled nodes.
133+
controlled = {t for (_, t) in deps}
134+
for n in cfg.nodes:
135+
if n.id in (cfg.entry_id, cfg.exit_id):
136+
continue
137+
if n.id not in controlled:
138+
deps.add((cfg.entry_id, n.id))
139+
140+
return sorted(deps)

test/test_dataflow_dominance.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""Stage-2 gate: post-dominators and control dependence.
2+
3+
Contract assertions:
4+
- the post-dominator tree is a tree with unique root EXIT (infinite loops
5+
included, thanks to the CFG's synthetic escape edge);
6+
- hand-computed control dependences for the fixture's if / loop /
7+
early-return functions match exactly.
8+
"""
9+
10+
import ast
11+
from pathlib import Path
12+
13+
from codeanalyzer.dataflow.cfg import ControlFlowGraph, build_cfg
14+
from codeanalyzer.dataflow.dominance import control_dependence, post_dominators
15+
16+
FIXTURE = Path(__file__).parent / "fixtures" / "single_functionalities" / "dataflow"
17+
18+
19+
def _cfg_of(file_name: str, func_name: str) -> ControlFlowGraph:
20+
tree = ast.parse((FIXTURE / file_name).read_text())
21+
for node in ast.walk(tree):
22+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == func_name:
23+
return build_cfg(node)
24+
raise AssertionError(f"{func_name} not found")
25+
26+
27+
def _all_fixture_cfgs():
28+
cfgs = {}
29+
for file_name in ("main.py", "pipeline.py", "state.py"):
30+
tree = ast.parse((FIXTURE / file_name).read_text())
31+
for node in ast.walk(tree):
32+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
33+
cfgs[f"{file_name}::{node.name}"] = build_cfg(node)
34+
return cfgs
35+
36+
37+
def _by_line(cfg: ControlFlowGraph):
38+
"""id ↔ line helpers for hand-computed expectations."""
39+
return {n.start_line: n.id for n in cfg.nodes if n.kind not in ("entry", "exit")}
40+
41+
42+
def test_post_dominator_tree_is_rooted_at_exit_for_every_function():
43+
for name, cfg in _all_fixture_cfgs().items():
44+
ipdom = post_dominators(cfg)
45+
ids = {n.id for n in cfg.nodes}
46+
assert set(ipdom) == ids, f"{name}: some node has no post-dominator"
47+
assert ipdom[cfg.exit_id] == cfg.exit_id, name
48+
# Tree: walking up from any node terminates at EXIT without cycles.
49+
for n in ids:
50+
seen = set()
51+
cur = n
52+
while cur != cfg.exit_id:
53+
assert cur not in seen, f"{name}: ipdom cycle at {cur}"
54+
seen.add(cur)
55+
cur = ipdom[cur]
56+
57+
58+
def test_branchy_control_dependence_exact():
59+
cfg = _cfg_of("main.py", "branchy")
60+
line = _by_line(cfg)
61+
header, then_s, else_s, ret = line[12], line[13], line[15], line[16]
62+
expected = {
63+
(cfg.entry_id, header),
64+
(cfg.entry_id, ret),
65+
(header, then_s),
66+
(header, else_s),
67+
}
68+
assert set(control_dependence(cfg)) == expected
69+
70+
71+
def test_looped_control_dependence_exact():
72+
cfg = _cfg_of("main.py", "looped")
73+
line = _by_line(cfg)
74+
s_total, s_i, header, s_add, s_inc, ret = (
75+
line[20], line[21], line[22], line[23], line[24], line[25],
76+
)
77+
expected = {
78+
(cfg.entry_id, s_total),
79+
(cfg.entry_id, s_i),
80+
(cfg.entry_id, header),
81+
(cfg.entry_id, ret),
82+
(header, s_add),
83+
(header, s_inc),
84+
}
85+
assert set(control_dependence(cfg)) == expected
86+
87+
88+
def test_early_exit_control_dependence_exact():
89+
cfg = _cfg_of("main.py", "early_exit")
90+
line = _by_line(cfg)
91+
header, ret1, s_y, ret2 = line[29], line[30], line[31], line[32]
92+
expected = {
93+
(cfg.entry_id, header),
94+
(header, ret1),
95+
(header, s_y),
96+
(header, ret2),
97+
}
98+
assert set(control_dependence(cfg)) == expected
99+
100+
101+
def test_infinite_loop_post_dominance_well_formed():
102+
cfg = _cfg_of("main.py", "infinite")
103+
ipdom = post_dominators(cfg)
104+
assert set(ipdom) == {n.id for n in cfg.nodes}

0 commit comments

Comments
 (0)