Clean up dominator/post-dominator impl
Tidies up the implementation a bit, splitting the single function and class into distinct computeDominatorTree() and computePostDominatorTree() functions and helper classes.
Joe Savona committed
May 8, 2023 at 15:35 UTC
90109b3572ed3089afd862fd92830f5ce4f447ad
3 files changed
+142
-68
compiler/forget/src/HIR/Dominator.ts
+131
-64
@@ -11,24 +11,45 @@ import { BlockId, HIRFunction } from "./HIR";
11
import { eachTerminalSuccessor } from "./visitors";
12
13
/**
14
- * Computes the dominator or post dominator tree of the given function. The returned `Dominator` stores
15
- * the immediate dominator of each node in the function, which can be retrieved with `Dominator.prototype.get()`.
14
+ * Computes the dominator tree of the given function. The returned `Dominator` stores the immediate
15
+ * dominator of each node in the function, which can be retrieved with `Dominator.prototype.get()`.
16
*
17
- * The implementation is a straightforward adaptation of https://www.cs.rice.edu/~keith/Embed/dom.pdf
18
- * except that CFG nodes ordering is inverted (so the comparison functions are swapped)
17
+ * A block X dominates block Y in the CFG if all paths to Y must flow through X. Thus the entry
18
+ * block dominates all other blocks. See https://en.wikipedia.org/wiki/Dominator_(graph_theory)
19
+ * for more.
20
*/
20
-export function computeDominators(
21
+export function computeDominatorTree(fn: HIRFunction): Dominator<BlockId> {
22
+ const graph = buildGraph(fn);
23
+ const nodes = computeImmediateDominators(graph);
24
+ return new Dominator(graph.entry, nodes);
25
+}
26
+
27
+/**
28
+ * Similar to `computeDominatorTree()` but computes the post dominators of the function. The returned
29
+ * `PostDominator` stores the immediate post-dominators of each node in the function.
30
+ *
31
+ * A block Y post-dominates block X in the CFG if all paths from X to the exit must flow through Y.
32
+ * The caller must specify whether to consider `throw` statements as exit nodes. If set to false,
33
+ * only return statements are considered exit nodes.
34
+ */
35
+export function computePostDominatorTree(
36
fn: HIRFunction,
22
- options: { reverse: boolean } | null = null
23
-): Dominator<BlockId> {
24
- const reverse = options?.reverse === true;
25
- let graph: Graph<BlockId>;
26
- if (reverse) {
27
- graph = computeReverseGraph(fn);
28
- } else {
29
- graph = computeGraph(fn);
37
+ options: { includeThrowsAsExitNode: boolean }
38
+): PostDominator<BlockId> {
39
+ const graph = buildReverseGraph(fn, options.includeThrowsAsExitNode);
40
+ const nodes = computeImmediateDominators(graph);
41
+
42
+ // When options.includeThrowsAsExitNode is false, nodes that flow into a throws
43
+ // terminal and don't reach the exit node won't be in the node map. Add them
44
+ // with themselves as dominator to reflect that they don't flow into the exit.
45
+ if (!options.includeThrowsAsExitNode) {
46
+ for (const [id] of fn.body.blocks) {
47
+ if (!nodes.has(id)) {
48
+ nodes.set(id, id);
49
+ }
50
+ }
51
}
31
- return Dominator.create(graph);
52
+ return new PostDominator(graph.entry, nodes);
53
}
54
55
type Node<T> = {
@@ -49,62 +70,47 @@ class Dominator<T> {
70
#entry: T;
71
#nodes: Map<T, T>;
72
52
- private constructor(entry: T, nodes: Map<T, T>) {
73
+ constructor(entry: T, nodes: Map<T, T>) {
74
this.#entry = entry;
75
this.#nodes = nodes;
76
}
77
57
- static create<T>(graph: Graph<T>): Dominator<T> {
58
- const nodes: Map<T, T> = new Map();
59
- nodes.set(graph.entry, graph.entry);
60
- let changed = true;
61
- while (changed) {
62
- changed = false;
63
- for (const [id, node] of graph.nodes) {
64
- // Skip start node
65
- if (node.id === graph.entry) {
66
- continue;
67
- }
78
+ /**
79
+ * Returns the entry node
80
+ */
81
+ get entry(): T {
82
+ return this.#entry;
83
+ }
84
69
- // first processed predecessor
70
- let newIdom: T | null = null;
71
- for (const pred of node.preds) {
72
- if (nodes.has(pred)) {
73
- newIdom = pred;
74
- break;
75
- }
76
- }
77
- invariant(
78
- newIdom !== null,
79
- `At least one predecessor must have been visited for block ${id}`
80
- );
81
-
82
- for (const pred of node.preds) {
83
- // For all other predecessors
84
- if (pred === newIdom) {
85
- continue;
86
- }
87
- const predDom = nodes.get(pred);
88
- if (predDom !== undefined) {
89
- newIdom = intersect(pred, newIdom, graph, nodes);
90
- }
91
- }
85
+ /**
86
+ * Returns the immediate dominator of the block with @param id if present. Returns null
87
+ * if there is no immediate dominator (ie if the dominator is @param id itself).
88
+ */
89
+ get(id: T): T | null {
90
+ const dominator = this.#nodes.get(id);
91
+ invariant(dominator !== undefined, "Unknown node");
92
+ return dominator === id ? null : dominator;
93
+ }
94
93
- if (nodes.get(id) !== newIdom) {
94
- nodes.set(id, newIdom);
95
- changed = true;
96
- }
97
- }
98
- }
95
+ debug(): string {
96
+ return prettyFormat(this.#nodes);
97
+ }
98
+}
99
+
100
+class PostDominator<T> {
101
+ #exit: T;
102
+ #nodes: Map<T, T>;
103
100
- return new Dominator(graph.entry, nodes);
104
+ constructor(exit: T, nodes: Map<T, T>) {
105
+ this.#exit = exit;
106
+ this.#nodes = nodes;
107
}
108
109
/**
104
- * Returns the entry node
110
+ * Returns the node representing normal exit from the function, ie return terminals.
111
*/
106
- get entry(): T {
107
- return this.#entry;
112
+ get exit(): T {
113
+ return this.#exit;
114
}
115
116
/**
@@ -113,9 +119,7 @@ class Dominator<T> {
119
*/
120
get(id: T): T | null {
121
const dominator = this.#nodes.get(id);
116
- if (dominator === undefined) {
117
- return null;
118
- }
122
+ invariant(dominator !== undefined, "Unknown node");
123
return dominator === id ? null : dominator;
124
}
125
@@ -124,6 +128,55 @@ class Dominator<T> {
128
}
129
}
130
131
+/**
132
+ * The implementation is a straightforward adaptation of https://www.cs.rice.edu/~keith/Embed/dom.pdf
133
+ * except that CFG nodes ordering is inverted (so the comparison functions are swapped)
134
+ */
135
+function computeImmediateDominators<T>(graph: Graph<T>): Map<T, T> {
136
+ const nodes: Map<T, T> = new Map();
137
+ nodes.set(graph.entry, graph.entry);
138
+ let changed = true;
139
+ while (changed) {
140
+ changed = false;
141
+ for (const [id, node] of graph.nodes) {
142
+ // Skip start node
143
+ if (node.id === graph.entry) {
144
+ continue;
145
+ }
146
+
147
+ // first processed predecessor
148
+ let newIdom: T | null = null;
149
+ for (const pred of node.preds) {
150
+ if (nodes.has(pred)) {
151
+ newIdom = pred;
152
+ break;
153
+ }
154
+ }
155
+ invariant(
156
+ newIdom !== null,
157
+ `At least one predecessor must have been visited for block ${id}`
158
+ );
159
+
160
+ for (const pred of node.preds) {
161
+ // For all other predecessors
162
+ if (pred === newIdom) {
163
+ continue;
164
+ }
165
+ const predDom = nodes.get(pred);
166
+ if (predDom !== undefined) {
167
+ newIdom = intersect(pred, newIdom, graph, nodes);
168
+ }
169
+ }
170
+
171
+ if (nodes.get(id) !== newIdom) {
172
+ nodes.set(id, newIdom);
173
+ changed = true;
174
+ }
175
+ }
176
+ }
177
+ return nodes;
178
+}
179
+
180
function intersect<T>(a: T, b: T, graph: Graph<T>, nodes: Map<T, T>): T {
181
let block1: Node<T> = graph.nodes.get(a)!;
182
let block2: Node<T> = graph.nodes.get(b)!;
@@ -140,7 +193,10 @@ function intersect<T>(a: T, b: T, graph: Graph<T>, nodes: Map<T, T>): T {
193
return block1.id;
194
}
195
143
-function computeGraph(fn: HIRFunction): Graph<BlockId> {
196
+/**
197
+ * Turns the HIRFunction into a simplified internal form that is shared for dominator/post-dominator computation
198
+ */
199
+function buildGraph(fn: HIRFunction): Graph<BlockId> {
200
const graph: Graph<BlockId> = { entry: fn.body.entry, nodes: new Map() };
201
let index = 0;
202
for (const [id, block] of fn.body.blocks) {
@@ -154,7 +210,15 @@ function computeGraph(fn: HIRFunction): Graph<BlockId> {
210
return graph;
211
}
212
157
-function computeReverseGraph(fn: HIRFunction): Graph<BlockId> {
213
+/**
214
+ * Turns the HIRFunction into a simplified internal form that is shared for dominator/post-dominator computation,
215
+ * notably this version flips the graph and puts the reversed form back into RPO (such that successors are before predecessors).
216
+ * Note that RPO of the reversed graph isn't the same as reversed RPO of the forward graph because of loops.
217
+ */
218
+function buildReverseGraph(
219
+ fn: HIRFunction,
220
+ includeThrowsAsExitNode: boolean
221
+): Graph<BlockId> {
222
const nodes: Map<BlockId, Node<BlockId>> = new Map();
223
const exitId = fn.env.nextBlockId;
224
const exit: Node<BlockId> = {
@@ -175,6 +239,9 @@ function computeReverseGraph(fn: HIRFunction): Graph<BlockId> {
239
if (block.terminal.kind === "return") {
240
node.preds.add(exitId);
241
exit.succs.add(id);
242
+ } else if (block.terminal.kind === "throw" && includeThrowsAsExitNode) {
243
+ node.preds.add(exitId);
244
+ exit.succs.add(id);
245
}
246
nodes.set(id, node);
247
}
compiler/forget/src/HIR/ValidateUnconditionalHooks.ts
+10
-4
@@ -11,7 +11,7 @@ import {
11
ErrorSeverity,
12
} from "../CompilerError";
13
import { findBlocksWithBackEdges } from "../Optimization/DeadCodeElimination";
14
-import { computeDominators } from "./Dominator";
14
+import { computePostDominatorTree } from "./Dominator";
15
import { BlockId, HIRFunction, isHookType } from "./HIR";
16
17
/**
@@ -58,9 +58,12 @@ export function validateUnconditionalHooks(fn: HIRFunction): void {
58
// Construct the set of blocks that is always reachable from the entry block.
59
const unconditionalBlocks = new Set<BlockId>();
60
const blocksWithBackEdges = findBlocksWithBackEdges(fn);
61
- const dominators = computeDominators(fn, { reverse: true });
62
- // Post dominator graph so .entry is the "exit" node
63
- const exit = dominators.entry;
61
+ const dominators = computePostDominatorTree(fn, {
62
+ // Hooks must only be in a consistent order for executions that return normally,
63
+ // so we opt-in to viewing throw as a non-exit node.
64
+ includeThrowsAsExitNode: false,
65
+ });
66
+ const exit = dominators.exit;
67
let current: BlockId | null = fn.body.entry;
68
while (
69
current !== null &&
@@ -82,6 +85,9 @@ export function validateUnconditionalHooks(fn: HIRFunction): void {
85
isHookType(instr.value.callee.identifier)
86
) {
87
const loc = instr.loc;
88
+ // TODO: the current ESLint rule has different error messages for code that is called conditionally, in a loop, etc.
89
+ // An option would be to first record an Array<[BlockId, Place]> of problematic hooks, then compute the normal dominator graph
90
+ // and walk upward to determine whether each error location was due to a loop, if, etc.
91
errors.pushErrorDetail(
92
new CompilerErrorDetail({
93
codeframe: null,
compiler/forget/src/HIR/index.ts
+1
@@ -6,6 +6,7 @@
6
*/
7
8
export { lower } from "./BuildHIR";
9
+export { computeDominatorTree, computePostDominatorTree } from "./Dominator";
10
export { Environment } from "./Environment";
11
export * from "./HIR";
12
export {