@samitouri / QOS-React / commits / 656b8c9a0e

While terminal and codegen

Adds a new 'while' terminal variant, which will be a model for other loop terminals, and adds support for the entire compilation pipeline through codegen. To understand the structure of the terminal consider this input: ```javascript let x = 0; while (x) { x = foo(x); } return x; ``` We currently lower this to ifs and gotos: ``` bb0: precursor to loop let x = 0; goto(break) bb1; // <-- **The new terminal replaces this** bb1: test block, whether to (re-)enter the loop if (x) consequent=bb2 alternate=bb3; bb2: loop body x = foo(x); goto(continue) bb1; bb3: fallthrough after the loop return x ``` This representation correctly models the semantics of while statements, but loses the high-level information that there was a loop. The new 'while' terminal replaces the first 'goto(break) bb1'. Conceptually, the 'while' terminal means "enter the starting point of a while loop". In this example the terminal would look like this: ``` { kind: 'while', testBlock: 'bb1', // the basic block that checks whether to enter the loop or not loop: 'bb2', // the block containing the loop body fallthrough: 'bb3' // the block that goes after the loop } ``` Most passes will only look at 'testBlock', ie they will treat this terminal as a simple goto:testBlock. However, codegen uses the full information in the terminal to reconstruct the loop. My previous PR, #755, added a mechanism to be smart about when to emit or not emit `break` statements; this PR improves upon that to accurately emit the minimal break and continue statements: ie omitting entirely where they are extraneous, emitting unlabeled break/continue when sufficient, and falling back to labeled break/continue only where strictly necessary. The logic is very much analogous to IR construction.

