Implement dominator/post-dominator tree calculation
Implements an efficient algorithm for computing the dominator (or post dominator) tree of a CFG, following https://www.cs.rice.edu/~keith/Embed/dom.pdf. This is used/tested in the next PR to validate that hooks are called unconditionally. note: I clean up the implementation quite a bit late in the stack in #1584
Joe Savona committed
May 8, 2023 at 08:55 UTC
2ddd00dbb1771617565133a01eb61a8c8e1d358d
3 files changed
+304
compiler/forget/src/HIR/Dominator.ts
new
+207
@@ -0,0 +1,207 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+import invariant from "invariant";
9
+import prettyFormat from "pretty-format";
10
+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()`.
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)
19
+ */
20
+export function computeDominators(
21
+ 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);
30
+ }
31
+ return Dominator.create(graph);
32
+}
33
+
34
+type Node<T> = {
35
+ id: T;
36
+ index: number;
37
+ preds: Set<T>;
38
+ succs: Set<T>;
39
+};
40
+type Graph<T> = {
41
+ entry: T;
42
+ nodes: Map<T, Node<T>>;
43
+};
44
+
45
+/**
46
+ * A dominator tree that stores the immediate dominator for each block in function.
47
+ */
48
+class Dominator<T> {
49
+ #entry: T;
50
+ #nodes: Map<T, T>;
51
+
52
+ private constructor(entry: T, nodes: Map<T, T>) {
53
+ this.#entry = entry;
54
+ this.#nodes = nodes;
55
+ }
56
+
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
+ }
68
+
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
+ }
92
+
93
+ if (nodes.get(id) !== newIdom) {
94
+ nodes.set(id, newIdom);
95
+ changed = true;
96
+ }
97
+ }
98
+ }
99
+
100
+ return new Dominator(graph.entry, nodes);
101
+ }
102
+
103
+ /**
104
+ * Returns the entry node
105
+ */
106
+ get entry(): T {
107
+ return this.#entry;
108
+ }
109
+
110
+ /**
111
+ * Returns the immediate dominator of the block with @param id if present. Returns null
112
+ * if there is no immediate dominator (ie if the dominator is @param id itself).
113
+ */
114
+ get(id: T): T | null {
115
+ const dominator = this.#nodes.get(id);
116
+ invariant(
117
+ dominator !== undefined,
118
+ `Called on invalid node identifier '${id}'`
119
+ );
120
+ return dominator === id ? null : dominator;
121
+ }
122
+
123
+ debug(): string {
124
+ return prettyFormat(this.#nodes);
125
+ }
126
+}
127
+
128
+function intersect<T>(a: T, b: T, graph: Graph<T>, nodes: Map<T, T>): T {
129
+ let block1: Node<T> = graph.nodes.get(a)!;
130
+ let block2: Node<T> = graph.nodes.get(b)!;
131
+ while (block1 !== block2) {
132
+ while (block1.index > block2.index) {
133
+ const dom = nodes.get(block1.id)!;
134
+ block1 = graph.nodes.get(dom)!;
135
+ }
136
+ while (block2.index > block1.index) {
137
+ const dom = nodes.get(block2.id)!;
138
+ block2 = graph.nodes.get(dom)!;
139
+ }
140
+ }
141
+ return block1.id;
142
+}
143
+
144
+function computeGraph(fn: HIRFunction): Graph<BlockId> {
145
+ const graph: Graph<BlockId> = { entry: fn.body.entry, nodes: new Map() };
146
+ let index = 0;
147
+ for (const [id, block] of fn.body.blocks) {
148
+ graph.nodes.set(id, {
149
+ id,
150
+ index: index++,
151
+ preds: block.preds,
152
+ succs: new Set(eachTerminalSuccessor(block.terminal)),
153
+ });
154
+ }
155
+ return graph;
156
+}
157
+
158
+function computeReverseGraph(fn: HIRFunction): Graph<BlockId> {
159
+ const nodes: Map<BlockId, Node<BlockId>> = new Map();
160
+ const exitId = fn.env.nextBlockId;
161
+ const exit: Node<BlockId> = {
162
+ id: exitId,
163
+ index: 0,
164
+ preds: new Set(),
165
+ succs: new Set(),
166
+ };
167
+ nodes.set(exitId, exit);
168
+
169
+ for (const [id, block] of fn.body.blocks) {
170
+ const node: Node<BlockId> = {
171
+ id,
172
+ index: 0,
173
+ preds: new Set(eachTerminalSuccessor(block.terminal)),
174
+ succs: new Set(block.preds),
175
+ };
176
+ if (block.terminal.kind === "return" || block.terminal.kind === "throw") {
177
+ node.preds.add(exitId);
178
+ exit.succs.add(id);
179
+ }
180
+ nodes.set(id, node);
181
+ }
182
+
183
+ // Put nodes into RPO form
184
+ const visited = new Set<BlockId>();
185
+ const postorder: Array<BlockId> = [];
186
+ function visit(id: BlockId): void {
187
+ if (visited.has(id)) {
188
+ return;
189
+ }
190
+ visited.add(id);
191
+ const node = nodes.get(id)!;
192
+ for (const successor of node.succs) {
193
+ visit(successor);
194
+ }
195
+ postorder.push(id);
196
+ }
197
+ visit(exitId);
198
+
199
+ const rpo: Graph<BlockId> = { entry: exitId, nodes: new Map() };
200
+ let index = 0;
201
+ for (const id of postorder.reverse()) {
202
+ const node = nodes.get(id)!;
203
+ node.index = index++;
204
+ rpo.nodes.set(id, node);
205
+ }
206
+ return rpo;
207
+}
compiler/forget/src/__tests__/fixtures/compiler/dominator.expect.md
new
+63
@@ -0,0 +1,63 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @only @debug
6
+function Component(props) {
7
+ let x = 0;
8
+ label: if (props.a) {
9
+ x = 1;
10
+ } else {
11
+ if (props.b) {
12
+ x = 2;
13
+ } else {
14
+ break label;
15
+ }
16
+ x = 3;
17
+ }
18
+ // label2: switch (props.c) {
19
+ // case "a": {
20
+ // x = 4;
21
+ // break;
22
+ // }
23
+ // case "b": {
24
+ // break label2;
25
+ // }
26
+ // case "c": {
27
+ // x = 5;
28
+ // // intentional fallthrough
29
+ // }
30
+ // default: {
31
+ // x = 6;
32
+ // }
33
+ // }
34
+ if (props.d) {
35
+ return null;
36
+ }
37
+ return x;
38
+}
39
+
40
+```
41
+
42
+## Code
43
+
44
+```javascript
45
+// @only @debug
46
+function Component(props) {
47
+ let x = 0;
48
+ if (props.a) {
49
+ x = 1;
50
+ } else {
51
+ if (props.b) {
52
+ x = 3;
53
+ } else {
54
+ }
55
+ }
56
+ if (props.d) {
57
+ return null;
58
+ }
59
+ return x;
60
+}
61
+
62
+```
63
+
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/dominator.js
new
+34
@@ -0,0 +1,34 @@
1
+// @only @debug
2
+function Component(props) {
3
+ let x = 0;
4
+ label: if (props.a) {
5
+ x = 1;
6
+ } else {
7
+ if (props.b) {
8
+ x = 2;
9
+ } else {
10
+ break label;
11
+ }
12
+ x = 3;
13
+ }
14
+ // label2: switch (props.c) {
15
+ // case "a": {
16
+ // x = 4;
17
+ // break;
18
+ // }
19
+ // case "b": {
20
+ // break label2;
21
+ // }
22
+ // case "c": {
23
+ // x = 5;
24
+ // // intentional fallthrough
25
+ // }
26
+ // default: {
27
+ // x = 6;
28
+ // }
29
+ // }
30
+ if (props.d) {
31
+ return null;
32
+ }
33
+ return x;
34
+}