@samitouri / QOS-React-2 / commits / 90109b3572

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 {