@samitouri / QOS-React-2 / commits / 4a70aa7faf

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 }