@samitouri / QOS-React-2 / commits / 5149ce0fbb

[new-arch] Store HIR blocks in reverse postorder

* Changes HIR to store blocks in reverse postorder, which allows forward data flow analysis to iterate the blocks in order and (in the absence of loops) see all predecessors before visiting a successor. * Updates reference kind inference to exploit this ordering Note that the approach of modifying the ordering in `mapTerminalSuccessors()` feels gross, i'd like to split this up a bit.

Joseph Savona committed Oct 12, 2022 at 16:11 UTC 5149ce0fbb5114a8ee46f313365d5e4984fcab94
8 files changed +260 -91
compiler/forget/src/HIR/HIR.ts
+8
@@ -72,6 +72,13 @@ export type HIRFunction = {
72 */
73 export type HIR = {
74 entry: BlockId;
75 +
76 + /**
77 + * Basic blocks are stored as a map to aid certain operations that need to
78 + * lookup blocks by their id. However, the order of the items in the map is
79 + * reverse postorder, that is, barring cycles, predecessors appear before
80 + * successors. This is designed to facilitate forward data flow analysis.
81 + */
82 blocks: Map<BlockId, BasicBlock>;
83 };
84
@@ -84,6 +91,7 @@ export type HIR = {
91 * statements and not implicit exceptions which may occur.
92 */
93 export type BasicBlock = {
94 + id: BlockId;
95 instructions: Array<Instruction>;
96 terminal: Terminal;
97 };
compiler/forget/src/HIR/HIRBuilder.ts
+48 -35
@@ -111,6 +111,7 @@ export default class HIRBuilder {
111 build(): HIR {
112 const { id: blockId, instructions } = this.#current;
113 this.#completed.set(blockId, {
114 + id: blockId,
115 instructions,
116 terminal: { kind: "return", value: null },
117 });
@@ -126,6 +127,7 @@ export default class HIRBuilder {
127 terminate(terminal: Terminal) {
128 const { id: blockId, instructions } = this.#current;
129 this.#completed.set(blockId, {
130 + id: blockId,
131 instructions,
132 terminal,
133 });
@@ -140,6 +142,7 @@ export default class HIRBuilder {
142 terminateWithContinuation(terminal: Terminal, continuation: WipBlock) {
143 const { id: blockId, instructions } = this.#current;
144 this.#completed.set(blockId, {
145 + id: blockId,
146 instructions,
147 terminal,
148 });
@@ -160,7 +163,7 @@ export default class HIRBuilder {
163 */
164 complete(block: WipBlock, terminal: Terminal) {
165 const { id: blockId, instructions } = block;
163 - this.#completed.set(blockId, { instructions, terminal });
166 + this.#completed.set(blockId, { id: blockId, instructions, terminal });
167 }
168
169 /**
@@ -175,7 +178,7 @@ export default class HIRBuilder {
178 this.#current = newBlock(nextId);
179 const terminal = fn(nextId);
180 const { id: blockId, instructions } = this.#current;
178 - this.#completed.set(blockId, { instructions, terminal });
181 + this.#completed.set(blockId, { id: blockId, instructions, terminal });
182 this.#current = current;
183 return nextId;
184 }
@@ -288,7 +291,10 @@ export default class HIRBuilder {
291 /**
292 * Helper to shrink a CFG to eliminate unreachable node and eliminate jump-only blocks.
293 */
291 -function shrink(func: HIR): HIR {
294 +function shrink(func: {
295 + blocks: Map<BlockId, BasicBlock>;
296 + entry: BlockId;
297 +}): HIR {
298 const gotos = new Map();
299 /**
300 * Given a target block for some terminator, resolves the ideal block that should be
@@ -313,45 +319,49 @@ function shrink(func: HIR): HIR {
319 }
320 }
321
316 - /**
317 - * Fixpoint iteration to explore all blocks reachable from the entry block,
318 - * and to resolve their terminator targets to avoid indirections.
319 - * This implicitly prunes unreachable blocks since they are never visited and
320 - * therefore not added to the output.
321 - */
322 - const queue = [func.entry];
323 - // new set of output blocks
322 + const visited: Set<BlockId> = new Set();
323 const blocks: Map<BlockId, BasicBlock> = new Map();
325 - while (queue.length !== 0) {
326 - const blockId = queue.shift()!;
327 - if (blocks.has(blockId)) {
328 - continue;
329 - }
330 - const { instructions, terminal: prevTerminal } = func.blocks.get(blockId)!;
331 - const terminal = mapTerminalSuccessors(prevTerminal, (prevTarget) => {
332 - const target = resolveBlockTarget(prevTarget);
333 - queue.push(target);
334 - return target;
335 - });
336 - blocks.set(blockId, {
337 - instructions,
338 - terminal,
339 - });
324 +
325 + // Visit all the blocks and map their successors to remove indirections,
326 + // and eliminate unreachable blocks. Stores blocks in postorder,
327 + // successors before predecessors.
328 + function visit(blockId: BlockId) {
329 + visited.add(blockId);
330 + const block = func.blocks.get(blockId)!;
331 + const { instructions, terminal: prevTerminal } = block;
332 + const terminal = mapTerminalSuccessors(
333 + prevTerminal,
334 + (prevTarget, isFallthrough) => {
335 + const target = resolveBlockTarget(prevTarget);
336 + if (!visited.has(target) && !isFallthrough) {
337 + visit(target);
338 + }
339 + return target;
340 + }
341 + );
342 + block.terminal = terminal;
343 + blocks.set(blockId, block);
344 }
345 + visit(func.entry);
346
342 - for (const block of blocks.values()) {
347 + // Cleanup any fallthrough blocks that weren't visited
348 + // also store into reverse postorder.
349 + const reversedBlocks: Map<BlockId, BasicBlock> = new Map();
350 + for (const blockId of Array.from(blocks.keys()).reverse()) {
351 + const block = blocks.get(blockId)!;
352 if (block.terminal.kind === "if" || block.terminal.kind === "switch") {
353 if (
354 block.terminal.fallthrough !== null &&
346 - !blocks.has(block.terminal.fallthrough)
355 + !visited.has(block.terminal.fallthrough)
356 ) {
357 block.terminal.fallthrough = null;
358 }
359 }
360 + reversedBlocks.set(blockId, block);
361 }
362
363 return {
354 - blocks,
364 + blocks: reversedBlocks,
365 entry: func.entry,
366 };
367 }
@@ -368,6 +378,9 @@ function getTargetIfIndirection(block: BasicBlock): number | null {
378
379 /**
380 * Maps a terminal node's block assignments using the provided function.
381 + *
382 + * TODO: this visits successors in reverse ordering to facilitate shrink()'s
383 + * goal of producing a reverse postorder graph where siblings are in-order.
384 */
385 export function mapTerminalSuccessors(
386 terminal: Terminal,
@@ -382,10 +395,10 @@ export function mapTerminalSuccessors(
395 };
396 }
397 case "if": {
385 - const consequent = fn(terminal.consequent, false);
386 - const alternate = fn(terminal.alternate, false);
398 const fallthrough =
399 terminal.fallthrough !== null ? fn(terminal.fallthrough, true) : null;
400 + const alternate = fn(terminal.alternate, false);
401 + const consequent = fn(terminal.consequent, false);
402 return {
403 kind: "if",
404 test: terminal.test,
@@ -395,19 +408,19 @@ export function mapTerminalSuccessors(
408 };
409 }
410 case "switch": {
398 - const cases = terminal.cases.map((case_) => {
411 + const fallthrough =
412 + terminal.fallthrough !== null ? fn(terminal.fallthrough, true) : null;
413 + const cases = [...terminal.cases].reverse().map((case_) => {
414 const target = fn(case_.block, false);
415 return {
416 test: case_.test,
417 block: target,
418 };
419 });
405 - const fallthrough =
406 - terminal.fallthrough !== null ? fn(terminal.fallthrough, true) : null;
420 return {
421 kind: "switch",
422 test: terminal.test,
410 - cases,
423 + cases: cases.reverse(),
424 fallthrough,
425 };
426 }
compiler/forget/src/HIR/InferReferenceCapability.ts
+44 -37
@@ -99,51 +99,58 @@ export default function inferReferenceCapability(fn: HIRFunction) {
99 initialEnvironment.define(place, value);
100 }
101
102 - // Queue of blocks to visit, with block and the incoming Environment value
103 - const queue: Array<QueueEntry> = [
104 - { blockId: fn.body.entry, environment: initialEnvironment },
105 - ];
106 - // Map of blocks to the last incoming environment that was processed
102 + // Map of blocks to the last (merged) incoming environment that was processed
103 const environmentsByBlock: Map<BlockId, Environment> = new Map();
104
109 - while (queue.length !== 0) {
110 - const { blockId, environment: incomingEnvironment } = queue.shift()!;
111 -
112 - let previousEnvironment = environmentsByBlock.get(blockId);
113 - let nextEnvironment: Environment | null = null;
114 - if (previousEnvironment === undefined) {
115 - // If no previous environment, save the incoming environment and
116 - // infer the block with this environment
117 - environmentsByBlock.set(blockId, incomingEnvironment);
118 - nextEnvironment = incomingEnvironment.clone();
105 + // Multiple predecessors may be visited prior to reaching a given successor,
106 + // so track the list of incoming environments for each successor block.
107 + // These are merged when reaching that block again.
108 + const queuedEnvironments: Map<BlockId, Environment> = new Map();
109 + function queue(blockId: BlockId, environment: Environment) {
110 + let queuedEnvironment = queuedEnvironments.get(blockId);
111 + if (queuedEnvironment != null) {
112 + // merge the queued environments for this block
113 + environment = queuedEnvironment.merge(environment) ?? environment;
114 + queuedEnvironments.set(blockId, environment);
115 } else {
120 - // If there's a previous environment, merge the previous/new incoming
121 - // environments. If there are no changes, then the block can be skipped.
122 - // otherwise save the merged environment and revisit the block with it.
123 - const mergedEnvironment = previousEnvironment.merge(incomingEnvironment);
124 - if (mergedEnvironment !== null) {
125 - environmentsByBlock.set(blockId, mergedEnvironment);
126 - nextEnvironment = mergedEnvironment.clone();
127 - } else {
128 - continue;
116 + // this is the first queued environment for this block, see whether
117 + // there are changed relative to the last time it was processed.
118 + const prevEnvironment = environmentsByBlock.get(blockId);
119 + const nextEnvironment =
120 + prevEnvironment != null
121 + ? prevEnvironment.merge(environment)
122 + : environment;
123 + if (nextEnvironment != null) {
124 + queuedEnvironments.set(blockId, nextEnvironment);
125 }
126 }
127 + }
128 + queue(fn.body.entry, initialEnvironment);
129
132 - const environment = nextEnvironment; // rebind to preserve the non-null refinement
133 - const block = fn.body.blocks.get(blockId)!;
134 - inferBlock(environment, block);
130 + while (queuedEnvironments.size !== 0) {
131 + for (const [blockId, block] of fn.body.blocks) {
132 + const incomingEnvironment = queuedEnvironments.get(blockId);
133 + queuedEnvironments.delete(blockId);
134 + if (incomingEnvironment == null) {
135 + continue;
136 + }
137
136 - // TODO: add a `forEachTerminalSuccessor` helper, we don't actually want the result
137 - // here
138 - const _ = mapTerminalSuccessors(
139 - block.terminal,
140 - (nextBlockId, isFallthrough) => {
141 - if (!isFallthrough) {
142 - queue.push({ blockId: nextBlockId, environment });
138 + environmentsByBlock.set(blockId, incomingEnvironment);
139 + const environment = incomingEnvironment.clone();
140 + inferBlock(environment, block);
141 +
142 + // TODO: add a `forEachTerminalSuccessor` helper, we don't actually want the result
143 + // here
144 + const _ = mapTerminalSuccessors(
145 + block.terminal,
146 + (nextBlockId, isFallthrough) => {
147 + if (!isFallthrough) {
148 + queue(nextBlockId, environment);
149 + }
150 + return nextBlockId;
151 }
144 - return nextBlockId;
145 - }
146 - );
152 + );
153 + }
154 }
155 }
156
compiler/forget/src/__tests__/fixtures/hir/component.expect.md
+11 -11
@@ -48,17 +48,6 @@ bb3:
48 Const mutable $11 = null
49 Const mutable $12 = Binary readonly item$10 == readonly $11
50 If (readonly $12) then:bb8 else:bb9
51 -bb2:
52 - Const mutable count$17 = readonly renderedItems$4.length
53 - Const mutable $18 = "div"
54 - Const mutable $19 = "\n "
55 - Const mutable $20 = "h1"
56 - Const mutable $21 = " Items"
57 - Const mutable $22 = JSX <readonly $20>{freeze count$17}{readonly $21}</readonly $20>
58 - Const mutable $23 = "\n "
59 - Const mutable $24 = "\n "
60 - Const mutable $25 = JSX <readonly $18>{readonly $19}{readonly $22}{readonly $23}{freeze renderedItems$4}{readonly $24}</readonly $18>
61 - Return readonly $25
51 bb8:
52 Const mutable $13 = readonly $12
53 Goto bb7
@@ -74,6 +63,17 @@ bb4:
63 Call mutable renderedItems$4.push(readonly $15)
64 Const mutable $16 = Binary readonly renderedItems$4.length >= readonly max$7
65 If (readonly $16) then:bb2 else:bb1
66 +bb2:
67 + Const mutable count$17 = readonly renderedItems$4.length
68 + Const mutable $18 = "div"
69 + Const mutable $19 = "\n "
70 + Const mutable $20 = "h1"
71 + Const mutable $21 = " Items"
72 + Const mutable $22 = JSX <readonly $20>{freeze count$17}{readonly $21}</readonly $20>
73 + Const mutable $23 = "\n "
74 + Const mutable $24 = "\n "
75 + Const mutable $25 = JSX <readonly $18>{readonly $19}{readonly $22}{readonly $23}{freeze renderedItems$4}{readonly $24}</readonly $18>
76 + Return readonly $25
77 ```
78
79 ## Code
compiler/forget/src/__tests__/fixtures/hir/conditional-break.expect.md
+3 -3
@@ -201,12 +201,12 @@ bb0:
201 Const mutable a$2 = Array []
202 Call mutable a$2.push(readonly props$1.a)
203 If (readonly props$1.b) then:bb1 else:bb2
204 -bb1:
205 - Call mutable a$2.push(readonly props$1.d)
206 - Return freeze a$2
204 bb2:
205 Call mutable a$2.push(readonly props$1.c)
206 Goto bb1
207 +bb1:
208 + Call mutable a$2.push(readonly props$1.d)
209 + Return freeze a$2
210 ```
211
212 ## Code
compiler/forget/src/__tests__/fixtures/hir/reverse-postorder.expect.md new
+114
@@ -0,0 +1,114 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component(props) {
6 + let x;
7 + if (props.cond) {
8 + switch (props.test) {
9 + case 0: {
10 + x = props.v0;
11 + break;
12 + }
13 + case 1: {
14 + x = props.v1;
15 + break;
16 + }
17 + case 2: {
18 + }
19 + default: {
20 + x = props.v2;
21 + }
22 + }
23 + } else {
24 + if (props.cond2) {
25 + x = props.b;
26 + } else {
27 + x = props.c;
28 + }
29 + }
30 + x;
31 +}
32 +
33 +```
34 +
35 +## HIR
36 +
37 +```
38 +bb0:
39 + Let mutable x$2 = undefined
40 + If (readonly props$1.cond) then:bb2 else:bb10
41 +bb2:
42 + Const mutable $3 = 2
43 + Const mutable $4 = 1
44 + Const mutable $5 = 0
45 + Switch (<unknown> props$1.test)
46 + Case readonly $5: bb8
47 + Case readonly $4: bb6
48 + Case readonly $3: bb4
49 + Default: bb4
50 +bb8:
51 + Reassign mutable x$2 = readonly props$1.v0
52 + Goto bb1
53 +bb6:
54 + Reassign mutable x$2 = readonly props$1.v1
55 + Goto bb1
56 +bb4:
57 + Reassign mutable x$2 = readonly props$1.v2
58 + Goto bb1
59 +bb10:
60 + If (readonly props$1.cond2) then:bb12 else:bb13
61 +bb12:
62 + Reassign mutable x$2 = readonly props$1.b
63 + Goto bb1
64 +bb13:
65 + Reassign mutable x$2 = readonly props$1.c
66 + Goto bb1
67 +bb1:
68 + readonly x$2
69 + Return
70 +```
71 +
72 +## Code
73 +
74 +```javascript
75 +function Component$0(props$1) {
76 + let x$2 = undefined;
77 + if (props$1.cond) {
78 + switch (props$1.test) {
79 + case 0: {
80 + x$2 = props$1.v0;
81 + ("<<TODO: handle complex control flow in codegen>>");
82 + }
83 + case 1: {
84 + x$2 = props$1.v1;
85 + ("<<TODO: handle complex control flow in codegen>>");
86 + }
87 + case 2: {
88 + x$2 = props$1.v2;
89 + ("<<TODO: handle complex control flow in codegen>>");
90 + }
91 + default: {
92 + x$2 = props$1.v2;
93 + ("<<TODO: handle complex control flow in codegen>>");
94 + }
95 + }
96 + x$2;
97 + return;
98 + } else {
99 + if (props$1.cond2) {
100 + x$2 = props$1.b;
101 + ("<<TODO: handle complex control flow in codegen>>");
102 + } else {
103 + x$2 = props$1.c;
104 + ("<<TODO: handle complex control flow in codegen>>");
105 + }
106 + x$2;
107 + return;
108 + }
109 + x$2;
110 + return;
111 +}
112 +
113 +```
114 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/reverse-postorder.js new
+27
@@ -0,0 +1,27 @@
1 +function Component(props) {
2 + let x;
3 + if (props.cond) {
4 + switch (props.test) {
5 + case 0: {
6 + x = props.v0;
7 + break;
8 + }
9 + case 1: {
10 + x = props.v1;
11 + break;
12 + }
13 + case 2: {
14 + }
15 + default: {
16 + x = props.v2;
17 + }
18 + }
19 + } else {
20 + if (props.cond2) {
21 + x = props.b;
22 + } else {
23 + x = props.c;
24 + }
25 + }
26 + x;
27 +}
compiler/forget/src/__tests__/fixtures/hir/switch-non-final-default.expect.md
+5 -5
@@ -42,11 +42,6 @@ bb0:
42 Case readonly $5: bb6
43 Default: bb1
44 Case readonly $4: bb2
45 -bb1:
46 - Const mutable child$7 = JSX <readonly Component$0 data={freeze x$2} ></readonly Component$0>
47 - Call readonly y$3.push(readonly props$1.p4)
48 - Const mutable $8 = JSX <readonly Component$0 data={freeze y$3} >{readonly child$7}</readonly Component$0>
49 - Return readonly $8
45 bb6:
46 Call mutable x$2.push(readonly props$1.p2)
47 Reassign mutable y$3 = Array []
@@ -54,6 +49,11 @@ bb6:
49 bb2:
50 Reassign mutable y$3 = readonly x$2
51 Goto bb1
52 +bb1:
53 + Const mutable child$7 = JSX <readonly Component$0 data={freeze x$2} ></readonly Component$0>
54 + Call readonly y$3.push(readonly props$1.p4)
55 + Const mutable $8 = JSX <readonly Component$0 data={freeze y$3} >{readonly child$7}</readonly Component$0>
56 + Return readonly $8
57 ```
58
59 ## Code