@samitouri / QOS-React / commits / 2782fa2664

[rhir] Preserve block labels + target blockIds for break/continue terminals

RFC: we can either retain break/continue target ids (instead of pruning them in `buildReactiveFunction`) or re-implement the same logic in `propagateScopeDeps` (ignore implicit breaks; match unlabeled break / continues to their closest loop / while parent terminal). If we go ahead with this approach, I'll clean up this PR (add relevant types and comments)

Mofei Zhang committed Mar 27, 2024 at 20:26 UTC 2782fa2664784d7c3c941aa708349e570418103d
7 files changed +120 -116
compiler/packages/babel-plugin-react-forget/src/HIR/HIR.ts
+9 -5
@@ -80,7 +80,10 @@ export type ReactiveTerminalStatement<
80 > = {
81 kind: "terminal";
82 terminal: Tterminal;
83 - label: BlockId | null;
83 + label: {
84 + id: BlockId;
85 + implicit: boolean;
86 + } | null;
87 };
88
89 export type ReactiveInstruction = {
@@ -153,17 +156,18 @@ export type ReactiveTerminal =
156 | ReactiveLabelTerminal
157 | ReactiveTryTerminal;
158
159 +export type ReactiveTerminalTargetKind = "implicit" | "labeled" | "unlabeled";
160 export type ReactiveBreakTerminal = {
161 kind: "break";
158 - label: BlockId | null;
162 + target: BlockId;
163 id: InstructionId | null;
160 - implicit: boolean;
164 + targetKind: ReactiveTerminalTargetKind;
165 };
166 export type ReactiveContinueTerminal = {
167 kind: "continue";
164 - label: BlockId | null;
168 + target: BlockId;
169 id: InstructionId;
166 - implicit: boolean;
170 + targetKind: ReactiveTerminalTargetKind;
171 };
172 export type ReactiveReturnTerminal = {
173 kind: "return";
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/BuildReactiveBlocks.ts
+8 -2
@@ -61,7 +61,10 @@ class Context {
61 this.#builders.at(-1)!.startScope(scope);
62 }
63
64 - append(stmt: ReactiveStatement, label: BlockId | null): void {
64 + append(
65 + stmt: ReactiveStatement,
66 + label: { id: BlockId; implicit: boolean } | null
67 + ): void {
68 this.#builders.at(-1)!.append(stmt, label);
69 }
70
@@ -93,7 +96,10 @@ class Builder {
96 this.#stack = [{ kind: "block", block }];
97 }
98
96 - append(item: ReactiveStatement, label: BlockId | null): void {
99 + append(
100 + item: ReactiveStatement,
101 + label: { id: BlockId; implicit: boolean } | null
102 + ): void {
103 if (label !== null) {
104 CompilerError.invariant(item.kind === "terminal", {
105 reason: "Only terminals may have a label",
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/BuildReactiveFunction.ts
+75 -79
@@ -24,6 +24,7 @@ import {
24 ReactiveLogicalValue,
25 ReactiveSequenceValue,
26 ReactiveTerminalStatement,
27 + ReactiveTerminalTargetKind,
28 ReactiveTernaryValue,
29 ReactiveValue,
30 Terminal,
@@ -157,7 +158,13 @@ class Driver {
158 alternate: alternate,
159 id: terminal.id,
160 },
160 - label: fallthroughId,
161 + label:
162 + fallthroughId == null
163 + ? null
164 + : {
165 + id: fallthroughId,
166 + implicit: false,
167 + },
168 });
169 if (fallthroughId !== null) {
170 this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
@@ -193,7 +200,7 @@ class Driver {
200 const break_ = this.visitBreak(case_.block, null);
201 if (
202 index === 0 &&
196 - break_.terminal.implicit &&
203 + break_.terminal.targetKind === "implicit" &&
204 case_.block === terminal.fallthrough &&
205 case_.test === null
206 ) {
@@ -229,7 +236,13 @@ class Driver {
236 cases,
237 id: terminal.id,
238 },
232 - label: fallthroughId,
239 + label:
240 + fallthroughId == null
241 + ? null
242 + : {
243 + id: fallthroughId,
244 + implicit: false,
245 + },
246 });
247 if (fallthroughId !== null) {
248 this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
@@ -280,7 +293,13 @@ class Driver {
293 loop: loopBody,
294 id: terminal.id,
295 },
283 - label: fallthroughId,
296 + label:
297 + fallthroughId == null
298 + ? null
299 + : {
300 + id: fallthroughId,
301 + implicit: false,
302 + },
303 });
304 if (fallthroughId !== null) {
305 this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
@@ -334,7 +353,13 @@ class Driver {
353 loop: loopBody,
354 id: terminal.id,
355 },
337 - label: fallthroughId,
356 + label:
357 + fallthroughId == null
358 + ? null
359 + : {
360 + id: fallthroughId,
361 + implicit: false,
362 + },
363 });
364 if (fallthroughId !== null) {
365 this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
@@ -421,7 +446,10 @@ class Driver {
446 loop: loopBody,
447 id: terminal.id,
448 },
424 - label: fallthroughId,
449 + label:
450 + fallthroughId == null
451 + ? null
452 + : { id: fallthroughId, implicit: false },
453 });
454 if (fallthroughId !== null) {
455 this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
@@ -496,7 +524,10 @@ class Driver {
524 loop: loopBody,
525 id: terminal.id,
526 },
499 - label: fallthroughId,
527 + label:
528 + fallthroughId == null
529 + ? null
530 + : { id: fallthroughId, implicit: false },
531 });
532 if (fallthroughId !== null) {
533 this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
@@ -571,7 +602,10 @@ class Driver {
602 loop: loopBody,
603 id: terminal.id,
604 },
574 - label: fallthroughId,
605 + label:
606 + fallthroughId == null
607 + ? null
608 + : { id: fallthroughId, implicit: false },
609 });
610 if (fallthroughId !== null) {
611 this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
@@ -651,7 +685,10 @@ class Driver {
685 block,
686 id: terminal.id,
687 },
654 - label: fallthroughId,
688 + label:
689 + fallthroughId == null
690 + ? null
691 + : { id: fallthroughId, implicit: false },
692 });
693 if (fallthroughId !== null) {
694 this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
@@ -753,7 +790,10 @@ class Driver {
790 this.cx.unscheduleAll(scheduleIds);
791 blockValue.push({
792 kind: "terminal",
756 - label: fallthroughId,
793 + label:
794 + fallthroughId == null
795 + ? null
796 + : { id: fallthroughId, implicit: false },
797 terminal: {
798 kind: "try",
799 block,
@@ -1105,35 +1145,16 @@ class Driver {
1145 suggestions: null,
1146 });
1147 }
1108 - switch (target.type) {
1109 - case "implicit": {
1110 - return {
1111 - kind: "terminal",
1112 - terminal: { kind: "break", label: null, id, implicit: true },
1113 - label: null,
1114 - };
1115 - }
1116 - case "labeled": {
1117 - return {
1118 - kind: "terminal",
1119 - terminal: { kind: "break", label: target.block, id, implicit: false },
1120 - label: null,
1121 - };
1122 - }
1123 - case "unlabeled": {
1124 - return {
1125 - kind: "terminal",
1126 - terminal: { kind: "break", label: null, id, implicit: false },
1127 - label: null,
1128 - };
1129 - }
1130 - default: {
1131 - assertExhaustive(
1132 - target.type,
1133 - `Unexpected break target kind '${(target as any).type}'`
1134 - );
1135 - }
1136 - }
1148 + return {
1149 + kind: "terminal",
1150 + terminal: {
1151 + kind: "break",
1152 + target: target.block,
1153 + id,
1154 + targetKind: target.type,
1155 + },
1156 + label: null,
1157 + };
1158 }
1159
1160 visitContinue(
@@ -1147,40 +1168,17 @@ class Driver {
1168 loc: null,
1169 suggestions: null,
1170 });
1150 - switch (target.type) {
1151 - case "implicit": {
1152 - return {
1153 - kind: "terminal",
1154 - terminal: { kind: "continue", label: null, id, implicit: true },
1155 - label: null,
1156 - };
1157 - }
1158 - case "labeled": {
1159 - return {
1160 - kind: "terminal",
1161 - terminal: {
1162 - kind: "continue",
1163 - label: target.block,
1164 - id,
1165 - implicit: false,
1166 - },
1167 - label: null,
1168 - };
1169 - }
1170 - case "unlabeled": {
1171 - return {
1172 - kind: "terminal",
1173 - terminal: { kind: "continue", label: null, id, implicit: false },
1174 - label: null,
1175 - };
1176 - }
1177 - default: {
1178 - assertExhaustive(
1179 - target.type,
1180 - `Unexpected continue target kind '${(target as any).type}'`
1181 - );
1182 - }
1183 - }
1171 +
1172 + return {
1173 + kind: "terminal",
1174 + terminal: {
1175 + kind: "continue",
1176 + target: target.block,
1177 + id,
1178 + targetKind: target.type,
1179 + },
1180 + label: null,
1181 + };
1182 }
1183 }
1184
@@ -1323,12 +1321,12 @@ class Context {
1321 */
1322 getBreakTarget(
1323 block: BlockId
1326 - ): { block: BlockId; type: ControlFlowKind } | null {
1324 + ): { block: BlockId; type: ReactiveTerminalTargetKind } | null {
1325 let hasPrecedingLoop = false;
1326 for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
1327 const target = this.#controlFlowStack[i]!;
1328 if (target.block === block) {
1331 - let type: ControlFlowKind;
1329 + let type: ReactiveTerminalTargetKind;
1330 if (target.type === "loop") {
1331 /*
1332 * breaking out of a loop requires an explicit break,
@@ -1367,12 +1365,12 @@ class Context {
1365 */
1366 getContinueTarget(
1367 block: BlockId
1370 - ): { block: BlockId; type: ControlFlowKind } | null {
1368 + ): { block: BlockId; type: ReactiveTerminalTargetKind } | null {
1369 let hasPrecedingLoop = false;
1370 for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
1371 const target = this.#controlFlowStack[i]!;
1372 if (target.type == "loop" && target.continueBlock === block) {
1375 - let type: ControlFlowKind;
1373 + let type: ReactiveTerminalTargetKind;
1374 if (hasPrecedingLoop) {
1375 /*
1376 * continuing to a loop that is not the innermost loop always requires
@@ -1407,8 +1405,6 @@ class Context {
1405 }
1406 }
1407
1410 -type ControlFlowKind = "implicit" | "labeled" | "unlabeled";
1411 -
1408 type ControlFlowTarget =
1409 | { type: "if"; block: BlockId; id: number }
1410 | { type: "switch"; block: BlockId; id: number }
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/CodegenReactiveFunction.ts
+8 -8
@@ -318,13 +318,13 @@ function codegenBlockNoReset(
318 if (statement === null) {
319 break;
320 }
321 - if (item.label !== null) {
321 + if (item.label !== null && !item.label.implicit) {
322 const block =
323 statement.type === "BlockStatement" && statement.body.length === 1
324 ? statement.body[0]
325 : statement;
326 statements.push(
327 - t.labeledStatement(t.identifier(codegenLabel(item.label)), block)
327 + t.labeledStatement(t.identifier(codegenLabel(item.label.id)), block)
328 );
329 } else if (statement.type === "BlockStatement") {
330 statements.push(...statement.body);
@@ -615,22 +615,22 @@ function codegenTerminal(
615 ): t.Statement | null {
616 switch (terminal.kind) {
617 case "break": {
618 - if (terminal.implicit) {
618 + if (terminal.targetKind === "implicit") {
619 return null;
620 }
621 return t.breakStatement(
622 - terminal.label !== null
623 - ? t.identifier(codegenLabel(terminal.label))
622 + terminal.targetKind === "labeled"
623 + ? t.identifier(codegenLabel(terminal.target))
624 : null
625 );
626 }
627 case "continue": {
628 - if (terminal.implicit) {
628 + if (terminal.targetKind === "implicit") {
629 return null;
630 }
631 return t.continueStatement(
632 - terminal.label !== null
633 - ? t.identifier(codegenLabel(terminal.label))
632 + terminal.targetKind === "labeled"
633 + ? t.identifier(codegenLabel(terminal.target))
634 : null
635 );
636 }
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PrintReactiveFunction.ts
+8 -13
@@ -135,7 +135,7 @@ function writeReactiveInstruction(
135 }
136 case "terminal": {
137 if (instr.label !== null) {
138 - writer.write(`bb${instr.label}: `);
138 + writer.write(`bb${instr.label.id}: `);
139 }
140 writeTerminal(writer, instr.terminal);
141 break;
@@ -229,22 +229,17 @@ function writeTerminal(writer: Writer, terminal: ReactiveTerminal): void {
229 switch (terminal.kind) {
230 case "break": {
231 const id = terminal.id !== null ? `[${terminal.id}]` : [];
232 - const implicit = terminal.implicit ? "(implicit) " : "";
233 - if (terminal.label !== null) {
234 - writer.writeLine(`${id} ${implicit}break bb${terminal.label}`);
235 - } else {
236 - writer.writeLine(`${id} ${implicit}break`);
237 - }
232 + writer.writeLine(
233 + `${id} break bb${terminal.target} (${terminal.targetKind})`
234 + );
235 +
236 break;
237 }
238 case "continue": {
239 const id = `[${terminal.id}]`;
242 - const implicit = terminal.implicit ? "(implicit) " : "";
243 - if (terminal.label !== null) {
244 - writer.writeLine(`${id} ${implicit}continue bb${terminal.label}`);
245 - } else {
246 - writer.writeLine(`${id} ${implicit}continue`);
247 - }
240 + writer.writeLine(
241 + `${id} continue bb${terminal.target} (${terminal.targetKind})`
242 + );
243 break;
244 }
245 case "do-while": {
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PropagateEarlyReturns.ts
+6 -3
@@ -238,7 +238,10 @@ class Transform extends ReactiveFunctionTransform<State> {
238 },
239 {
240 kind: "terminal",
241 - label: earlyReturnValue.label,
241 + label: {
242 + id: earlyReturnValue.label,
243 + implicit: false,
244 + },
245 terminal: {
246 kind: "label",
247 id: makeInstructionId(0),
@@ -316,8 +319,8 @@ class Transform extends ReactiveFunctionTransform<State> {
319 terminal: {
320 kind: "break",
321 id: makeInstructionId(0),
319 - implicit: false,
320 - label: earlyReturnValue.label,
322 + targetKind: "labeled",
323 + target: earlyReturnValue.label,
324 },
325 },
326 ],
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PruneUnusedLabels.ts
+6 -6
@@ -37,12 +37,12 @@ class Transform extends ReactiveFunctionTransform<Labels> {
37 const { terminal } = stmt;
38 if (
39 (terminal.kind === "break" || terminal.kind === "continue") &&
40 - terminal.label !== null
40 + terminal.targetKind === "labeled"
41 ) {
42 - state.add(terminal.label);
42 + state.add(terminal.target);
43 }
44 // Is this terminal reachable via a break/continue to its label?
45 - const isReachableLabel = stmt.label !== null && state.has(stmt.label);
45 + const isReachableLabel = stmt.label !== null && state.has(stmt.label.id);
46 if (stmt.terminal.kind === "label" && !isReachableLabel) {
47 // Flatten labeled terminals where the label isn't necessary
48 const block = [...stmt.terminal.block];
@@ -51,14 +51,14 @@ class Transform extends ReactiveFunctionTransform<Labels> {
51 last !== undefined &&
52 last.kind === "terminal" &&
53 last.terminal.kind === "break" &&
54 - last.terminal.label === null
54 + last.terminal.target === null
55 ) {
56 block.pop();
57 }
58 return { kind: "replace-many", value: block };
59 } else {
60 - if (!isReachableLabel) {
61 - stmt.label = null;
60 + if (!isReachableLabel && stmt.label != null) {
61 + stmt.label.implicit = true;
62 }
63 return { kind: "keep" };
64 }