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