@samitouri / QOS-React-2 / commits / 4837e21de3

[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 }