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
```