Add support for DoWhile statements

Adds support for DoWhileStatements. It's pretty similar to how we handle While, 
except in the case where a test block is unreachable (for example, an early 
unconditional `break` within the loop body). In this scenario we eliminate the 
terminal altogether and replace it with a goto to the loop block.
This commit is contained in:
Lauren Tan
2023-03-01 19:45:02 -05:00
parent 71db40c6ff
commit 440fd1f24a
26 changed files with 536 additions and 70 deletions
+53 -1
View File
@@ -644,9 +644,61 @@ function lowerStatement(
});
return;
}
case "DoWhileStatement": {
const stmt = stmtPath as NodePath<t.DoWhileStatement>;
// Block used to evaluate whether to (re)enter or exit the loop
const conditionalBlock = builder.reserve("loop");
// Block for code following the loop
const continuationBlock = builder.reserve("block");
// Loop body, executed at least once uncondtionally prior to exit
const loopBlock = builder.enter("block", (_loopBlockId) => {
return builder.loop(
label,
conditionalBlock.id,
continuationBlock.id,
() => {
lowerStatement(builder, stmt.get("body"));
return {
kind: "goto",
block: conditionalBlock.id,
variant: GotoVariant.Continue,
id: makeInstructionId(0),
};
}
);
});
// Jump to the conditional block to evaluate whether to (re)enter the loop or exit to the
// continuation block.
const loc = stmt.node.loc ?? GeneratedSource;
builder.terminateWithContinuation(
{
kind: "do-while",
loc,
test: conditionalBlock.id,
loop: loopBlock,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
},
conditionalBlock
);
/**
* The conditional block is empty and exists solely as conditional for
* (re)entering or exiting the loop
*/
const test = lowerExpressionToTemporary(builder, stmt.get("test"));
const terminal: BranchTerminal = {
kind: "branch",
test,
consequent: loopBlock,
alternate: continuationBlock.id,
id: makeInstructionId(0),
};
// Complete the conditional and continue with code after the loop
builder.terminateWithContinuation(terminal, continuationBlock);
return;
}
case "ForOfStatement":
case "ForInStatement":
case "DoWhileStatement":
case "ClassDeclaration":
case "DebuggerStatement":
case "DeclareClass":
+17
View File
@@ -122,6 +122,7 @@ export type ReactiveTerminal =
| ReactiveReturnTerminal
| ReactiveThrowTerminal
| ReactiveSwitchTerminal
| ReactiveDoWhileTerminal
| ReactiveWhileTerminal
| ReactiveForTerminal
| ReactiveIfTerminal;
@@ -157,6 +158,12 @@ export type ReactiveSwitchTerminal = {
}>;
id: InstructionId;
};
export type ReactiveDoWhileTerminal = {
kind: "do-while";
loop: ReactiveBlock;
test: ReactiveValue;
id: InstructionId;
};
export type ReactiveWhileTerminal = {
kind: "while";
test: ReactiveValue;
@@ -241,6 +248,7 @@ export type Terminal =
| BranchTerminal
| SwitchTerminal
| ForTerminal
| DoWhileTerminal
| WhileTerminal
| LogicalTerminal
| TernaryTerminal;
@@ -298,6 +306,15 @@ export type SwitchTerminal = {
id: InstructionId;
};
export type DoWhileTerminal = {
kind: "do-while";
loop: BlockId;
test: BlockId;
fallthrough: BlockId;
id: InstructionId;
loc: SourceLocation;
};
export type WhileTerminal = {
kind: "while";
loc: SourceLocation;
+29
View File
@@ -264,6 +264,7 @@ export default class HIRBuilder {
// then convert to reverse postorder
reversePostorderBlocks(ir);
removeUnreachableFallthroughs(ir);
removeDeadDoWhileStatements(ir);
markInstructionIds(ir);
markPredecessors(ir);
@@ -530,6 +531,30 @@ export function removeUnreachableFallthroughs(func: HIR): void {
}
}
}
export function removeDeadDoWhileStatements(func: HIR): void {
const visited: Set<BlockId> = new Set();
for (const [_, block] of func.blocks) {
visited.add(block.id);
}
// If the test condition of a DoWhile is unreachable, the terminal is effectively deadcode and we
// can just inline the loop body. We replace the terminal with a goto to the loop block and
// MergeConsecutiveBlocks figures out how to merge as appropriate.
for (const [_, block] of func.blocks) {
if (block.terminal.kind === "do-while") {
if (!visited.has(block.terminal.test)) {
block.terminal = {
kind: "goto",
block: block.terminal.loop,
variant: GotoVariant.Break,
id: block.terminal.id,
};
}
}
}
}
/**
* Converts the graph to reverse-postorder, with predecessor blocks appearing
* before successors except in the case of back links (ie loops).
@@ -604,6 +629,10 @@ export function reversePostorderBlocks(func: HIR): void {
visit(terminal.test);
break;
}
case "do-while": {
visit(terminal.loop);
break;
}
case "while": {
visit(terminal.test);
break;
+8 -1
View File
@@ -87,7 +87,8 @@ export function printMixedHIR(
case "while":
case "for":
case "unsupported":
case "goto": {
case "goto":
case "do-while": {
const terminal = printTerminal(value);
if (Array.isArray(terminal)) {
return terminal.join("; ");
@@ -184,6 +185,12 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
value = output;
break;
}
case "do-while": {
value = `[${terminal.id}] DoWhile loop=${`bb${terminal.loop}`} test=bb${
terminal.test
} fallthrough=${`bb${terminal.fallthrough}`}`;
break;
}
case "while": {
value = `[${terminal.id}] While test=bb${terminal.test} loop=${
terminal.loop !== null ? `bb${terminal.loop}` : ""
+19
View File
@@ -366,6 +366,19 @@ export function mapTerminalSuccessors(
case "throw": {
return terminal;
}
case "do-while": {
const loop = fn(terminal.loop);
const test = fn(terminal.test);
const fallthrough = fn(terminal.fallthrough);
return {
kind: "do-while",
loc: terminal.loc,
test,
loop,
fallthrough,
id: makeInstructionId(0),
};
}
case "while": {
const test = fn(terminal.test);
const loop = fn(terminal.loop);
@@ -446,6 +459,10 @@ export function* eachTerminalSuccessor(terminal: Terminal): Iterable<BlockId> {
case "throw": {
break;
}
case "do-while": {
yield terminal.loop;
break;
}
case "while": {
yield terminal.test;
break;
@@ -497,6 +514,7 @@ export function mapTerminalOperands(
}
case "ternary":
case "logical":
case "do-while":
case "while":
case "for":
case "goto":
@@ -542,6 +560,7 @@ export function* eachTerminalOperand(terminal: Terminal): Iterable<Place> {
}
case "ternary":
case "logical":
case "do-while":
case "while":
case "for":
case "goto":
@@ -22,6 +22,7 @@ import {
reversePostorderBlocks,
shrink,
} from "../HIR";
import { removeDeadDoWhileStatements } from "../HIR/HIRBuilder";
import { eliminateRedundantPhi } from "../SSA";
/**
@@ -49,6 +50,7 @@ export function constantPropagation(fn: HIRFunction): void {
shrink(fn.body);
reversePostorderBlocks(fn.body);
removeUnreachableFallthroughs(fn.body);
removeDeadDoWhileStatements(fn.body);
markInstructionIds(fn.body);
markPredecessors(fn.body);
@@ -231,6 +231,55 @@ class Driver {
}
break;
}
case "do-while": {
const fallthroughId = !this.cx.isScheduled(terminal.fallthrough)
? terminal.fallthrough
: null;
const loopId =
!this.cx.isScheduled(terminal.loop) &&
terminal.loop !== terminal.fallthrough
? terminal.loop
: null;
const scheduleId = this.cx.scheduleLoop(
terminal.fallthrough,
terminal.test,
terminal.loop
);
scheduleIds.push(scheduleId);
let loopBody: ReactiveBlock;
if (loopId) {
loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
} else {
const break_ = this.visitBreak(terminal.loop, null);
invariant(
break_ !== null,
"If loop body is already scheduled it must be a break"
);
loopBody = [break_];
}
const testValue = this.visitValueBlock(
terminal.test,
terminal.loc
).value;
this.cx.unscheduleAll(scheduleIds);
blockValue.push({
kind: "terminal",
terminal: {
kind: "do-while",
test: testValue,
loop: loopBody,
id: terminal.id,
},
label: fallthroughId,
});
if (fallthroughId !== null) {
this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
}
break;
}
case "while": {
const fallthroughId =
terminal.fallthrough !== null &&
@@ -340,6 +340,10 @@ function codegenTerminal(
case "throw": {
return t.throwStatement(codegenPlace(cx, terminal.value));
}
case "do-while": {
const test = codegenInstructionValue(cx, terminal.test);
return t.doWhileStatement(test, codegenBlock(cx, terminal.loop));
}
case "while": {
const test = codegenInstructionValue(cx, terminal.test);
return t.whileStatement(test, codegenBlock(cx, terminal.loop));
@@ -46,6 +46,7 @@ class Transform extends ReactiveFunctionTransform<boolean> {
): void {
switch (stmt.terminal.kind) {
// Loop terminals flatten nested scopes
case "do-while":
case "while":
case "for": {
this.traverseTerminal(stmt, true);
@@ -578,6 +578,11 @@ function visit(context: Context, block: ReactiveBlock): void {
});
break;
}
case "do-while": {
visit(context, terminal.loop);
visitReactiveValue(context, terminal.test);
break;
}
case "while": {
visitReactiveValue(context, terminal.test);
context.enterConditional(() => {
@@ -106,6 +106,11 @@ export class ReactiveFunctionVisitor<TState = void> {
this.visitBlock(terminal.loop, state);
break;
}
case "do-while": {
this.visitBlock(terminal.loop, state);
this.visitValue(terminal.id, terminal.test, state);
break;
}
case "while": {
this.visitValue(terminal.id, terminal.test, state);
this.visitBlock(terminal.loop, state);
@@ -309,6 +314,7 @@ export function mapTerminalBlocks(
terminal.loop = fn(terminal.loop);
break;
}
case "do-while":
case "while": {
terminal.loop = fn(terminal.loop);
break;
@@ -107,6 +107,10 @@ function printTerminalLabel(terminal: Terminal): string {
buffer.push("Ternary");
break;
}
case "do-while": {
buffer.push("DoWhile");
break;
}
case "while":
buffer.push("While");
break;
@@ -174,6 +178,12 @@ function printTerminalArrows(blockId: BlockId, terminal: Terminal): string {
}
break;
}
case "do-while": {
buffer.push(printJumpArrow(blockId, terminal.loop, "loop"));
buffer.push(printJumpArrow(blockId, terminal.test, "test"));
buffer.push(printJumpArrow(blockId, terminal.fallthrough, "fallthrough"));
break;
}
case "while": {
buffer.push(printJumpArrow(blockId, terminal.test, "test"));
buffer.push(printJumpArrow(blockId, terminal.loop, "loop"));
@@ -0,0 +1,22 @@
## Input
```javascript
function Component(props) {
do {
break;
} while (props.cond);
return props;
}
```
## Code
```javascript
function Component(props) {
return props;
}
```
@@ -0,0 +1,6 @@
function Component(props) {
do {
break;
} while (props.cond);
return props;
}
@@ -0,0 +1,40 @@
## Input
```javascript
function Component(props) {
let x = [1, 2, 3];
let ret = [];
do {
let item = x.pop();
ret.push(item * 2);
} while (x.length && props.cond);
return ret;
}
```
## Code
```javascript
function Component(props) {
const $ = React.unstable_useMemoCache(2);
const c_0 = $[0] !== props;
let ret;
if (c_0) {
const x = [1, 2, 3];
ret = [];
do {
const item = x.pop();
ret.push(item * 2);
} while (x.length && props.cond);
$[0] = props;
$[1] = ret;
} else {
ret = $[1];
}
return ret;
}
```
@@ -0,0 +1,9 @@
function Component(props) {
let x = [1, 2, 3];
let ret = [];
do {
let item = x.pop();
ret.push(item * 2);
} while (x.length && props.cond);
return ret;
}
@@ -0,0 +1,43 @@
## Input
```javascript
function Component(props) {
let x = [0, 1, 2, 3];
do {
if (x === 0) {
break;
}
mutate(x);
} while (props.cond);
return x;
}
```
## Code
```javascript
function Component(props) {
const $ = React.unstable_useMemoCache(2);
const c_0 = $[0] !== props.cond;
let x;
if (c_0) {
x = [0, 1, 2, 3];
do {
if (x === 0) {
break;
}
mutate(x);
} while (props.cond);
$[0] = props.cond;
$[1] = x;
} else {
x = $[1];
}
return x;
}
```
@@ -0,0 +1,10 @@
function Component(props) {
let x = [0, 1, 2, 3];
do {
if (x === 0) {
break;
}
mutate(x);
} while (props.cond);
return x;
}
@@ -0,0 +1,46 @@
## Input
```javascript
function Component() {
const x = [0, 1, 2, 3];
const ret = [];
do {
const item = x.pop();
if (item === 0) {
continue;
}
ret.push(item / 2);
} while (x.length);
return ret;
}
```
## Code
```javascript
function Component() {
const $ = React.unstable_useMemoCache(1);
let ret;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const x = [0, 1, 2, 3];
ret = [];
do {
const item = x.pop();
if (item === 0) {
continue;
}
ret.push(item / 2);
} while (x.length);
$[0] = ret;
} else {
ret = $[0];
}
return ret;
}
```
@@ -0,0 +1,13 @@
function Component() {
const x = [0, 1, 2, 3];
const ret = [];
do {
const item = x.pop();
if (item === 0) {
continue;
}
ret.push(item / 2);
} while (x.length);
return ret;
}
@@ -0,0 +1,34 @@
## Input
```javascript
function Component(props) {
let x = [1, 2, 3];
do {
mutate(x);
break;
} while (props.cond);
return x;
}
```
## Code
```javascript
function Component(props) {
const $ = React.unstable_useMemoCache(1);
let x;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
x = [1, 2, 3];
mutate(x);
$[0] = x;
} else {
x = $[0];
}
return x;
}
```
@@ -0,0 +1,8 @@
function Component(props) {
let x = [1, 2, 3];
do {
mutate(x);
break;
} while (props.cond);
return x;
}
@@ -0,0 +1,38 @@
## Input
```javascript
function Component() {
let x = [1, 2, 3];
let ret = [];
do {
let item = x.pop();
ret.push(item * 2);
} while (x.length);
return ret;
}
```
## Code
```javascript
function Component() {
const $ = React.unstable_useMemoCache(1);
let ret;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const x = [1, 2, 3];
ret = [];
do {
const item = x.pop();
ret.push(item * 2);
} while (x.length);
$[0] = ret;
} else {
ret = $[0];
}
return ret;
}
```
@@ -0,0 +1,9 @@
function Component() {
let x = [1, 2, 3];
let ret = [];
do {
let item = x.pop();
ret.push(item * 2);
} while (x.length);
return ret;
}
@@ -50,8 +50,6 @@ function foo([a, b], { c, d, e = "e" }, f = "f", ...args) {
for (let x in { a: 1 }) {
}
do {} while (i < 3);
let updateIdentifier = 0;
--updateIdentifier;
++updateIdentifier;
@@ -333,97 +331,88 @@ let moduleLocal = false;
| ^
47 | }
48 |
49 | do {} while (i < 3);
49 | let updateIdentifier = 0;
[ReactForget] TodoError: (BuildHIR::lowerStatement) Handle DoWhileStatement statements
47 | }
[ReactForget] TodoError: (BuildHIR::lowerExpression) Handle prefix UpdateExpression
48 |
> 49 | do {} while (i < 3);
| ^^^^^^^^^^^^^^^^^^^^
50 |
51 | let updateIdentifier = 0;
52 | --updateIdentifier;
49 | let updateIdentifier = 0;
> 50 | --updateIdentifier;
| ^^^^^^^^^^^^^^^^^^
51 | ++updateIdentifier;
52 | updateIdentifier.y++;
53 | updateIdentifier.y--;
[ReactForget] TodoError: (BuildHIR::lowerExpression) Handle prefix UpdateExpression
50 |
51 | let updateIdentifier = 0;
> 52 | --updateIdentifier;
49 | let updateIdentifier = 0;
50 | --updateIdentifier;
> 51 | ++updateIdentifier;
| ^^^^^^^^^^^^^^^^^^
53 | ++updateIdentifier;
54 | updateIdentifier.y++;
55 | updateIdentifier.y--;
[ReactForget] TodoError: (BuildHIR::lowerExpression) Handle prefix UpdateExpression
51 | let updateIdentifier = 0;
52 | --updateIdentifier;
> 53 | ++updateIdentifier;
| ^^^^^^^^^^^^^^^^^^
54 | updateIdentifier.y++;
55 | updateIdentifier.y--;
56 |
52 | updateIdentifier.y++;
53 | updateIdentifier.y--;
54 |
[ReactForget] TodoError: (BuildHIR::lowerExpression) Handle UpdateExpression with MemberExpression argument
52 | --updateIdentifier;
53 | ++updateIdentifier;
> 54 | updateIdentifier.y++;
50 | --updateIdentifier;
51 | ++updateIdentifier;
> 52 | updateIdentifier.y++;
| ^^^^^^^^^^^^^^^^^^^^
55 | updateIdentifier.y--;
56 |
57 | switch (i) {
53 | updateIdentifier.y--;
54 |
55 | switch (i) {
[ReactForget] TodoError: (BuildHIR::lowerExpression) Handle UpdateExpression with MemberExpression argument
53 | ++updateIdentifier;
54 | updateIdentifier.y++;
> 55 | updateIdentifier.y--;
51 | ++updateIdentifier;
52 | updateIdentifier.y++;
> 53 | updateIdentifier.y--;
| ^^^^^^^^^^^^^^^^^^^^
56 |
57 | switch (i) {
58 | case 1 + 1: {
54 |
55 | switch (i) {
56 | case 1 + 1: {
[ReactForget] TodoError: (BuildHIR::lowerStatement) Switch case test values must be identifiers or primitives, compound values are not yet supported
60 | case foo(): {
61 | }
> 62 | case x.y: {
58 | case foo(): {
59 | }
> 60 | case x.y: {
| ^^^
63 | }
64 | default: {
65 | }
[ReactForget] TodoError: (BuildHIR::lowerStatement) Switch case test values must be identifiers or primitives, compound values are not yet supported
58 | case 1 + 1: {
59 | }
> 60 | case foo(): {
| ^^^^^
61 | }
62 | case x.y: {
62 | default: {
63 | }
[ReactForget] TodoError: (BuildHIR::lowerStatement) Switch case test values must be identifiers or primitives, compound values are not yet supported
56 |
57 | switch (i) {
> 58 | case 1 + 1: {
56 | case 1 + 1: {
57 | }
> 58 | case foo(): {
| ^^^^^
59 | }
60 | case foo(): {
60 | case x.y: {
61 | }
[ReactForget] TodoError: (BuildHIR::lowerStatement) Switch case test values must be identifiers or primitives, compound values are not yet supported
54 |
55 | switch (i) {
> 56 | case 1 + 1: {
| ^^^^^
57 | }
58 | case foo(): {
59 | }
[ReactForget] InvalidInputError: (BuildHIR::lowerAssignment) Assigning to an identifier defined outside the function scope is not supported.
67 |
68 | // Cannot assign to globals
> 69 | someUnknownGlobal = true;
65 |
66 | // Cannot assign to globals
> 67 | someUnknownGlobal = true;
| ^^^^^^^^^^^^^^^^^
70 | moduleLocal = true;
71 | }
72 |
68 | moduleLocal = true;
69 | }
70 |
[ReactForget] InvalidInputError: (BuildHIR::lowerAssignment) Assigning to an identifier defined outside the function scope is not supported.
68 | // Cannot assign to globals
69 | someUnknownGlobal = true;
> 70 | moduleLocal = true;
66 | // Cannot assign to globals
67 | someUnknownGlobal = true;
> 68 | moduleLocal = true;
| ^^^^^^^^^^^
71 | }
72 |
73 | let moduleLocal = false;
69 | }
70 |
71 | let moduleLocal = false;
```
@@ -46,8 +46,6 @@ function foo([a, b], { c, d, e = "e" }, f = "f", ...args) {
for (let x in { a: 1 }) {
}
do {} while (i < 3);
let updateIdentifier = 0;
--updateIdentifier;
++updateIdentifier;