@samitouri / QOS-React-2 / commits / a97c55dc5d

Support for with empty update expression

Adds support for `for` statements with an empty or unreachable update expression. In both cases, reversePostorderBlocks() will remove the empty/unreachable update block, leaving the ForTerminal.update pointing to a non-existent block. We explicitly rewrite this (much like we null out unreachable fallthroughs after shrink). When transforming to ReactiveFunction, we emit the update block as null if it was the same as the test block.

Joe Savona committed Apr 4, 2023 at 13:29 UTC a97c55dc5da37c0d0358fee660be4343e7528c38
18 files changed +158 -97
compiler/forget/src/HIR/BuildHIR.ts
+24 -24
@@ -307,35 +307,35 @@ function lowerStatement(
307 };
308 });
309
310 - const updateBlock = builder.enter("loop", (_blockId) => {
311 - const update = stmt.get("update");
312 - if (update.node == null) {
313 - builder.errors.push({
314 - reason: `(BuildHIR::lowerStatement) Handle empty update in ForStatement`,
315 - severity: ErrorSeverity.Todo,
316 - nodePath: stmt,
317 - });
318 - return { kind: "unsupported", id: makeInstructionId(0) };
319 - }
320 - lowerExpressionToTemporary(builder, update as NodePath<t.Expression>);
321 - return {
322 - kind: "goto",
323 - block: testBlock.id,
324 - variant: GotoVariant.Break,
325 - id: makeInstructionId(0),
326 - };
327 - });
328 -
329 - const bodyBlock = builder.enter("block", (_blockId) => {
330 - return builder.loop(label, updateBlock, continuationBlock.id, () => {
331 - lowerStatement(builder, stmt.get("body"));
310 + let updateBlock: BlockId | null = null;
311 + const update = stmt.get("update");
312 + if (update.node != null) {
313 + updateBlock = builder.enter("loop", (_blockId) => {
314 + lowerExpressionToTemporary(builder, update as NodePath<t.Expression>);
315 return {
316 kind: "goto",
334 - block: updateBlock,
335 - variant: GotoVariant.Continue,
317 + block: testBlock.id,
318 + variant: GotoVariant.Break,
319 id: makeInstructionId(0),
320 };
321 });
322 + }
323 +
324 + const bodyBlock = builder.enter("block", (_blockId) => {
325 + return builder.loop(
326 + label,
327 + updateBlock ?? testBlock.id,
328 + continuationBlock.id,
329 + () => {
330 + lowerStatement(builder, stmt.get("body"));
331 + return {
332 + kind: "goto",
333 + block: updateBlock ?? testBlock.id,
334 + variant: GotoVariant.Continue,
335 + id: makeInstructionId(0),
336 + };
337 + }
338 + );
339 });
340
341 builder.terminateWithContinuation(
compiler/forget/src/HIR/HIR.ts
+1 -1
@@ -347,7 +347,7 @@ export type ForTerminal = {
347 loc: SourceLocation;
348 init: BlockId;
349 test: BlockId;
350 - update: BlockId;
350 + update: BlockId | null;
351 loop: BlockId;
352 fallthrough: BlockId;
353 id: InstructionId;
compiler/forget/src/HIR/HIRBuilder.ts
+13
@@ -282,6 +282,7 @@ export default class HIRBuilder {
282 logHIR("Build (shrunk)", ir);
283 // then convert to reverse postorder
284 reversePostorderBlocks(ir);
285 + removeUnreachableForUpdates(ir);
286 removeUnreachableFallthroughs(ir);
287 removeDeadDoWhileStatements(ir);
288 markInstructionIds(ir);
@@ -528,6 +529,18 @@ export function shrink(func: HIR): void {
529 }
530 }
531
532 +export function removeUnreachableForUpdates(fn: HIR): void {
533 + for (const [, block] of fn.blocks) {
534 + if (
535 + block.terminal.kind === "for" &&
536 + block.terminal.update !== null &&
537 + !fn.blocks.has(block.terminal.update)
538 + ) {
539 + block.terminal.update = null;
540 + }
541 + }
542 +}
543 +
544 export function removeUnreachableFallthroughs(func: HIR): void {
545 const visited: Set<BlockId> = new Set();
546 for (const [_, block] of func.blocks) {
compiler/forget/src/HIR/visitors.ts
+1 -1
@@ -632,7 +632,7 @@ export function mapTerminalSuccessors(
632 case "for": {
633 const init = fn(terminal.init);
634 const test = fn(terminal.test);
635 - const update = fn(terminal.update);
635 + const update = terminal.update !== null ? fn(terminal.update) : null;
636 const loop = fn(terminal.loop);
637 const fallthrough = fn(terminal.fallthrough);
638 return {
compiler/forget/src/Optimization/ConstantPropagation.ts
+5 -1
@@ -24,7 +24,10 @@ import {
24 validateConsistentIdentifiers,
25 validateTerminalSuccessors,
26 } from "../HIR";
27 -import { removeDeadDoWhileStatements } from "../HIR/HIRBuilder";
27 +import {
28 + removeDeadDoWhileStatements,
29 + removeUnreachableForUpdates,
30 +} from "../HIR/HIRBuilder";
31 import { eliminateRedundantPhi } from "../SSA";
32
33 /**
@@ -52,6 +55,7 @@ export function constantPropagation(fn: HIRFunction): void {
55 shrink(fn.body);
56 reversePostorderBlocks(fn.body);
57 removeUnreachableFallthroughs(fn.body);
58 + removeUnreachableForUpdates(fn.body);
59 removeDeadDoWhileStatements(fn.body);
60 markInstructionIds(fn.body);
61 markPredecessors(fn.body);
compiler/forget/src/ReactiveScopes/BuildReactiveFunction.ts
+5 -5
@@ -347,7 +347,7 @@ class Driver {
347
348 const scheduleId = this.cx.scheduleLoop(
349 terminal.fallthrough,
350 - terminal.update,
350 + terminal.update ?? terminal.test,
351 terminal.loop
352 );
353 scheduleIds.push(scheduleId);
@@ -382,10 +382,10 @@ class Driver {
382 terminal.loc
383 ).value;
384
385 - const updateValue = this.visitValueBlock(
386 - terminal.update,
387 - terminal.loc
388 - ).value;
385 + const updateValue =
386 + terminal.update !== null
387 + ? this.visitValueBlock(terminal.update, terminal.loc).value
388 + : null;
389
390 let loopBody: ReactiveBlock;
391 if (loopId) {
compiler/forget/src/SSA/LeaveSSA.ts
+1 -1
@@ -335,7 +335,7 @@ export function leaveSSA(fn: HIRFunction): void {
335 }
336 }
337
338 - if (terminal.kind === "for") {
338 + if (terminal.kind === "for" && terminal.update !== null) {
339 const update = fn.body.blocks.get(terminal.update)!;
340 pushPhis(update);
341 }
compiler/forget/src/Utils/VisualizeHIRMermaid.ts
+3 -1
@@ -205,7 +205,9 @@ function printTerminalArrows(blockId: BlockId, terminal: Terminal): string {
205 case "for": {
206 buffer.push(printJumpArrow(blockId, terminal.init, "init"));
207 buffer.push(printJumpArrow(blockId, terminal.test, "test"));
208 - buffer.push(printJumpArrow(blockId, terminal.update, "update"));
208 + if (terminal.update !== null) {
209 + buffer.push(printJumpArrow(blockId, terminal.update, "update"));
210 + }
211 buffer.push(printJumpArrow(blockId, terminal.loop, "loop"));
212 buffer.push(printJumpArrow(blockId, terminal.fallthrough, "fallthrough"));
213 break;
compiler/forget/src/__tests__/fixtures/compiler/error.for-return.expect.md deleted
-20
@@ -1,20 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -function Component(props) {
6 - for (let i = 0; i < props.count; i++) {
7 - return;
8 - }
9 -}
10 -
11 -```
12 -
13 -
14 -## Error
15 -
16 -```
17 -[ReactForget] Invariant: Block bb4 does not exist for terminal '[1] For init=bb3 test=bb1 loop=bb5 update=bb4 fallthrough=bb2' (2:4)
18 -```
19 -
20 -
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/error.leave-ssa-handle-return-in-for.expect.md deleted
-20
@@ -1,20 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -function foo(props) {
6 - for (let i = 0; i < 10; i += 1) {
7 - return;
8 - }
9 -}
10 -
11 -```
12 -
13 -
14 -## Error
15 -
16 -```
17 -Cannot read properties of undefined (reading 'phis')
18 -```
19 -
20 -
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/error.leave-ssa-handle-return-in-for.js deleted
-5
@@ -1,5 +0,0 @@
1 -function foo(props) {
2 - for (let i = 0; i < 10; i += 1) {
3 - return;
4 - }
5 -}
compiler/forget/src/__tests__/fixtures/compiler/error.todo-kitchensink.expect.md
-18
@@ -144,15 +144,6 @@ let moduleLocal = false;
144 25 | }
145 26 | for (;;) {
146
147 -[ReactForget] TodoError: (BuildHIR::lowerStatement) Handle empty update in ForStatement
148 - 21 | x.push(i);
149 - 22 | }
150 -> 23 | for (; i < 3; ) {
151 - | ^
152 - 24 | break;
153 - 25 | }
154 - 26 | for (;;) {
155 -
147 [ReactForget] TodoError: (BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement
148 24 | break;
149 25 | }
@@ -162,15 +153,6 @@ let moduleLocal = false;
153 28 | }
154 29 |
155
165 -[ReactForget] TodoError: (BuildHIR::lowerStatement) Handle empty update in ForStatement
166 - 24 | break;
167 - 25 | }
168 -> 26 | for (;;) {
169 - | ^
170 - 27 | break;
171 - 28 | }
172 - 29 |
173 -
156 [ReactForget] TodoError: (BuildHIR::lowerStatement) Handle empty test in ForStatement
157 24 | break;
158 25 | }
compiler/forget/src/__tests__/fixtures/compiler/for-empty-update-with-continue.expect.md new
+30
@@ -0,0 +1,30 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component(props) {
6 + let x = 0;
7 + for (let i = 0; i < props.count; ) {
8 + x += i;
9 + i += 1;
10 + continue;
11 + }
12 + return x;
13 +}
14 +
15 +```
16 +
17 +## Code
18 +
19 +```javascript
20 +function Component(props) {
21 + let x = 0;
22 + for (let i = 0; i < props.count; ) {
23 + x = x + i;
24 + i = i + 1;
25 + }
26 + return x;
27 +}
28 +
29 +```
30 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/for-empty-update-with-continue.js new
+9
@@ -0,0 +1,9 @@
1 +function Component(props) {
2 + let x = 0;
3 + for (let i = 0; i < props.count; ) {
4 + x += i;
5 + i += 1;
6 + continue;
7 + }
8 + return x;
9 +}
compiler/forget/src/__tests__/fixtures/compiler/for-empty-update.expect.md new
+33
@@ -0,0 +1,33 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component(props) {
6 + let x = 0;
7 + for (let i = 0; i < props.count; ) {
8 + x += i;
9 + if (x > 10) {
10 + break;
11 + }
12 + }
13 + return x;
14 +}
15 +
16 +```
17 +
18 +## Code
19 +
20 +```javascript
21 +function Component(props) {
22 + let x = 0;
23 + for (const i = 0; 0 < props.count; ) {
24 + x = x + 0;
25 + if (x > 10) {
26 + break;
27 + }
28 + }
29 + return x;
30 +}
31 +
32 +```
33 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/for-empty-update.js new
+10
@@ -0,0 +1,10 @@
1 +function Component(props) {
2 + let x = 0;
3 + for (let i = 0; i < props.count; ) {
4 + x += i;
5 + if (x > 10) {
6 + break;
7 + }
8 + }
9 + return x;
10 +}
compiler/forget/src/__tests__/fixtures/compiler/for-return.expect.md new
+23
@@ -0,0 +1,23 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component(props) {
6 + for (let i = 0; i < props.count; i++) {
7 + return;
8 + }
9 +}
10 +
11 +```
12 +
13 +## Code
14 +
15 +```javascript
16 +function Component(props) {
17 + for (const i = 0; 0 < props.count; ) {
18 + return;
19 + }
20 +}
21 +
22 +```
23 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/for-return.js renamed