@samitouri / QOS-React-2 / commits / d243e748d0

Build -> Codegen for LabelTerminal

NOTE: See background in #1476. Updates BuildHIR to use the new LabelTerminal for LabeledStatements, and adds support for HIR->ReactiveFunction transformation and codegen. Note that we sometimes produce an extraneous block wrapper if it turns out the label wasn't necessary, that seems...fine?

Joe Savona committed Apr 5, 2023 at 16:26 UTC d243e748d00773c1eb9dbdc4eceed019323fdc9d
18 files changed +167 -55
compiler/forget/src/HIR/BuildHIR.ts
+14 -5
@@ -450,15 +450,24 @@ function lowerStatement(
450 // All other statements create a continuation block to allow `break`,
451 // explicitly *don't* pass the label down
452 const continuationBlock = builder.reserve("block");
453 - builder.label(label, continuationBlock.id, () => {
454 - lowerStatement(builder, stmt.get("body"));
455 - });
456 - builder.terminateWithContinuation(
457 - {
453 + const block = builder.enter("block", () => {
454 + builder.label(label, continuationBlock.id, () => {
455 + lowerStatement(builder, stmt.get("body"));
456 + });
457 + return {
458 kind: "goto",
459 block: continuationBlock.id,
460 variant: GotoVariant.Break,
461 id: makeInstructionId(0),
462 + };
463 + });
464 + builder.terminateWithContinuation(
465 + {
466 + kind: "label",
467 + block,
468 + fallthrough: continuationBlock.id,
469 + id: makeInstructionId(0),
470 + loc: stmt.node.loc ?? GeneratedSource,
471 },
472 continuationBlock
473 );
compiler/forget/src/HIR/HIR.ts
+7 -1
@@ -135,7 +135,8 @@ export type ReactiveTerminal =
135 | ReactiveWhileTerminal
136 | ReactiveForTerminal
137 | ReactiveForOfTerminal
138 - | ReactiveIfTerminal;
138 + | ReactiveIfTerminal
139 + | ReactiveLabelTerminal;
140
141 export type ReactiveBreakTerminal = {
142 kind: "break";
@@ -201,6 +202,11 @@ export type ReactiveIfTerminal = {
202 alternate: ReactiveBlock | null;
203 id: InstructionId;
204 };
205 +export type ReactiveLabelTerminal = {
206 + kind: "label";
207 + block: ReactiveBlock;
208 + id: InstructionId;
209 +};
210
211 /**
212 * A function lowered to HIR form, ie where its body is lowered to an HIR control-flow graph
compiler/forget/src/HIR/HIRBuilder.ts
+2 -1
@@ -541,7 +541,8 @@ export function removeUnreachableFallthroughs(func: HIR): void {
541 if (
542 block.terminal.kind === "if" ||
543 block.terminal.kind === "switch" ||
544 - block.terminal.kind === "while"
544 + block.terminal.kind === "while" ||
545 + block.terminal.kind === "label"
546 ) {
547 if (
548 block.terminal.fallthrough !== null &&
compiler/forget/src/ReactiveScopes/BuildReactiveFunction.ts
+29 -2
@@ -29,7 +29,6 @@ import {
29 ReactiveValue,
30 Terminal,
31 } from "../HIR/HIR";
32 -import todo from "../Utils/todo";
32 import { assertExhaustive } from "../Utils/utils";
33
34 /**
@@ -531,7 +530,35 @@ class Driver {
530 break;
531 }
532 case "label": {
534 - todo("Support label terminals");
533 + const fallthroughId =
534 + terminal.fallthrough !== null &&
535 + !this.cx.isScheduled(terminal.fallthrough)
536 + ? terminal.fallthrough
537 + : null;
538 + if (fallthroughId !== null) {
539 + const scheduleId = this.cx.schedule(fallthroughId, "if");
540 + scheduleIds.push(scheduleId);
541 + }
542 +
543 + const block = this.traverseBlock(
544 + this.cx.ir.blocks.get(terminal.block)!
545 + );
546 +
547 + this.cx.unscheduleAll(scheduleIds);
548 + blockValue.push({
549 + kind: "terminal",
550 + terminal: {
551 + kind: "label",
552 + block,
553 + id: terminal.id,
554 + },
555 + label: fallthroughId,
556 + });
557 + if (fallthroughId !== null) {
558 + this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
559 + }
560 +
561 + break;
562 }
563 case "optional-call":
564 case "ternary":
compiler/forget/src/ReactiveScopes/CodegenReactiveFunction.ts
+3
@@ -409,6 +409,9 @@ function codegenTerminal(
409 const test = codegenInstructionValue(cx, terminal.test);
410 return t.whileStatement(test, codegenBlock(cx, terminal.loop));
411 }
412 + case "label": {
413 + return codegenBlock(cx, terminal.block);
414 + }
415 default: {
416 assertExhaustive(
417 terminal,
compiler/forget/src/ReactiveScopes/FlattenReactiveLoops.ts
+1
@@ -54,6 +54,7 @@ class Transform extends ReactiveFunctionTransform<boolean> {
54 break;
55 }
56 // Non-loop terminals passthrough is contextual, inherits the parent isWithinScope
57 + case "label":
58 case "break":
59 case "continue":
60 case "if":
compiler/forget/src/ReactiveScopes/PrintReactiveFunction.ts
+6
@@ -258,6 +258,12 @@ function printTerminal(writer: Writer, terminal: ReactiveTerminal): void {
258 }
259 break;
260 }
261 + case "label": {
262 + writer.writeLine("{");
263 + printReactiveInstructions(writer, terminal.block);
264 + writer.writeLine("}");
265 + break;
266 + }
267 default:
268 assertExhaustive(terminal, `Unhandled terminal ${terminal}`);
269 }
compiler/forget/src/ReactiveScopes/PropagateScopeDependencies.ts
+4
@@ -443,6 +443,10 @@ function visit(context: Context, block: ReactiveBlock): void {
443 }
444 break;
445 }
446 + case "label": {
447 + visit(context, terminal.block);
448 + break;
449 + }
450 default: {
451 assertExhaustive(
452 terminal,
compiler/forget/src/ReactiveScopes/visitors.ts
+8
@@ -149,6 +149,10 @@ export class ReactiveFunctionVisitor<TState = void> {
149 }
150 break;
151 }
152 + case "label": {
153 + this.visitBlock(terminal.block, state);
154 + break;
155 + }
156 default: {
157 assertExhaustive(
158 terminal,
@@ -355,6 +359,10 @@ export function mapTerminalBlocks(
359 }
360 break;
361 }
362 + case "label": {
363 + terminal.block = fn(terminal.block);
364 + break;
365 + }
366 default: {
367 assertExhaustive(
368 terminal,
compiler/forget/src/SSA/LeaveSSA.ts
+2 -1
@@ -296,7 +296,8 @@ export function leaveSSA(fn: HIRFunction): void {
296 terminal.kind === "while" ||
297 terminal.kind === "do-while" ||
298 terminal.kind === "for" ||
299 - terminal.kind === "for-of") &&
299 + terminal.kind === "for-of" ||
300 + terminal.kind === "label") &&
301 terminal.fallthrough !== null
302 ) {
303 const fallthrough = fn.body.blocks.get(terminal.fallthrough)!;
compiler/forget/src/__tests__/fixtures/compiler/complex-while.expect.md
+6 -4
@@ -19,10 +19,12 @@ function foo(a, b, c) {
19
20 ```javascript
21 function foo(a, b, c) {
22 - if (a) {
23 - while (b) {
24 - if (c) {
25 - break;
22 + {
23 + if (a) {
24 + while (b) {
25 + if (c) {
26 + break;
27 + }
28 }
29 }
30 }
compiler/forget/src/__tests__/fixtures/compiler/conditional-break-labeled.expect.md new
+60
@@ -0,0 +1,60 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +/**
6 + * props.b *does* influence `a`
7 + */
8 +function Component(props) {
9 + const a = [];
10 + a.push(props.a);
11 + label: {
12 + if (props.b) {
13 + break label;
14 + }
15 + a.push(props.c);
16 + }
17 + a.push(props.d);
18 + return a;
19 +}
20 +
21 +```
22 +
23 +## Code
24 +
25 +```javascript
26 +/**
27 + * props.b *does* influence `a`
28 + */
29 +function Component(props) {
30 + const $ = React.unstable_useMemoCache(5);
31 + const c_0 = $[0] !== props.a;
32 + const c_1 = $[1] !== props.b;
33 + const c_2 = $[2] !== props.c;
34 + const c_3 = $[3] !== props.d;
35 + let a;
36 + if (c_0 || c_1 || c_2 || c_3) {
37 + a = [];
38 + a.push(props.a);
39 + bb1: {
40 + if (props.b) {
41 + break bb1;
42 + }
43 +
44 + a.push(props.c);
45 + }
46 +
47 + a.push(props.d);
48 + $[0] = props.a;
49 + $[1] = props.b;
50 + $[2] = props.c;
51 + $[3] = props.d;
52 + $[4] = a;
53 + } else {
54 + a = $[4];
55 + }
56 + return a;
57 +}
58 +
59 +```
60 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/conditional-break-labeled.js renamed
compiler/forget/src/__tests__/fixtures/compiler/error.conditional-break-labeled.expect.md deleted
-30
@@ -1,30 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -/**
6 - * props.b *does* influence `a`
7 - */
8 -function Component(props) {
9 - const a = [];
10 - a.push(props.a);
11 - label: {
12 - if (props.b) {
13 - break label;
14 - }
15 - a.push(props.c);
16 - }
17 - a.push(props.d);
18 - return a;
19 -}
20 -
21 -```
22 -
23 -
24 -## Error
25 -
26 -```
27 -Expected a break target
28 -```
29 -
30 -
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/inverted-if-else.expect.md renamed
+14 -4
@@ -16,11 +16,21 @@ function foo(a, b, c) {
16
17 ```
18
19 +## Code
20
20 -## Error
21 +```javascript
22 +function foo(a, b, c) {
23 + let x = undefined;
24 + bb1: {
25 + if (a) {
26 + x = b;
27 + break bb1;
28 + }
29 +
30 + x = c;
31 + }
32 + return x;
33 +}
34
35 ```
23 -Expected a break target
24 -```
25 -
36
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/inverted-if-else.js renamed
compiler/forget/src/__tests__/fixtures/compiler/inverted-if.expect.md
+8 -6
@@ -28,13 +28,15 @@ function foo(a, b, c, d) {
28 let y;
29 if (c_0 || c_1 || c_2 || c_3) {
30 y = [];
31 - bb1: if (a) {
32 - if (b) {
33 - y.push(c);
34 - break bb1;
35 - }
31 + bb1: {
32 + if (a) {
33 + if (b) {
34 + y.push(c);
35 + break bb1;
36 + }
37
37 - y.push(d);
38 + y.push(d);
39 + }
40 }
41 $[0] = a;
42 $[1] = b;
compiler/forget/src/__tests__/fixtures/compiler/unconditional-break-label.expect.md
+3 -1
@@ -17,7 +17,9 @@ function foo(a) {
17
18 ```javascript
19 function foo(a) {
20 - return a + 1;
20 + {
21 + return a + 1;
22 + }
23 }
24
25 ```