Make implicit break/continue explicit in ReactiveFunction
Previously when converting from HIR -> ReactiveFunction we elided break/continue terminals in places where control would implicitly transfer to the break/continue target and therefore nothing has to be emitted. The one downside of this approach is that it makes scope analysis a bit trickier. We want to close scopes once we see an instruction id past the end of the scope's range, but these implicit breaks were causing us to miss some instruction ids. We compensated for this, but it's helpful to keep the representation explicit and discard these terminals later in codegen.
Joe Savona committed
Jan 17, 2023 at 09:40 UTC
4a70aa7faf82bc20e09bca99b230c50a181378b3
11 files changed
+147
-121
compiler/forget/src/HIR/HIR.ts
+76
-36
@@ -68,10 +68,23 @@ export type ReactiveValueBlock = {
68
};
69
70
export type ReactiveStatement =
71
- | { kind: "instruction"; instruction: ReactiveInstruction }
72
- | { kind: "terminal"; terminal: ReactiveTerminal; label: BlockId | null }
71
+ | ReactiveInstructionStatement
72
+ | ReactiveTerminalStatement
73
| ReactiveScopeBlock;
74
75
+export type ReactiveInstructionStatement = {
76
+ kind: "instruction";
77
+ instruction: ReactiveInstruction;
78
+};
79
+
80
+export type ReactiveTerminalStatement<
81
+ Tterminal extends ReactiveTerminal = ReactiveTerminal
82
+> = {
83
+ kind: "terminal";
84
+ terminal: Tterminal;
85
+ label: BlockId | null;
86
+};
87
+
88
export type ReactiveInstruction = {
89
id: InstructionId;
90
lvalue: LValue | null;
@@ -80,40 +93,67 @@ export type ReactiveInstruction = {
93
};
94
95
export type ReactiveTerminal =
83
- | { kind: "break"; label: BlockId | null; id: InstructionId | null }
84
- | { kind: "continue"; label: BlockId | null; id: InstructionId }
85
- | { kind: "return"; value: Place | null; id: InstructionId }
86
- | { kind: "throw"; value: Place; id: InstructionId }
87
- | {
88
- kind: "switch";
89
- test: Place;
90
- cases: Array<{
91
- test: Place | null;
92
- block: ReactiveBlock | void;
93
- }>;
94
- id: InstructionId;
95
- }
96
- | {
97
- kind: "while";
98
- test: ReactiveValueBlock;
99
- loop: ReactiveBlock;
100
- id: InstructionId;
101
- }
102
- | {
103
- kind: "for";
104
- init: ReactiveValueBlock;
105
- test: ReactiveValueBlock;
106
- update: ReactiveValueBlock;
107
- loop: ReactiveBlock;
108
- id: InstructionId;
109
- }
110
- | {
111
- kind: "if";
112
- test: Place;
113
- consequent: ReactiveBlock;
114
- alternate: ReactiveBlock | null;
115
- id: InstructionId;
116
- };
96
+ | ReactiveBreakTerminal
97
+ | ReactiveContinueTerminal
98
+ | ReactiveReturnTerminal
99
+ | ReactiveThrowTerminal
100
+ | ReactiveSwitchTerminal
101
+ | ReactiveWhileTerminal
102
+ | ReactiveForTerminal
103
+ | ReactiveIfTerminal;
104
+
105
+export type ReactiveBreakTerminal = {
106
+ kind: "break";
107
+ label: BlockId | null;
108
+ id: InstructionId | null;
109
+ implicit: boolean;
110
+};
111
+export type ReactiveContinueTerminal = {
112
+ kind: "continue";
113
+ label: BlockId | null;
114
+ id: InstructionId;
115
+ implicit: boolean;
116
+};
117
+export type ReactiveReturnTerminal = {
118
+ kind: "return";
119
+ value: Place | null;
120
+ id: InstructionId;
121
+};
122
+export type ReactiveThrowTerminal = {
123
+ kind: "throw";
124
+ value: Place;
125
+ id: InstructionId;
126
+};
127
+export type ReactiveSwitchTerminal = {
128
+ kind: "switch";
129
+ test: Place;
130
+ cases: Array<{
131
+ test: Place | null;
132
+ block: ReactiveBlock | void;
133
+ }>;
134
+ id: InstructionId;
135
+};
136
+export type ReactiveWhileTerminal = {
137
+ kind: "while";
138
+ test: ReactiveValueBlock;
139
+ loop: ReactiveBlock;
140
+ id: InstructionId;
141
+};
142
+export type ReactiveForTerminal = {
143
+ kind: "for";
144
+ init: ReactiveValueBlock;
145
+ test: ReactiveValueBlock;
146
+ update: ReactiveValueBlock;
147
+ loop: ReactiveBlock;
148
+ id: InstructionId;
149
+};
150
+export type ReactiveIfTerminal = {
151
+ kind: "if";
152
+ test: Place;
153
+ consequent: ReactiveBlock;
154
+ alternate: ReactiveBlock | null;
155
+ id: InstructionId;
156
+};
157
158
/**
159
* A function lowered to HIR form, ie where its body is lowered to an HIR control-flow graph
compiler/forget/src/ReactiveScopes/BuildReactiveFunction.ts
+36
-13
@@ -15,10 +15,15 @@ import {
15
InstructionValue,
16
Place,
17
ReactiveBlock,
18
- ReactiveStatement,
18
ReactiveValueBlock,
19
} from "../HIR";
21
-import { HIRFunction, ReactiveFunction } from "../HIR/HIR";
20
+import {
21
+ HIRFunction,
22
+ ReactiveBreakTerminal,
23
+ ReactiveContinueTerminal,
24
+ ReactiveFunction,
25
+ ReactiveTerminalStatement,
26
+} from "../HIR/HIR";
27
import todo from "../Utils/todo";
28
import { assertExhaustive } from "../Utils/utils";
29
@@ -180,7 +185,7 @@ class Driver {
185
const break_ = this.visitBreak(case_.block, null);
186
if (
187
index === 0 &&
183
- break_ === null &&
188
+ break_.terminal.implicit &&
189
case_.block === terminal.fallthrough &&
190
case_.test === null
191
) {
@@ -383,6 +388,9 @@ class Driver {
388
}
389
break;
390
}
391
+ case "error": {
392
+ invariant(false, "Unexpected error terminal");
393
+ }
394
default: {
395
assertExhaustive(terminal, "Unexpected terminal");
396
}
@@ -434,27 +442,30 @@ class Driver {
442
visitBreak(
443
block: BlockId,
444
id: InstructionId | null
437
- ): ReactiveStatement | null {
445
+ ): ReactiveTerminalStatement<ReactiveBreakTerminal> {
446
const target = this.cx.getBreakTarget(block);
447
if (target === null) {
440
- // TODO: we should always have a target
441
- return null;
448
+ invariant(false, "Expected a break target");
449
}
450
switch (target.type) {
451
case "implicit": {
445
- return null;
452
+ return {
453
+ kind: "terminal",
454
+ terminal: { kind: "break", label: null, id, implicit: true },
455
+ label: null,
456
+ };
457
}
458
case "labeled": {
459
return {
460
kind: "terminal",
450
- terminal: { kind: "break", label: target.block, id },
461
+ terminal: { kind: "break", label: target.block, id, implicit: false },
462
label: null,
463
};
464
}
465
case "unlabeled": {
466
return {
467
kind: "terminal",
457
- terminal: { kind: "break", label: null, id },
468
+ terminal: { kind: "break", label: null, id, implicit: false },
469
label: null,
470
};
471
}
@@ -467,7 +478,10 @@ class Driver {
478
}
479
}
480
470
- visitContinue(block: BlockId, id: InstructionId): ReactiveStatement | null {
481
+ visitContinue(
482
+ block: BlockId,
483
+ id: InstructionId
484
+ ): ReactiveTerminalStatement<ReactiveContinueTerminal> {
485
const target = this.cx.getContinueTarget(block);
486
invariant(
487
target !== null,
@@ -475,19 +489,28 @@ class Driver {
489
);
490
switch (target.type) {
491
case "implicit": {
478
- return null;
492
+ return {
493
+ kind: "terminal",
494
+ terminal: { kind: "continue", label: null, id, implicit: true },
495
+ label: null,
496
+ };
497
}
498
case "labeled": {
499
return {
500
kind: "terminal",
483
- terminal: { kind: "continue", label: target.block, id },
501
+ terminal: {
502
+ kind: "continue",
503
+ label: target.block,
504
+ id,
505
+ implicit: false,
506
+ },
507
label: null,
508
};
509
}
510
case "unlabeled": {
511
return {
512
kind: "terminal",
490
- terminal: { kind: "continue", label: null, id },
513
+ terminal: { kind: "continue", label: null, id, implicit: false },
514
label: null,
515
};
516
}
compiler/forget/src/ReactiveScopes/CodegenReactiveFunction.ts
+13
-1
@@ -104,6 +104,9 @@ function codegenBlock(cx: Context, block: ReactiveBlock): t.BlockStatement {
104
}
105
case "terminal": {
106
const statement = codegenTerminal(cx, item.terminal);
107
+ if (statement === null) {
108
+ break;
109
+ }
110
if (item.label !== null) {
111
statements.push(
112
t.labeledStatement(
@@ -232,9 +235,15 @@ function codegenReactiveScope(
235
statements.push(t.ifStatement(testCondition, computationBlock, memoBlock));
236
}
237
235
-function codegenTerminal(cx: Context, terminal: ReactiveTerminal): t.Statement {
238
+function codegenTerminal(
239
+ cx: Context,
240
+ terminal: ReactiveTerminal
241
+): t.Statement | null {
242
switch (terminal.kind) {
243
case "break": {
244
+ if (terminal.implicit) {
245
+ return null;
246
+ }
247
return t.breakStatement(
248
terminal.label !== null
249
? t.identifier(codegenLabel(terminal.label))
@@ -242,6 +251,9 @@ function codegenTerminal(cx: Context, terminal: ReactiveTerminal): t.Statement {
251
);
252
}
253
case "continue": {
254
+ if (terminal.implicit) {
255
+ return null;
256
+ }
257
return t.continueStatement(
258
terminal.label !== null
259
? t.identifier(codegenLabel(terminal.label))
compiler/forget/src/__tests__/fixtures/hir/conditional-break.expect.md
-31
@@ -56,22 +56,6 @@ function Component(props) {
56
return a;
57
}
58
59
-/**
60
- * props.b *does* influence `a`
61
- */
62
-function Component(props) {
63
- const a = [];
64
- a.push(props.a);
65
- label: {
66
- if (props.b) {
67
- break label;
68
- }
69
- a.push(props.c);
70
- }
71
- a.push(props.d);
72
- return a;
73
-}
74
-
59
```
60
61
## Code
@@ -202,19 +186,4 @@ function Component(props) {
186
}
187
188
```
205
-## Code
206
-
207
-```javascript
208
-function Component(props) {
209
- const a = [];
210
- a.push(props.a);
211
- if (props.b) {
212
- a.push(props.d);
213
- return a;
214
- }
215
-
216
- a.push(props.c);
217
-}
218
-
219
-```
189
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/conditional-break.js
-16
@@ -51,19 +51,3 @@ function Component(props) {
51
a.push(props.d);
52
return a;
53
}
54
-
55
-/**
56
- * props.b *does* influence `a`
57
- */
58
-function Component(props) {
59
- const a = [];
60
- a.push(props.a);
61
- label: {
62
- if (props.b) {
63
- break label;
64
- }
65
- a.push(props.c);
66
- }
67
- a.push(props.d);
68
- return a;
69
-}
compiler/forget/src/__tests__/fixtures/hir/error.conditional-break-labeled.expect.md
renamed
+4
-12
@@ -20,19 +20,11 @@ function Component(props) {
20
21
```
22
23
-## Code
23
25
-```javascript
26
-function Component(props) {
27
- const a = [];
28
- a.push(props.a);
29
- if (props.b) {
30
- a.push(props.d);
31
- return a;
32
- }
33
-
34
- a.push(props.c);
35
-}
24
+## Error
25
26
```
27
+Expected a break target
28
+```
29
+
30
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/error.conditional-break-labeled.js
renamed
compiler/forget/src/__tests__/fixtures/hir/error.ssa-for-of.expect.md
renamed
+4
-5
@@ -15,12 +15,11 @@ function foo(cond) {
15
16
```
17
18
-## Code
18
20
-```javascript
21
-function foo(cond) {
22
- const items = [];
23
-}
19
+## Error
20
21
```
22
+Expected a break target
23
+```
24
+
25
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/error.ssa-for-of.js
renamed
compiler/forget/src/__tests__/fixtures/hir/inverted-if.expect.md
+12
-6
@@ -2,13 +2,14 @@
2
## Input
3
4
```javascript
5
-function foo(a, b, c) {
5
+function foo(a, b, c, d) {
6
let y = [];
7
label: if (a) {
8
if (b) {
9
y.push(c);
10
break label;
11
}
12
+ y.push(d);
13
}
14
return y;
15
}
@@ -18,27 +19,32 @@ function foo(a, b, c) {
19
## Code
20
21
```javascript
21
-function foo(a, b, c) {
22
+function foo(a, b, c, d) {
23
const $ = React.useMemoCache();
24
const c_0 = $[0] !== a;
25
const c_1 = $[1] !== b;
26
const c_2 = $[2] !== c;
27
+ const c_3 = $[3] !== d;
28
let y;
27
- if (c_0 || c_1 || c_2) {
29
+ if (c_0 || c_1 || c_2 || c_3) {
30
y = [];
31
30
- if (a) {
32
+ bb1: if (a) {
33
if (b) {
34
y.push(c);
35
+ break bb1;
36
}
37
+
38
+ y.push(d);
39
}
40
41
$[0] = a;
42
$[1] = b;
43
$[2] = c;
39
- $[3] = y;
44
+ $[3] = d;
45
+ $[4] = y;
46
} else {
41
- y = $[3];
47
+ y = $[4];
48
}
49
50
return y;
compiler/forget/src/__tests__/fixtures/hir/inverted-if.js
+2
-1
@@ -1,10 +1,11 @@
1
-function foo(a, b, c) {
1
+function foo(a, b, c, d) {
2
let y = [];
3
label: if (a) {
4
if (b) {
5
y.push(c);
6
break label;
7
}
8
+ y.push(d);
9
}
10
return y;
11
}