[HIR] add `for` terminal
The changes here are pretty significant and there's a bunch more left: - support for with any of `<init>`, `<test>` or `<update>` empty. - support for with `<init>` as `Expression` instead of VariableDeclaration` node - support assignment expressions in `<update>`, this seems like it might require further new abstractions to allow something like a block to codegen into a single expression.
Jan Kassens committed
Dec 6, 2022 at 12:01 UTC
4837e21de37098dc16fa78ad2f24495524f3aa90
12 files changed
+310
-92
compiler/forget/src/HIR/BuildHIR.ts
+64
-61
@@ -317,90 +317,77 @@ function lowerStatement(
317
}
318
case "ForStatement": {
319
const stmt = stmtPath as NodePath<t.ForStatement>;
320
- /**
321
- * The initializer is evaluated once prior to entering the loop.
322
- * here we are not concerned about scoping, so we can push the
323
- * initializer to the end of of the block leading up to the loop
324
- */
325
- const init = stmt.get("init");
326
- if (init.hasNode()) {
327
- // builder.push(init);
328
- throw new Error("todo: lower initializer in ForStatement");
329
- }
330
- // Block used to evaluate whether to (re)enter or exit the loop
331
- const conditionalBlock = builder.reserve();
320
+
321
+ const testBlock = builder.reserve();
322
// Block for code following the loop
323
const continuationBlock = builder.reserve();
334
- /**
335
- * Block for the updater, which runs after each iteration (including upon `continue`)
336
- * Generally this would increment or decrement the loop index variable
337
- */
338
- const updateBlock = builder.reserve();
339
- const update = stmt.get("update");
340
- if (update.hasNode()) {
341
- // updateBlock[1].push(stmt.get("update") as any as NodePath<t.Statement>);
342
- throw new Error("todo: lower updater for ForStatement");
343
- }
344
- builder.complete(updateBlock, {
345
- kind: "goto",
346
- block: conditionalBlock.id,
347
- variant: GotoVariant.Break,
348
- id: makeInstructionId(0),
324
+
325
+ const initBlock = builder.enter((blockId) => {
326
+ const init = stmt.get("init") as NodePath<t.VariableDeclaration>;
327
+ todoInvariant(
328
+ t.isVariableDeclaration(init.node),
329
+ "handle non variable initialization in for"
330
+ );
331
+ lowerStatement(builder, init);
332
+ return {
333
+ kind: "goto",
334
+ block: testBlock.id,
335
+ variant: GotoVariant.Break,
336
+ id: makeInstructionId(0),
337
+ };
338
});
350
- /**
351
- * Construct the loop itself: the loop body wraps around to the update block
352
- * and the update block is also set as the `continue` target
353
- */
354
- const loopBlock = builder.enter((blockId) => {
355
- return builder.loop(label, updateBlock.id, continuationBlock.id, () => {
339
+
340
+ const updateBlock = builder.enter((blockId) => {
341
+ const update = stmt.get("update");
342
+ if (update.hasNode()) {
343
+ lowerExpressionToVoid(builder, update);
344
+ }
345
+ return {
346
+ kind: "goto",
347
+ block: testBlock.id,
348
+ variant: GotoVariant.Break,
349
+ id: makeInstructionId(0),
350
+ };
351
+ });
352
+
353
+ const bodyBlock = builder.enter((blockId) => {
354
+ return builder.loop(label, updateBlock, continuationBlock.id, () => {
355
lowerStatement(builder, stmt.get("body"));
356
return {
357
kind: "goto",
359
- block: updateBlock.id,
358
+ block: updateBlock,
359
variant: GotoVariant.Continue,
360
id: makeInstructionId(0),
361
};
362
});
363
});
365
- // End the block leading up to the loop, jumping to the conditional block
364
+
365
builder.terminateWithContinuation(
366
{
368
- kind: "goto",
369
- block: conditionalBlock.id,
370
- variant: GotoVariant.Break,
367
+ kind: "for",
368
+ init: initBlock,
369
+ test: testBlock.id,
370
+ update: updateBlock,
371
+ loop: bodyBlock,
372
+ fallthrough: continuationBlock.id,
373
id: makeInstructionId(0),
374
},
373
- conditionalBlock
375
+ testBlock
376
);
377
376
- let terminal: Terminal;
378
const test = stmt.get("test");
378
- if (test.hasNode()) {
379
- /**
380
- * Terminate the conditional block with the test conditional of the for statement:
381
- * if the condition is true enter the loop block, else exit to the continuation
382
- */
383
- terminal = {
379
+ todoInvariant(test.hasNode(), "ForStatement without test");
380
+ builder.terminateWithContinuation(
381
+ {
382
kind: "if",
383
test: lowerExpressionToPlace(builder, test),
386
- consequent: loopBlock,
384
+ consequent: bodyBlock,
385
alternate: continuationBlock.id,
386
fallthrough: continuationBlock.id,
387
id: makeInstructionId(0),
390
- };
391
- } else {
392
- /**
393
- * If there is no test, then the "conditional" block unconditionally re-enters the loop.
394
- * this will create an indirection, but `shrink()` will eliminate this in post-processing.
395
- */
396
- terminal = {
397
- kind: "goto",
398
- block: loopBlock,
399
- variant: GotoVariant.Break,
400
- id: makeInstructionId(0),
401
- };
402
- }
403
- builder.terminateWithContinuation(terminal, continuationBlock);
388
+ },
389
+ continuationBlock
390
+ );
391
return;
392
}
393
case "DoWhileStatement": {
@@ -1278,6 +1265,22 @@ function lowerExpressionToPlace(
1265
return place;
1266
}
1267
1268
+function lowerExpressionToVoid(
1269
+ builder: HIRBuilder,
1270
+ exprPath: NodePath<t.Expression>
1271
+): void {
1272
+ const instr = lowerExpression(builder, exprPath);
1273
+ if (instr.kind !== "Identifier") {
1274
+ const exprLoc = exprPath.node.loc ?? GeneratedSource;
1275
+ builder.push({
1276
+ id: makeInstructionId(0),
1277
+ value: instr,
1278
+ loc: exprLoc,
1279
+ lvalue: null,
1280
+ });
1281
+ }
1282
+}
1283
+
1284
function lowerLVal(builder: HIRBuilder, exprPath: NodePath<t.LVal>): Place {
1285
const exprNode = exprPath.node;
1286
const exprLoc = exprNode.loc ?? GeneratedSource;
compiler/forget/src/HIR/Codegen.ts
+8
@@ -190,6 +190,14 @@ class CodegenVisitor
190
case "while": {
191
return createWhileStatement(terminal.loc, terminal.test, terminal.loop);
192
}
193
+ case "for": {
194
+ return t.forStatement(
195
+ terminal.init as any, // TODO: make sure it's a variable declaration
196
+ terminal.test,
197
+ terminal.update,
198
+ terminal.loop
199
+ );
200
+ }
201
case "return": {
202
const createReturnStatement = withLoc(t.returnStatement);
203
if (terminal.value !== null) {
compiler/forget/src/HIR/HIR.ts
+11
@@ -109,6 +109,7 @@ export type Terminal =
109
| GotoTerminal
110
| IfTerminal
111
| SwitchTerminal
112
+ | ForTerminal
113
| WhileTerminal;
114
115
export type ThrowTerminal = { kind: "throw"; value: Place; id: InstructionId };
@@ -158,6 +159,16 @@ export type WhileTerminal = {
159
id: InstructionId;
160
};
161
162
+export type ForTerminal = {
163
+ kind: "for";
164
+ init: BlockId;
165
+ test: BlockId;
166
+ update: BlockId;
167
+ loop: BlockId;
168
+ fallthrough: BlockId;
169
+ id: InstructionId;
170
+};
171
+
172
/**
173
* Instructions generally represent expressions but with all nesting flattened away,
174
* such that all operands to each instruction are either primitive values OR are
compiler/forget/src/HIR/HIRBuilder.ts
+6
-1
@@ -365,7 +365,8 @@ function shrink(func: HIR): HIR {
365
if (target !== null) {
366
return target;
367
}
368
- const block = func.blocks.get(blockId)!;
368
+ const block = func.blocks.get(blockId);
369
+ invariant(block != null, "expected block %s to exist", blockId);
370
target = getTargetIfIndirection(block);
371
if (target !== null) {
372
// the target might also be a simple goto, recurse
@@ -485,6 +486,10 @@ function reversePostorderBlocks(func: HIR): HIR {
486
visit(terminal.test);
487
break;
488
}
489
+ case "for": {
490
+ visit(terminal.init);
491
+ break;
492
+ }
493
default: {
494
assertExhaustive(
495
terminal,
compiler/forget/src/HIR/HIRTreeVisitor.ts
+114
@@ -311,6 +311,113 @@ class Driver<TBlock, TValue, TItem, TCase> {
311
}
312
break;
313
}
314
+ case "for": {
315
+ const loopId =
316
+ !this.cx.isScheduled(terminal.loop) &&
317
+ terminal.loop !== terminal.fallthrough
318
+ ? terminal.loop
319
+ : null;
320
+
321
+ const fallthroughId =
322
+ terminal.fallthrough !== null &&
323
+ !this.cx.isScheduled(terminal.fallthrough)
324
+ ? terminal.fallthrough
325
+ : null;
326
+
327
+ const scheduleId = this.cx.scheduleLoop(
328
+ terminal.fallthrough,
329
+ terminal.update,
330
+ terminal.loop
331
+ );
332
+ scheduleIds.push(scheduleId);
333
+
334
+ const initBlock = this.cx.ir.blocks.get(terminal.init)!;
335
+ const initTerminal = initBlock.terminal;
336
+ invariant(
337
+ initTerminal.kind === "goto",
338
+ "Expected for loop init block to end in a goto"
339
+ );
340
+
341
+ let initItem;
342
+ for (const instr of initBlock.instructions) {
343
+ const value = this.visitor.visitValue(instr.value, instr.id);
344
+ initItem = this.visitor.visitInstruction(instr, value);
345
+ }
346
+
347
+ const testBlock = this.cx.ir.blocks.get(terminal.test)!;
348
+ const testTerminal = testBlock.terminal;
349
+ invariant(
350
+ testTerminal.kind === "if",
351
+ "Expected for loop test block to end in an if"
352
+ );
353
+ const testValueBlock = this.visitor.enterValueBlock();
354
+ for (const instr of testBlock.instructions) {
355
+ const value = this.visitor.visitValue(instr.value, instr.id);
356
+ const item = this.visitor.visitInstruction(instr, value);
357
+ this.visitor.appendBlock(testValueBlock, item);
358
+ }
359
+ const testValueLast = this.visitor.visitValue(
360
+ testTerminal.test,
361
+ testTerminal.id
362
+ );
363
+ const testValue = this.visitor.leaveValueBlock(
364
+ testValueBlock,
365
+ testValueLast
366
+ );
367
+
368
+ const updateBlock = this.cx.ir.blocks.get(terminal.update)!;
369
+ const updateTerminal = updateBlock.terminal;
370
+ invariant(
371
+ updateTerminal.kind === "goto",
372
+ "Expected for loop update block to end in a goto"
373
+ );
374
+ let updateValue;
375
+ for (const instr of updateBlock.instructions) {
376
+ updateValue = this.visitor.visitValue(instr.value, instr.id);
377
+ }
378
+
379
+ let loopBody: TItem;
380
+ if (loopId) {
381
+ loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
382
+ } else {
383
+ const break_ = this.visitBreak(terminal.loop);
384
+ invariant(
385
+ break_ !== null,
386
+ "If loop body is already scheduled it must be a break"
387
+ );
388
+ const body = this.visitor.enterBlock();
389
+ this.visitor.appendBlock(body, break_);
390
+ loopBody = this.visitor.leaveBlock(body);
391
+ }
392
+
393
+ this.cx.unscheduleAll(scheduleIds);
394
+ if (fallthroughId !== null) {
395
+ this.visitor.appendBlock(
396
+ blockValue,
397
+ this.visitor.visitTerminal({
398
+ kind: "for",
399
+ init: initItem as any,
400
+ test: testValue,
401
+ update: updateValue as any,
402
+ loop: loopBody,
403
+ }),
404
+ fallthroughId
405
+ );
406
+ this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
407
+ } else {
408
+ this.visitor.appendBlock(
409
+ blockValue,
410
+ this.visitor.visitTerminal({
411
+ kind: "for",
412
+ init: initItem as any,
413
+ test: testValue,
414
+ update: updateValue as any,
415
+ loop: loopBody,
416
+ })
417
+ );
418
+ }
419
+ break;
420
+ }
421
case "goto": {
422
this.visitor.visitTerminalId(terminal.id);
423
switch (terminal.variant) {
@@ -722,5 +829,12 @@ export type BlockTerminal<TBlock, TValue, TItem, TCase> =
829
test: TValue;
830
loop: TItem;
831
}
832
+ | {
833
+ kind: "for";
834
+ init: TItem;
835
+ test: TValue;
836
+ update: TValue;
837
+ loop: TItem;
838
+ }
839
| { kind: "break"; label: BlockId | null }
840
| { kind: "continue"; label: BlockId | null };
compiler/forget/src/HIR/InferReactiveScopeDependencies.ts
+1
@@ -102,6 +102,7 @@ class ScopeDependenciesVisitor
102
switch (terminal.kind) {
103
case "if":
104
case "switch":
105
+ case "for":
106
case "while": {
107
for (const operand of eachInstructionValueOperand(terminal.test)) {
108
this.#addTerminalDependency(operand);
compiler/forget/src/HIR/PrintHIR.ts
+5
@@ -82,6 +82,7 @@ export function printMixedHIR(
82
case "switch":
83
case "throw":
84
case "while":
85
+ case "for":
86
case "goto": {
87
const terminal = printTerminal(value);
88
if (Array.isArray(terminal)) {
@@ -165,6 +166,10 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
166
} fallthrough=${terminal.fallthrough ? `bb${terminal.fallthrough}` : ""}`;
167
break;
168
}
169
+ case "for": {
170
+ value = `[${terminal.id}] For init=bb${terminal.init} test=bb${terminal.test} loop=bb${terminal.loop} update=bb${terminal.update} fallthrough=bb${terminal.fallthrough}`;
171
+ break;
172
+ }
173
default: {
174
assertExhaustive(
175
terminal,
compiler/forget/src/HIR/PrintHIRTree.ts
+6
@@ -94,6 +94,12 @@ class PrintVisitor implements Visitor<Array<string>, string, string, string> {
94
value = `While (${terminal.test}) ${terminal.loop.trimStart()}`;
95
break;
96
}
97
+ case "for": {
98
+ value = `For (TODO) (${
99
+ terminal.test
100
+ }) (TODO) ${terminal.loop.trimStart()}`;
101
+ break;
102
+ }
103
case "return": {
104
if (terminal.value !== null) {
105
value = `Return ${terminal.value}`;
compiler/forget/src/HIR/VisualizeHIRMermaid.ts
+11
@@ -98,6 +98,9 @@ function printTerminalLabel(terminal: Terminal): string {
98
case "while":
99
buffer.push("While");
100
break;
101
+ case "for":
102
+ buffer.push("For");
103
+ break;
104
default:
105
assertExhaustive(terminal, `unhandled terminal ${terminal}`);
106
}
@@ -147,6 +150,14 @@ function printTerminalArrows(blockId: BlockId, terminal: Terminal): string {
150
buffer.push(printJumpArrow(blockId, terminal.fallthrough, "fallthrough"));
151
break;
152
}
153
+ case "for": {
154
+ buffer.push(printJumpArrow(blockId, terminal.init, "init"));
155
+ buffer.push(printJumpArrow(blockId, terminal.test, "test"));
156
+ buffer.push(printJumpArrow(blockId, terminal.update, "update"));
157
+ buffer.push(printJumpArrow(blockId, terminal.loop, "loop"));
158
+ buffer.push(printJumpArrow(blockId, terminal.fallthrough, "fallthrough"));
159
+ break;
160
+ }
161
case "throw":
162
case "return": {
163
break;
compiler/forget/src/HIR/visitors.ts
+22
@@ -214,6 +214,22 @@ export function mapTerminalSuccessors(
214
id: makeInstructionId(0),
215
};
216
}
217
+ case "for": {
218
+ const init = fn(terminal.init);
219
+ const test = fn(terminal.test);
220
+ const update = fn(terminal.update);
221
+ const loop = fn(terminal.loop);
222
+ const fallthrough = fn(terminal.fallthrough);
223
+ return {
224
+ kind: "for",
225
+ init,
226
+ test,
227
+ update,
228
+ loop,
229
+ fallthrough,
230
+ id: makeInstructionId(0),
231
+ };
232
+ }
233
default: {
234
assertExhaustive(
235
terminal,
@@ -255,6 +271,10 @@ export function* eachTerminalSuccessor(terminal: Terminal): Iterable<BlockId> {
271
yield terminal.test;
272
break;
273
}
274
+ case "for": {
275
+ yield terminal.init;
276
+ break;
277
+ }
278
default: {
279
assertExhaustive(
280
terminal,
@@ -291,6 +311,7 @@ export function mapTerminalOperands(
311
break;
312
}
313
case "while":
314
+ case "for":
315
case "goto": {
316
// no-op
317
break;
@@ -328,6 +349,7 @@ export function* eachTerminalOperand(terminal: Terminal): Iterable<Place> {
349
break;
350
}
351
case "while":
352
+ case "for":
353
case "goto": {
354
// no-op
355
break;
compiler/forget/src/__tests__/fixtures/hir/ssa-for.expect.md
+59
-27
@@ -3,9 +3,9 @@
3
4
```javascript
5
function foo() {
6
- let x = 0;
7
- for (; x < 10; ) {
8
- x = x + 1;
6
+ let x = 1;
7
+ for (let i = 0; i < 10; update()) {
8
+ x += 1;
9
}
10
return x;
11
}
@@ -16,23 +16,32 @@ function foo() {
16
17
```
18
bb0:
19
- [1] Let mutate x$5_@0[0:8] = 0
20
- [2] Goto bb1
19
+ [1] Let mutate x$7_@0[0:13] = 1
20
+ [2] For init=bb3 test=bb1 loop=bb5 update=bb4 fallthrough=bb2
21
+bb3:
22
+ predecessor blocks: bb0
23
+ [3] Let mutate i$8_@1[3:13] = 0
24
+ [4] Goto bb1
25
bb1:
22
- predecessor blocks: bb0 bb4
23
- x$7_@0[0:8]: phi(bb0: x$5_@0, bb4: x$10_@0)
24
- [3] Const mutate $6_@1 = 10
25
- [4] Const mutate $8_@2 = Binary read x$7_@0 < read $6_@1
26
- [5] If (read $8_@2) then:bb4 else:bb2
27
-bb4:
26
+ predecessor blocks: bb3 bb4
27
+ x$13_@0[0:13]: phi(bb3: x$7_@0, bb4: x$14_@0)
28
+ [5] Const mutate $9_@2 = 10
29
+ [6] Const mutate $11_@3[6:8] = Binary read i$8_@1 < read $9_@2
30
+ [7] If (read $11_@3) then:bb5 else:bb2
31
+bb5:
32
predecessor blocks: bb1
29
- [6] Const mutate $9_@3 = 1
30
- [7] Reassign mutate x$10_@0[0:8] = Binary read x$7_@0 + read $9_@3
31
- [8] Goto(Continue) bb1
33
+ [8] Const mutate $12_@4 = 1
34
+ [9] Reassign mutate x$14_@0[0:13] = Binary read x$13_@0 + read $12_@4
35
+ [10] Goto(Continue) bb4
36
+bb4:
37
+ predecessor blocks: bb5
38
+ [11] Call mutate update$3_@5()
39
+ [12] Goto bb1
40
bb2:
41
predecessor blocks: bb1
34
- [9] Return read x$7_@0
35
-
42
+ [13] Return read x$13_@0
43
+scope3 [6:8]:
44
+ - read $9_@2
45
```
46
47
### CFG
@@ -42,32 +51,50 @@ flowchart TB
51
%% Basic Blocks
52
subgraph bb0
53
bb0_instrs["
45
- [1] Let mutate x$5_@0[0:8] = 0
54
+ [1] Let mutate x$7_@0[0:13] = 1
55
+ "]
56
+ bb0_instrs --> bb0_terminal(["For"])
57
+ end
58
+ subgraph bb3
59
+ bb3_instrs["
60
+ [3] Let mutate i$8_@1[3:13] = 0
61
"]
47
- bb0_instrs --> bb0_terminal(["Goto"])
62
+ bb3_instrs --> bb3_terminal(["Goto"])
63
end
64
subgraph bb1
65
bb1_instrs["
51
- [3] Const mutate $6_@1 = 10
52
- [4] Const mutate $8_@2 = Binary read x$7_@0 < read $6_@1
66
+ [5] Const mutate $9_@2 = 10
67
+ [6] Const mutate $11_@3[6:8] = Binary read i$8_@1 < read $9_@2
68
"]
54
- bb1_instrs --> bb1_terminal(["If (read $8_@2)"])
69
+ bb1_instrs --> bb1_terminal(["If (read $11_@3)"])
70
+ end
71
+ subgraph bb5
72
+ bb5_instrs["
73
+ [8] Const mutate $12_@4 = 1
74
+ [9] Reassign mutate x$14_@0[0:13] = Binary read x$13_@0 + read $12_@4
75
+ "]
76
+ bb5_instrs --> bb5_terminal(["Goto"])
77
end
78
subgraph bb4
79
bb4_instrs["
58
- [6] Const mutate $9_@3 = 1
59
- [7] Reassign mutate x$10_@0[0:8] = Binary read x$7_@0 + read $9_@3
80
+ [11] Call mutate update$3_@5()
81
"]
82
bb4_instrs --> bb4_terminal(["Goto"])
83
end
84
subgraph bb2
64
- bb2_terminal(["Return read x$7_@0"])
85
+ bb2_terminal(["Return read x$13_@0"])
86
end
87
88
%% Jumps
68
- bb0_terminal --> bb1
69
- bb1_terminal -- "then" --> bb4
89
+ bb0_terminal -- "init" --> bb3
90
+ bb0_terminal -- "test" --> bb1
91
+ bb0_terminal -- "update" --> bb4
92
+ bb0_terminal -- "loop" --> bb5
93
+ bb0_terminal -- "fallthrough" --> bb2
94
+ bb3_terminal --> bb1
95
+ bb1_terminal -- "then" --> bb5
96
bb1_terminal -- "else" --> bb2
97
+ bb5_terminal --> bb4
98
bb4_terminal --> bb1
99
100
```
@@ -76,7 +103,12 @@ flowchart TB
103
104
```javascript
105
function foo$0() {
79
- let x$1 = 0;
106
+ let x$1 = 1;
107
+ bb2: for (let i$2 = 0; i$2 < 10; update$3()) {
108
+ x$1 = x$1 + 1;
109
+ }
110
+
111
+ return x$1;
112
}
113
114
```
compiler/forget/src/__tests__/fixtures/hir/ssa-for.js
+3
-3
@@ -1,7 +1,7 @@
1
function foo() {
2
- let x = 0;
3
- for (; x < 10; ) {
4
- x = x + 1;
2
+ let x = 1;
3
+ for (let i = 0; i < 10; update()) {
4
+ x += 1;
5
}
6
return x;
7
}