[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