@samitouri / QOS-React / commits / 2ddd00dbb1

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 +}