main
ts 287 lines 8.17 KB
Raw
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 prettyFormat from 'pretty-format';
9 import {CompilerError} from '../CompilerError';
10 import {BlockId, GeneratedSource, HIRFunction} from './HIR';
11 import {eachTerminalSuccessor} from './visitors';
12
13 /*
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 * 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 */
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,
37 options: {includeThrowsAsExitNode: boolean},
38 ): PostDominator<BlockId> {
39 const graph = buildReverseGraph(fn, options.includeThrowsAsExitNode);
40 const nodes = computeImmediateDominators(graph);
41
42 /*
43 * When options.includeThrowsAsExitNode is false, nodes that flow into a throws
44 * terminal and don't reach the exit node won't be in the node map. Add them
45 * with themselves as dominator to reflect that they don't flow into the exit.
46 */
47 if (!options.includeThrowsAsExitNode) {
48 for (const [id] of fn.body.blocks) {
49 if (!nodes.has(id)) {
50 nodes.set(id, id);
51 }
52 }
53 }
54 return new PostDominator(graph.entry, nodes);
55 }
56
57 type Node<T> = {
58 id: T;
59 index: number;
60 preds: Set<T>;
61 succs: Set<T>;
62 };
63 type Graph<T> = {
64 entry: T;
65 nodes: Map<T, Node<T>>;
66 };
67
68 // A dominator tree that stores the immediate dominator for each block in function.
69 export class Dominator<T> {
70 #entry: T;
71 #nodes: Map<T, T>;
72
73 constructor(entry: T, nodes: Map<T, T>) {
74 this.#entry = entry;
75 this.#nodes = nodes;
76 }
77
78 // Returns the entry node
79 get entry(): T {
80 return this.#entry;
81 }
82
83 /*
84 * Returns the immediate dominator of the block with @param id if present. Returns null
85 * if there is no immediate dominator (ie if the dominator is @param id itself).
86 */
87 get(id: T): T | null {
88 const dominator = this.#nodes.get(id);
89 CompilerError.invariant(dominator !== undefined, {
90 reason: 'Unknown node',
91 loc: GeneratedSource,
92 });
93 return dominator === id ? null : dominator;
94 }
95
96 debug(): string {
97 const dominators = new Map();
98 for (const [key, value] of this.#nodes) {
99 dominators.set(`bb${key}`, `bb${value}`);
100 }
101 return prettyFormat({
102 entry: `bb${this.#entry}`,
103 dominators,
104 });
105 }
106 }
107
108 export class PostDominator<T> {
109 #exit: T;
110 #nodes: Map<T, T>;
111
112 constructor(exit: T, nodes: Map<T, T>) {
113 this.#exit = exit;
114 this.#nodes = nodes;
115 }
116
117 // Returns the node representing normal exit from the function, ie return terminals.
118 get exit(): T {
119 return this.#exit;
120 }
121
122 /*
123 * Returns the immediate dominator of the block with @param id if present. Returns null
124 * if there is no immediate dominator (ie if the dominator is @param id itself).
125 */
126 get(id: T): T | null {
127 const dominator = this.#nodes.get(id);
128 CompilerError.invariant(dominator !== undefined, {
129 reason: 'Unknown node',
130 loc: GeneratedSource,
131 });
132 return dominator === id ? null : dominator;
133 }
134
135 debug(): string {
136 const postDominators = new Map();
137 for (const [key, value] of this.#nodes) {
138 postDominators.set(`bb${key}`, `bb${value}`);
139 }
140 return prettyFormat({
141 exit: `bb${this.exit}`,
142 postDominators,
143 });
144 }
145 }
146
147 /*
148 * The implementation is a straightforward adaptation of https://www.cs.rice.edu/~keith/Embed/dom.pdf
149 * except that CFG nodes ordering is inverted (so the comparison functions are swapped)
150 */
151 function computeImmediateDominators<T>(graph: Graph<T>): Map<T, T> {
152 const nodes: Map<T, T> = new Map();
153 nodes.set(graph.entry, graph.entry);
154 let changed = true;
155 while (changed) {
156 changed = false;
157 for (const [id, node] of graph.nodes) {
158 // Skip start node
159 if (node.id === graph.entry) {
160 continue;
161 }
162
163 // first processed predecessor
164 let newIdom: T | null = null;
165 for (const pred of node.preds) {
166 if (nodes.has(pred)) {
167 newIdom = pred;
168 break;
169 }
170 }
171 CompilerError.invariant(newIdom !== null, {
172 reason: `At least one predecessor must have been visited for block ${id}`,
173 loc: GeneratedSource,
174 });
175
176 for (const pred of node.preds) {
177 // For all other predecessors
178 if (pred === newIdom) {
179 continue;
180 }
181 const predDom = nodes.get(pred);
182 if (predDom !== undefined) {
183 newIdom = intersect(pred, newIdom, graph, nodes);
184 }
185 }
186
187 if (nodes.get(id) !== newIdom) {
188 nodes.set(id, newIdom);
189 changed = true;
190 }
191 }
192 }
193 return nodes;
194 }
195
196 function intersect<T>(a: T, b: T, graph: Graph<T>, nodes: Map<T, T>): T {
197 let block1: Node<T> = graph.nodes.get(a)!;
198 let block2: Node<T> = graph.nodes.get(b)!;
199 while (block1 !== block2) {
200 while (block1.index > block2.index) {
201 const dom = nodes.get(block1.id)!;
202 block1 = graph.nodes.get(dom)!;
203 }
204 while (block2.index > block1.index) {
205 const dom = nodes.get(block2.id)!;
206 block2 = graph.nodes.get(dom)!;
207 }
208 }
209 return block1.id;
210 }
211
212 // Turns the HIRFunction into a simplified internal form that is shared for dominator/post-dominator computation
213 function buildGraph(fn: HIRFunction): Graph<BlockId> {
214 const graph: Graph<BlockId> = {entry: fn.body.entry, nodes: new Map()};
215 let index = 0;
216 for (const [id, block] of fn.body.blocks) {
217 graph.nodes.set(id, {
218 id,
219 index: index++,
220 preds: block.preds,
221 succs: new Set(eachTerminalSuccessor(block.terminal)),
222 });
223 }
224 return graph;
225 }
226
227 /*
228 * Turns the HIRFunction into a simplified internal form that is shared for dominator/post-dominator computation,
229 * notably this version flips the graph and puts the reversed form back into RPO (such that successors are before predecessors).
230 * Note that RPO of the reversed graph isn't the same as reversed RPO of the forward graph because of loops.
231 */
232 function buildReverseGraph(
233 fn: HIRFunction,
234 includeThrowsAsExitNode: boolean,
235 ): Graph<BlockId> {
236 const nodes: Map<BlockId, Node<BlockId>> = new Map();
237 const exitId = fn.env.nextBlockId;
238 const exit: Node<BlockId> = {
239 id: exitId,
240 index: 0,
241 preds: new Set(),
242 succs: new Set(),
243 };
244 nodes.set(exitId, exit);
245
246 for (const [id, block] of fn.body.blocks) {
247 const node: Node<BlockId> = {
248 id,
249 index: 0,
250 preds: new Set(eachTerminalSuccessor(block.terminal)),
251 succs: new Set(block.preds),
252 };
253 if (block.terminal.kind === 'return') {
254 node.preds.add(exitId);
255 exit.succs.add(id);
256 } else if (block.terminal.kind === 'throw' && includeThrowsAsExitNode) {
257 node.preds.add(exitId);
258 exit.succs.add(id);
259 }
260 nodes.set(id, node);
261 }
262
263 // Put nodes into RPO form
264 const visited = new Set<BlockId>();
265 const postorder: Array<BlockId> = [];
266 function visit(id: BlockId): void {
267 if (visited.has(id)) {
268 return;
269 }
270 visited.add(id);
271 const node = nodes.get(id)!;
272 for (const successor of node.succs) {
273 visit(successor);
274 }
275 postorder.push(id);
276 }
277 visit(exitId);
278
279 const rpo: Graph<BlockId> = {entry: exitId, nodes: new Map()};
280 let index = 0;
281 for (const id of postorder.reverse()) {
282 const node = nodes.get(id)!;
283 node.index = index++;
284 rpo.nodes.set(id, node);
285 }
286 return rpo;
287 }