Joe Savona committed Nov 9, 2022 at 16:39 UTC 656b8c9a0ed2dc2d99d07f9a4a878c35f9a0d09c
21 files changed +538 -95
compiler/forget/src/HIR/BuildHIR.ts
+4 -3
@@ -426,9 +426,10 @@ function lowerStatement(
426 */
427 builder.terminateWithContinuation(
428 {
429 - kind: "goto",
430 - block: conditionalBlock.id,
431 - variant: GotoVariant.Break,
429 + kind: "while",
430 + test: conditionalBlock.id,
431 + loop: loopBlock,
432 + fallthrough: continuationBlock.id,
433 },
434 conditionalBlock
435 );
compiler/forget/src/HIR/Codegen.ts
+231 -41
@@ -78,11 +78,13 @@ class Context {
78 #scheduled: Set<BlockId> = new Set();
79
80 /**
81 - * A stack of blocks that are in scope, used to decide whether/how to emit
82 - * break and continue statements. All blocks in the stack must also be
83 - * in 'scheduled'.
81 + * Represents which control flow operations are currently in scope, with the innermost
82 + * scope last. Roughly speaking, the last ControlFlowTarget on the stack indicates where
83 + * control will implicitly transfer, such that gotos to that block can be elided. Gotos
84 + * targeting items higher up the stack may need labeled break or continue; see
85 + * getBreakTarget() and getContinueTarget() for more details.
86 */
85 - #breakTargets: Array<BreakTarget> = [];
87 + #controlFlowStack: Array<ControlFlowTarget> = [];
88
89 constructor(ir: HIR) {
90 this.ir = ir;
@@ -96,7 +98,36 @@ class Context {
98 const id = this.#nextScheduleId++;
99 invariant(!this.#scheduled.has(block), "Block is already scheduled");
100 this.#scheduled.add(block);
99 - this.#breakTargets.push({ block, id, type });
101 + this.#controlFlowStack.push({ block, id, type });
102 + return id;
103 + }
104 +
105 + scheduleLoop(
106 + fallthroughBlock: BlockId,
107 + continueBlock: BlockId,
108 + loopBlock: BlockId | null
109 + ): number {
110 + const id = this.#nextScheduleId++;
111 + const ownsBlock = !this.#scheduled.has(fallthroughBlock);
112 + this.#scheduled.add(fallthroughBlock);
113 + invariant(
114 + !this.#scheduled.has(continueBlock),
115 + "Block is already scheduled"
116 + );
117 + this.#scheduled.add(continueBlock);
118 + if (loopBlock !== null) {
119 + invariant(!this.#scheduled.has(loopBlock), "Block is already scheduled");
120 + this.#scheduled.add(loopBlock);
121 + }
122 +
123 + this.#controlFlowStack.push({
124 + block: fallthroughBlock,
125 + ownsBlock,
126 + id,
127 + type: "loop",
128 + continueBlock,
129 + loopBlock,
130 + });
131 return id;
132 }
133
@@ -104,12 +135,20 @@ class Context {
135 * Removes a block that was scheduled; must be called after that block is emitted.
136 */
137 unschedule(scheduleId: number): void {
107 - const last = this.#breakTargets.pop();
138 + const last = this.#controlFlowStack.pop();
139 invariant(
140 last !== undefined && last.id === scheduleId,
141 "Can only unschedule the last target"
142 );
112 - this.#scheduled.delete(last.block);
143 + if (last.type !== "loop" || last.ownsBlock !== null) {
144 + this.#scheduled.delete(last.block);
145 + }
146 + if (last.type === "loop") {
147 + this.#scheduled.delete(last.continueBlock);
148 + if (last.loopBlock !== null) {
149 + this.#scheduled.delete(last.loopBlock);
150 + }
151 + }
152 }
153
154 /**
@@ -130,32 +169,105 @@ class Context {
169 }
170
171 /**
133 - * Lookup the break target for the given @param block. This will return non-null
134 - * if and only if isScheduled() returns true for the given @param block. Returns
135 - * the break target and whether this is the most recent target (which can be used
136 - * to elide unnecessary break statemetns).
172 + * Given the current control flow stack, determines how a `break` to the given @param block
173 + * must be emitted. Returns as follows:
174 + * - 'implicit' if control would implicitly transfer to that block
175 + * - 'labeled' if a labeled break is required to transfer control to that block
176 + * - 'unlabeled' if an unlabeled break would transfer to that block
177 + * - null if there is no information for this block
178 + *
179 + * The returned 'block' value should be used as the label if necessary.
180 */
181 getBreakTarget(
182 block: BlockId
140 - ): { target: BreakTarget; last: boolean } | null {
141 - for (let i = this.#breakTargets.length - 1; i >= 0; i--) {
142 - const target = this.#breakTargets[i]!;
183 + ): { block: BlockId; type: ControlFlowKind } | null {
184 + let hasPrecedingLoop = false;
185 + for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
186 + const target = this.#controlFlowStack[i]!;
187 if (target.block === block) {
188 + let type: ControlFlowKind;
189 + if (target.type === "loop") {
190 + // breaking out of a loop requires an explicit break,
191 + // but only requires a label if breaking past the innermost loop.
192 + type = hasPrecedingLoop ? "labeled" : "unlabeled";
193 + } else if (i === this.#controlFlowStack.length - 1) {
194 + // breaking to the last break point, which is where control will transfer
195 + // implicitly
196 + type = "implicit";
197 + } else {
198 + // breaking somewhere else requires an explicit break
199 + type = "labeled";
200 + }
201 + return {
202 + block: target.block,
203 + type,
204 + };
205 + }
206 + hasPrecedingLoop ||= target.type === "loop";
207 + }
208 + return null;
209 + }
210 +
211 + /**
212 + * Given the current control flow stack, determines how a `continue` to the given @param block
213 + * must be emitted. Returns as follows:
214 + * - 'implicit' if control would implicitly continue to that block
215 + * - 'labeled' if a labeled continue is required to continue to that block
216 + * - 'unlabeled' if an unlabeled continue would transfer to that block
217 + * - null if there is no information for this block
218 + *
219 + * The returned 'block' value should be used as the label if necessary.
220 + */
221 + getContinueTarget(
222 + block: BlockId
223 + ): { block: BlockId; type: ControlFlowKind } | null {
224 + let hasPrecedingLoop = false;
225 + for (let i = this.#controlFlowStack.length - 1; i >= 0; i--) {
226 + const target = this.#controlFlowStack[i]!;
227 + if (target.type == "loop" && target.continueBlock === block) {
228 + let type: ControlFlowKind;
229 + if (hasPrecedingLoop) {
230 + // continuing to a loop that is not the innermost loop always requires
231 + // a label
232 + type = "labeled";
233 + } else if (i === this.#controlFlowStack.length - 1) {
234 + // continuing to the last break point, which is where control will
235 + // transfer to naturally
236 + type = "implicit";
237 + } else {
238 + // the continue is inside some conditional logic, requires an explicit
239 + // continue
240 + type = "unlabeled";
241 + }
242 return {
145 - target,
146 - last: i === this.#breakTargets.length - 1,
243 + block: target.block,
244 + type,
245 };
246 }
247 + hasPrecedingLoop ||= target.type === "loop";
248 }
249 return null;
250 }
251 +
252 + debugBreakTargets(): Array<ControlFlowTarget> {
253 + return this.#controlFlowStack.map((target) => ({ ...target }));
254 + }
255 }
256
154 -type BreakTarget = {
155 - block: BlockId;
156 - id: number;
157 - type: "if" | "switch" | "case";
158 -};
257 +type ControlFlowKind = "implicit" | "labeled" | "unlabeled";
258 +
259 +type ControlFlowTarget =
260 + | { type: "if"; block: BlockId; id: number }
261 + | { type: "switch"; block: BlockId; id: number }
262 + | { type: "case"; block: BlockId; id: number }
263 + | {
264 + type: "loop";
265 + block: BlockId;
266 + ownsBlock: boolean;
267 + continueBlock: BlockId;
268 + loopBlock: BlockId | null;
269 + id: number;
270 + };
271
272 function codegenBlock(cx: Context, block: BasicBlock): t.BlockStatement {
273 invariant(
@@ -216,6 +328,7 @@ function writeBlock(cx: Context, block: BasicBlock, body: Array<t.Statement>) {
328 }
329 }
330
331 + cx.unscheduleAll(scheduleIds);
332 if (fallthroughId !== null) {
333 if (consequent === null && alternate === null) {
334 body.push(t.expressionStatement(test));
@@ -286,6 +399,7 @@ function writeBlock(cx: Context, block: BasicBlock, body: Array<t.Statement>) {
399 });
400 cases.reverse();
401
402 + cx.unscheduleAll(scheduleIds);
403 if (fallthroughId !== null) {
404 body.push(
405 t.labeledStatement(
@@ -299,6 +413,61 @@ function writeBlock(cx: Context, block: BasicBlock, body: Array<t.Statement>) {
413 }
414 break;
415 }
416 + case "while": {
417 + const testBlock = cx.ir.blocks.get(terminal.test)!;
418 + const testTerminal = testBlock.terminal;
419 + invariant(
420 + testTerminal.kind === "if",
421 + "Expected while loop test block to end in an if"
422 + );
423 + const bodyLength = body.length;
424 + for (const instr of testBlock.instructions) {
425 + writeInstr(cx, instr, body);
426 + }
427 + invariant(
428 + body.length === bodyLength,
429 + "Expected test to produce only temporaries"
430 + );
431 + const testValue =
432 + cx.temp.get(testTerminal.test.identifier.id) ??
433 + codegenPlace(cx, testTerminal.test);
434 + invariant(
435 + testValue != null,
436 + "Expected test to produce a temporary value"
437 + );
438 +
439 + const fallthroughId =
440 + terminal.fallthrough !== null && !cx.isScheduled(terminal.fallthrough)
441 + ? terminal.fallthrough
442 + : null;
443 + const scheduleId = cx.scheduleLoop(
444 + terminal.fallthrough,
445 + terminal.test,
446 + terminal.loop
447 + );
448 + scheduleIds.push(scheduleId);
449 +
450 + let loopBody: t.Statement;
451 + if (terminal.loop !== null) {
452 + loopBody = codegenBlock(cx, cx.ir.blocks.get(terminal.loop)!);
453 + } else {
454 + loopBody = t.blockStatement([]);
455 + }
456 +
457 + cx.unscheduleAll(scheduleIds);
458 + if (fallthroughId !== null) {
459 + body.push(
460 + t.labeledStatement(
461 + t.identifier(`bb${fallthroughId}`),
462 + t.whileStatement(testValue, loopBody)
463 + )
464 + );
465 + writeBlock(cx, cx.ir.blocks.get(fallthroughId)!, body);
466 + } else {
467 + body.push(t.whileStatement(testValue, loopBody));
468 + }
469 + break;
470 + }
471 case "goto": {
472 switch (terminal.variant) {
473 case GotoVariant.Break: {
@@ -309,11 +478,10 @@ function writeBlock(cx: Context, block: BasicBlock, body: Array<t.Statement>) {
478 break;
479 }
480 case GotoVariant.Continue: {
312 - invariant(
313 - cx.isScheduled(terminal.block),
314 - "Expected continue target to be scheduled"
315 - );
316 - body.push(t.continueStatement(t.identifier(`bb${terminal.block}`)));
481 + const continue_ = codegenContinue(cx, terminal.block);
482 + if (continue_ !== null) {
483 + body.push(continue_);
484 + }
485 break;
486 }
487 default: {
@@ -329,27 +497,49 @@ function writeBlock(cx: Context, block: BasicBlock, body: Array<t.Statement>) {
497 assertExhaustive(terminal, "Unexpected terminal");
498 }
499 }
332 - cx.unscheduleAll(scheduleIds);
500 }
501
502 function codegenBreak(cx: Context, block: BlockId): t.Statement | null {
336 - const breakTarget = cx.getBreakTarget(block);
337 - if (breakTarget === null) {
503 + const target = cx.getBreakTarget(block);
504 + if (target === null) {
505 // TODO: we should always have a target
506 return null;
507 }
341 - const { target, last } = breakTarget;
342 - if (target.type === "case") {
343 - // This break is transitioning to the next case statement. JS doesn't allow
344 - // labeling cases, the only option is to emit a plain break.
345 - return null;
346 - } else if (last) {
347 - // This break is to the most recent break target. Control flow will naturally
348 - // transition to this target, so a break is not required.
349 - return null;
350 - } else {
351 - // We're trying to break somewhere else, emit a label
352 - return t.breakStatement(t.identifier(`bb${block}`));
508 + switch (target.type) {
509 + case "implicit": {
510 + return null;
511 + }
512 + case "unlabeled": {
513 + return t.breakStatement();
514 + }
515 + case "labeled": {
516 + return t.breakStatement(t.identifier(`bb${target.block}`));
517 + }
518 + }
519 +}
520 +
521 +function codegenContinue(cx: Context, block: BlockId): t.Statement | null {
522 + const target = cx.getContinueTarget(block);
523 + invariant(
524 + target !== null,
525 + `Expected continue target to be scheduled for bb${block}`
526 + );
527 + switch (target.type) {
528 + case "labeled": {
529 + return t.continueStatement(t.identifier(`bb${target.block}`));
530 + }
531 + case "unlabeled": {
532 + return t.continueStatement();
533 + }
534 + case "implicit": {
535 + return null;
536 + }
537 + default: {
538 + assertExhaustive(
539 + target.type,
540 + `Unexpected continue target kind '${(target as any).type}'`
541 + );
542 + }
543 }
544 }
545
compiler/forget/src/HIR/HIR.ts
+10 -1
@@ -118,7 +118,8 @@ export type Terminal =
118 | ReturnTerminal
119 | GotoTerminal
120 | IfTerminal
121 - | SwitchTerminal;
121 + | SwitchTerminal
122 + | WhileTerminal;
123
124 export type ThrowTerminal = { kind: "throw"; value: Place };
125
@@ -142,6 +143,7 @@ export type IfTerminal = {
143 alternate: BlockId;
144 fallthrough: BlockId | null;
145 };
146 +
147 export type SwitchTerminal = {
148 kind: "switch";
149 test: Place;
@@ -149,6 +151,13 @@ export type SwitchTerminal = {
151 fallthrough: BlockId | null;
152 };
153
154 +export type WhileTerminal = {
155 + kind: "while";
156 + test: BlockId;
157 + loop: BlockId;
158 + fallthrough: BlockId;
159 +};
160 +
161 /**
162 * Instructions generally represent expressions but with all nesting flattened away,
163 * such that all operands to each instruction are either primitive values OR are
compiler/forget/src/HIR/HIRBuilder.ts
+21 -4
@@ -11,6 +11,7 @@ import { invariant } from "../CompilerError";
11 import {
12 BasicBlock,
13 BlockId,
14 + GotoVariant,
15 HIR,
16 Identifier,
17 IdentifierId,
@@ -390,7 +391,11 @@ function shrink(func: HIR): HIR {
391
392 // Cleanup any fallthrough blocks that weren't visited
393 for (const block of blocks.values()) {
393 - if (block.terminal.kind === "if" || block.terminal.kind === "switch") {
394 + if (
395 + block.terminal.kind === "if" ||
396 + block.terminal.kind === "switch" ||
397 + block.terminal.kind === "while"
398 + ) {
399 if (
400 block.terminal.fallthrough !== null &&
401 !blocks.has(block.terminal.fallthrough)
@@ -398,6 +403,12 @@ function shrink(func: HIR): HIR {
403 block.terminal.fallthrough = null;
404 }
405 }
406 + if (block.terminal.kind === "while") {
407 + invariant(
408 + resolveBlockTarget(block.terminal.loop) === block.terminal.loop,
409 + `Expected while loop body to remain after shrinking`
410 + );
411 + }
412 }
413 return { blocks, entry: func.entry };
414 }
@@ -465,6 +476,10 @@ function reversePostorderBlocks(func: HIR): HIR {
476 }
477 break;
478 }
479 + case "while": {
480 + visit(terminal.test);
481 + break;
482 + }
483 default: {
484 assertExhaustive(
485 terminal,
@@ -520,11 +535,13 @@ function markPredecessors(func: HIR) {
535 }
536
537 /**
523 - * If the given block is a simple indirection (empty terminated with a goto),
524 - * returns the block being pointed to. Otherwise returns null
538 + * If the given block is a simple indirection — empty terminated with a goto(break) —
539 + * returns the block being pointed to. Otherwise returns null.
540 */
541 function getTargetIfIndirection(block: BasicBlock): number | null {
527 - return block.instructions.length === 0 && block.terminal.kind === "goto"
542 + return block.instructions.length === 0 &&
543 + block.terminal.kind === "goto" &&
544 + block.terminal.variant === GotoVariant.Break
545 ? block.terminal.block
546 : null;
547 }
compiler/forget/src/HIR/PrintHIR.ts
+11 -1
@@ -8,6 +8,7 @@
8 import generate from "@babel/generator";
9 import { assertExhaustive } from "../Common/utils";
10 import {
11 + GotoVariant,
12 HIR,
13 Identifier,
14 Instruction,
@@ -70,6 +71,7 @@ export function printMixedHIR(
71 case "return":
72 case "switch":
73 case "throw":
74 + case "while":
75 case "goto": {
76 const terminal = printTerminal(value);
77 if (Array.isArray(terminal)) {
@@ -129,7 +131,9 @@ function printTerminal(terminal: Terminal): Array<string> | string {
131 break;
132 }
133 case "goto": {
132 - value = `Goto bb${terminal.block}`;
134 + value = `Goto${
135 + terminal.variant === GotoVariant.Continue ? "(Continue)" : ""
136 + } bb${terminal.block}`;
137 break;
138 }
139 case "switch": {
@@ -145,6 +149,12 @@ function printTerminal(terminal: Terminal): Array<string> | string {
149 value = output;
150 break;
151 }
152 + case "while": {
153 + value = `While test=bb${terminal.test} loop=${
154 + terminal.loop !== null ? `bb${terminal.loop}` : ""
155 + } fallthrough=${terminal.fallthrough ? `bb${terminal.fallthrough}` : ""}`;
156 + break;
157 + }
158 default: {
159 assertExhaustive(
160 terminal,
compiler/forget/src/HIR/visitors.ts
+17
@@ -184,6 +184,17 @@ export function mapTerminalSuccessors(
184 case "throw": {
185 return terminal;
186 }
187 + case "while": {
188 + const test = fn(terminal.test, false);
189 + const loop = fn(terminal.loop, true);
190 + const fallthrough = fn(terminal.fallthrough, true);
191 + return {
192 + kind: "while",
193 + test,
194 + loop,
195 + fallthrough,
196 + };
197 + }
198 default: {
199 assertExhaustive(
200 terminal,
@@ -221,6 +232,10 @@ export function* eachTerminalSuccessor(terminal: Terminal): Iterable<BlockId> {
232 case "throw": {
233 break;
234 }
235 + case "while": {
236 + yield terminal.test;
237 + break;
238 + }
239 default: {
240 assertExhaustive(
241 terminal,
@@ -256,6 +271,7 @@ export function mapTerminalOperands(
271 }
272 break;
273 }
274 + case "while":
275 case "goto": {
276 // no-op
277 break;
@@ -292,6 +308,7 @@ export function* eachTerminalOperand(terminal: Terminal): Iterable<Place> {
308 }
309 break;
310 }
311 + case "while":
312 case "goto": {
313 // no-op
314 break;
compiler/forget/src/__tests__/fixtures/hir/complex-while.expect.md new
+52
@@ -0,0 +1,52 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function foo(a, b, c) {
6 + label: if (a) {
7 + while (b) {
8 + if (c) {
9 + break;
10 + }
11 + }
12 + }
13 +}
14 +
15 +```
16 +
17 +## HIR
18 +
19 +```
20 +bb0:
21 + If (read a$4) then:bb3 else:bb1
22 +bb3:
23 + predecessor blocks: bb0
24 + While test=bb4 loop=bb6 fallthrough=bb1
25 +bb4:
26 + predecessor blocks: bb3 bb7
27 + If (read b$5) then:bb6 else:bb1
28 +bb6:
29 + predecessor blocks: bb4
30 + If (read c$6) then:bb1 else:bb7
31 +bb7:
32 + predecessor blocks: bb6
33 + Goto(Continue) bb4
34 +bb1:
35 + predecessor blocks: bb6 bb4 bb0
36 + Return
37 +```
38 +
39 +## Code
40 +
41 +```javascript
42 +function foo$0(a$4, b$5, c$6) {
43 + bb1: if (a$4) {
44 + while (b$5) {
45 + bb7: if (c$6) break;
46 + }
47 + }
48 + return;
49 +}
50 +
51 +```
52 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/complex-while.js new
+9
@@ -0,0 +1,9 @@
1 +function foo(a, b, c) {
2 + label: if (a) {
3 + while (b) {
4 + if (c) {
5 + break;
6 + }
7 + }
8 + }
9 +}
compiler/forget/src/__tests__/fixtures/hir/component.expect.md
+9 -3
@@ -43,7 +43,7 @@ bb0:
43 [6] Const mutate max$32 = Call mutate Math$8.max(read $31, read maxItems$28)
44 Goto bb1
45 bb1:
46 - predecessor blocks: bb0 bb7 bb4
46 + predecessor blocks: bb0 bb5 bb10
47 If (read items$27) then:bb3 else:bb2
48 bb3:
49 predecessor blocks: bb1
@@ -60,7 +60,10 @@ bb9:
60 Goto bb7
61 bb7:
62 predecessor blocks: bb8 bb9
63 - If (read $13) then:bb1 else:bb4
63 + If (read $13) then:bb5 else:bb4
64 +bb5:
65 + predecessor blocks: bb7
66 + Goto(Continue) bb1
67 bb4:
68 predecessor blocks: bb7
69 [11] Call mutate seen$30.add(mutate item$10)
@@ -68,7 +71,10 @@ bb4:
71 [13] Const mutate $44 = JSX <read $43>{read item$10}</read $43>
72 [14] Call mutate renderedItems$29.push(read $44)
73 [15] Const mutate $49 = Binary read renderedItems$29.length >= read max$32
71 - If (read $49) then:bb2 else:bb1
74 + If (read $49) then:bb2 else:bb10
75 +bb10:
76 + predecessor blocks: bb4
77 + Goto(Continue) bb1
78 bb2:
79 predecessor blocks: bb4 bb1
80 [16] Const mutate count$52 = read renderedItems$29.length
compiler/forget/src/__tests__/fixtures/hir/mutable-lifetime-loops.expect.md
+23 -3
@@ -77,9 +77,9 @@ bb0:
77 [2] Let mutate b$3 = Object { }
78 [3] Let mutate c$4 = Object { }
79 [4] Let mutate d$5 = Object { }
80 - Goto bb1
80 + While test=bb1 loop=bb3 fallthrough=bb2
81 bb1:
82 - predecessor blocks: bb0 bb3
82 + predecessor blocks: bb0 bb4
83 [5] Const mutate $17 = true
84 If (read $17) then:bb3 else:bb2
85 bb3:
@@ -91,7 +91,10 @@ bb3:
91 [10] Reassign mutate d$5 = read z$19
92 [11] Call mutate mutate$7(mutate a$2, mutate b$3)
93 [12] Const mutate $29 = Call mutate cond$8(mutate a$2)
94 - If (read $29) then:bb2 else:bb1
94 + If (read $29) then:bb2 else:bb4
95 +bb4:
96 + predecessor blocks: bb3
97 + Goto(Continue) bb1
98 bb2:
99 predecessor blocks: bb3 bb1
100 If (read a$2) then:bb7 else:bb7
@@ -119,6 +122,23 @@ function Component$0(props$12) {
122 let b$3 = {};
123 let c$4 = {};
124 let d$5 = {};
125 + bb2: while (true) {
126 + let z$19 = a$2;
127 + a$2 = b$3;
128 + b$3 = c$4;
129 + c$4 = d$5;
130 + d$5 = z$19;
131 + mutate$7(a$2, b$3);
132 +
133 + bb4: if (cond$8(a$2)) break;
134 + }
135 +
136 + a$2;
137 + b$3;
138 + c$4;
139 + d$5;
140 + mutate$7(d$5, null);
141 + return;
142 }
143
144 ```
compiler/forget/src/__tests__/fixtures/hir/mutable-liverange-loop.expect.md
+18 -3
@@ -72,16 +72,19 @@ bb0:
72 [2] Let mutate b$13[2:6] = Object { }
73 [3] Let mutate c$14 = Object { }
74 [4] Let mutate d$15[4:9] = Object { }
75 - Goto bb1
75 + While test=bb1 loop=bb3 fallthrough=bb2
76 bb1:
77 - predecessor blocks: bb0 bb3
77 + predecessor blocks: bb0 bb4
78 [5] Const mutate $16 = true
79 If (read $16) then:bb3 else:bb2
80 bb3:
81 predecessor blocks: bb1
82 [6] Call mutate mutate$6(mutate a$12, mutate b$13)
83 [7] Const mutate $21 = Call mutate cond$7(mutate a$12)
84 - If (read $21) then:bb2 else:bb1
84 + If (read $21) then:bb2 else:bb4
85 +bb4:
86 + predecessor blocks: bb3
87 + Goto(Continue) bb1
88 bb2:
89 predecessor blocks: bb3 bb1
90 If (read a$12) then:bb7 else:bb7
@@ -109,6 +112,18 @@ function Component$0(props$11) {
112 let b$13 = {};
113 let c$14 = {};
114 let d$15 = {};
115 + bb2: while (true) {
116 + mutate$6(a$12, b$13);
117 +
118 + bb4: if (cond$7(a$12)) break;
119 + }
120 +
121 + a$12;
122 + b$13;
123 + c$14;
124 + d$15;
125 + mutate$6(d$15, null);
126 + return;
127 }
128
129 ```
compiler/forget/src/__tests__/fixtures/hir/ssa-for-of.expect.md
+6 -3
@@ -22,16 +22,19 @@ bb0:
22 [1] Let mutate items$5 = Array []
23 Goto bb1
24 bb1:
25 - predecessor blocks: bb0 bb5 bb3
25 + predecessor blocks: bb0 bb4
26 If (read items$5) then:bb3 else:bb2
27 bb3:
28 predecessor blocks: bb1
29 [2] Let mutate y$7 = 0
30 - If (read cond$4) then:bb5 else:bb1
30 + If (read cond$4) then:bb5 else:bb4
31 bb5:
32 predecessor blocks: bb3
33 [3] Reassign mutate y$9 = 1
34 - Goto bb1
34 + Goto bb4
35 +bb4:
36 + predecessor blocks: bb5 bb3
37 + Goto(Continue) bb1
38 bb2:
39 predecessor blocks: bb1
40 Return freeze items$5
compiler/forget/src/__tests__/fixtures/hir/ssa-for.expect.md
+1 -1
@@ -27,7 +27,7 @@ bb4:
27 predecessor blocks: bb1
28 [4] Const mutate $9 = 1
29 [5] Reassign mutate x$1 = Binary read x$1 + read $9
30 - Goto bb1
30 + Goto(Continue) bb1
31 bb2:
32 predecessor blocks: bb1
33 Return read x$1
compiler/forget/src/__tests__/fixtures/hir/ssa-nested-loops-no-reassign.expect.md
+32 -10
@@ -2,6 +2,7 @@
2 ## Input
3
4 ```javascript
5 +// @xonly
6 function foo(a, b, c) {
7 let x = 0;
8 while (a) {
@@ -21,21 +22,33 @@ function foo(a, b, c) {
22 ```
23 bb0:
24 [1] Let mutate x$9 = 0
24 - Goto bb1
25 + While test=bb1 loop=bb3 fallthrough=bb2
26 bb1:
26 - predecessor blocks: bb0 bb4
27 - If (read a$6) then:bb4 else:bb2
27 + predecessor blocks: bb0 bb5
28 + If (read a$6) then:bb3 else:bb2
29 +bb3:
30 + predecessor blocks: bb1
31 + While test=bb4 loop=bb6 fallthrough=bb5
32 bb4:
29 - predecessor blocks: bb1 bb7
30 - If (read b$7) then:bb7 else:bb1
33 + predecessor blocks: bb3 bb8
34 + If (read b$7) then:bb6 else:bb5
35 +bb6:
36 + predecessor blocks: bb4
37 + While test=bb7 loop=bb9 fallthrough=bb8
38 bb7:
32 - predecessor blocks: bb4 bb9
33 - If (read c$8) then:bb9 else:bb4
39 + predecessor blocks: bb6 bb9
40 + If (read c$8) then:bb9 else:bb8
41 bb9:
42 predecessor blocks: bb7
36 - [2] Const mutate $17 = 1
37 - [3] Binary read x$9 + read $17
38 - Goto bb7
43 + [2] Const mutate $13 = 1
44 + [3] Binary read x$9 + read $13
45 + Goto(Continue) bb7
46 +bb8:
47 + predecessor blocks: bb7
48 + Goto(Continue) bb4
49 +bb5:
50 + predecessor blocks: bb4
51 + Goto(Continue) bb1
52 bb2:
53 predecessor blocks: bb1
54 Return read x$9
@@ -46,6 +59,15 @@ bb2:
59 ```javascript
60 function foo$0(a$6, b$7, c$8) {
61 let x$9 = 0;
62 + bb2: while (a$6) {
63 + bb5: while (b$7) {
64 + bb8: while (c$8) {
65 + x$9 + 1;
66 + }
67 + }
68 + }
69 +
70 + return x$9;
71 }
72
73 ```
compiler/forget/src/__tests__/fixtures/hir/ssa-nested-loops-no-reassign.js
+1
@@ -1,3 +1,4 @@
1 +// @xonly
2 function foo(a, b, c) {
3 let x = 0;
4 while (a) {
compiler/forget/src/__tests__/fixtures/hir/ssa-while-no-reassign.expect.md
+7 -2
@@ -18,7 +18,7 @@ function foo() {
18 ```
19 bb0:
20 [1] Let mutate x$5 = 1
21 - Goto bb1
21 + While test=bb1 loop=bb3 fallthrough=bb2
22 bb1:
23 predecessor blocks: bb0 bb3
24 [2] Const mutate $6 = 10
@@ -28,7 +28,7 @@ bb3:
28 predecessor blocks: bb1
29 [4] Const mutate $9 = 1
30 [5] Binary read x$5 + read $9
31 - Goto bb1
31 + Goto(Continue) bb1
32 bb2:
33 predecessor blocks: bb1
34 Return read x$5
@@ -39,6 +39,11 @@ bb2:
39 ```javascript
40 function foo$0() {
41 let x$5 = 1;
42 + bb2: while (x$5 < 10) {
43 + x$5 + 1;
44 + }
45 +
46 + return x$5;
47 }
48
49 ```
compiler/forget/src/__tests__/fixtures/hir/ssa-while.expect.md
+7 -2
@@ -18,7 +18,7 @@ function foo() {
18 ```
19 bb0:
20 [1] Let mutate x$1 = 1
21 - Goto bb1
21 + While test=bb1 loop=bb3 fallthrough=bb2
22 bb1:
23 predecessor blocks: bb0 bb3
24 [2] Const mutate $6 = 10
@@ -28,7 +28,7 @@ bb3:
28 predecessor blocks: bb1
29 [4] Const mutate $9 = 1
30 [5] Reassign mutate x$1 = Binary read x$1 + read $9
31 - Goto bb1
31 + Goto(Continue) bb1
32 bb2:
33 predecessor blocks: bb1
34 Return read x$1
@@ -39,6 +39,11 @@ bb2:
39 ```javascript
40 function foo$0() {
41 let x$1 = 1;
42 + bb2: while (x$1 < 10) {
43 + x$1 = x$1 + 1;
44 + }
45 +
46 + return x$1;
47 }
48
49 ```
compiler/forget/src/__tests__/fixtures/hir/while-conditional-continue.expect.md new
+58
@@ -0,0 +1,58 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function foo(a, b, c, d) {
6 + while (a) {
7 + if (b) {
8 + continue;
9 + }
10 + c();
11 + continue;
12 + }
13 + d();
14 +}
15 +
16 +```
17 +
18 +## HIR
19 +
20 +```
21 +bb0:
22 + While test=bb1 loop=bb3 fallthrough=bb2
23 +bb1:
24 + predecessor blocks: bb0 bb5 bb4
25 + If (read a$5) then:bb3 else:bb2
26 +bb3:
27 + predecessor blocks: bb1
28 + If (read b$6) then:bb5 else:bb4
29 +bb5:
30 + predecessor blocks: bb3
31 + Goto(Continue) bb1
32 +bb4:
33 + predecessor blocks: bb3
34 + [1] Call read c$7()
35 + Goto(Continue) bb1
36 +bb2:
37 + predecessor blocks: bb1
38 + [2] Call read d$8()
39 + Return
40 +```
41 +
42 +## Code
43 +
44 +```javascript
45 +function foo$0(a$5, b$6, c$7, d$8) {
46 + bb2: while (a$5) {
47 + bb4: if (b$6) {
48 + continue;
49 + }
50 + c$7();
51 + }
52 +
53 + d$8();
54 + return;
55 +}
56 +
57 +```
58 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/while-conditional-continue.js new
+10
@@ -0,0 +1,10 @@
1 +function foo(a, b, c, d) {
2 + while (a) {
3 + if (b) {
4 + continue;
5 + }
6 + c();
7 + continue;
8 + }
9 + d();
10 +}
compiler/forget/src/__tests__/hir-test.ts
-15
@@ -70,22 +70,7 @@ describe("React Forget (HIR version)", () => {
70 inferReferenceEffects(ir);
71 inferMutableRanges(ir);
72 leaveSSA(ir);
73 - // const lifetimeGraph = buildDefUseGraph(ir);
73 const textHIR = printHIR(ir.body);
75 - // const textLifetimeGraph = printGraph(lifetimeGraph);
76 - // const graphvizFile = path.join(
77 - // __dirname,
78 - // "fixtures",
79 - // "hir-svg",
80 - // file + ".svg"
81 - // );
82 - // if (input.indexOf("@Out DefUseGraph") !== -1) {
83 - // graphviz.layout(textLifetimeGraph, "svg", "dot").then((svg) => {
84 - // writeFileSync(graphvizFile, svg);
85 - // });
86 - // } else if (existsSync(graphvizFile)) {
87 - // unlinkSync(graphvizFile);
88 - // }
74
75 const ast = codegen(ir);
76 const text = prettier.format(
compiler/forget/src/__tests__/test-utils/generateTestsFromFixtures.ts
+11 -3
@@ -72,7 +72,6 @@ export default function generateTestsFromFixtures(
72 input: inputFile,
73 output: outputFile,
74 } of Array.from(fixtures.values())) {
75 - let receivedOutput;
75 let testCommand;
76
77 switch (basename.split(".")[0]) {
@@ -87,9 +86,18 @@ export default function generateTestsFromFixtures(
86 break;
87 }
88
89 + let input: string | null = null;
90 + if (inputFile != null) {
91 + input = fs.readFileSync(inputFile, "utf8");
92 + const lines = input.split("\n");
93 + if (lines[0]!.indexOf("@only") !== -1) {
94 + testCommand = test.only;
95 + }
96 + }
97 +
98 testCommand(basename, () => {
91 - if (inputFile != null) {
92 - const input = fs.readFileSync(inputFile, "utf8");
99 + let receivedOutput;
100 + if (input !== null) {
101 receivedOutput = transform(input, basename);
102 } else {
103 receivedOutput = "<<input deleted>>";