mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[HIR] add for terminal
The changes here are pretty significant and there's a bunch more left: - support for with any of `<init>`, `<test>` or `<update>` empty. - support for with `<init>` as `Expression` instead of VariableDeclaration` node - support assignment expressions in `<update>`, this seems like it might require further new abstractions to allow something like a block to codegen into a single expression.
This commit is contained in:
@@ -317,90 +317,77 @@ function lowerStatement(
|
||||
}
|
||||
case "ForStatement": {
|
||||
const stmt = stmtPath as NodePath<t.ForStatement>;
|
||||
/**
|
||||
* The initializer is evaluated once prior to entering the loop.
|
||||
* here we are not concerned about scoping, so we can push the
|
||||
* initializer to the end of of the block leading up to the loop
|
||||
*/
|
||||
const init = stmt.get("init");
|
||||
if (init.hasNode()) {
|
||||
// builder.push(init);
|
||||
throw new Error("todo: lower initializer in ForStatement");
|
||||
}
|
||||
// Block used to evaluate whether to (re)enter or exit the loop
|
||||
const conditionalBlock = builder.reserve();
|
||||
|
||||
const testBlock = builder.reserve();
|
||||
// Block for code following the loop
|
||||
const continuationBlock = builder.reserve();
|
||||
/**
|
||||
* Block for the updater, which runs after each iteration (including upon `continue`)
|
||||
* Generally this would increment or decrement the loop index variable
|
||||
*/
|
||||
const updateBlock = builder.reserve();
|
||||
const update = stmt.get("update");
|
||||
if (update.hasNode()) {
|
||||
// updateBlock[1].push(stmt.get("update") as any as NodePath<t.Statement>);
|
||||
throw new Error("todo: lower updater for ForStatement");
|
||||
}
|
||||
builder.complete(updateBlock, {
|
||||
kind: "goto",
|
||||
block: conditionalBlock.id,
|
||||
variant: GotoVariant.Break,
|
||||
id: makeInstructionId(0),
|
||||
|
||||
const initBlock = builder.enter((blockId) => {
|
||||
const init = stmt.get("init") as NodePath<t.VariableDeclaration>;
|
||||
todoInvariant(
|
||||
t.isVariableDeclaration(init.node),
|
||||
"handle non variable initialization in for"
|
||||
);
|
||||
lowerStatement(builder, init);
|
||||
return {
|
||||
kind: "goto",
|
||||
block: testBlock.id,
|
||||
variant: GotoVariant.Break,
|
||||
id: makeInstructionId(0),
|
||||
};
|
||||
});
|
||||
/**
|
||||
* Construct the loop itself: the loop body wraps around to the update block
|
||||
* and the update block is also set as the `continue` target
|
||||
*/
|
||||
const loopBlock = builder.enter((blockId) => {
|
||||
return builder.loop(label, updateBlock.id, continuationBlock.id, () => {
|
||||
|
||||
const updateBlock = builder.enter((blockId) => {
|
||||
const update = stmt.get("update");
|
||||
if (update.hasNode()) {
|
||||
lowerExpressionToVoid(builder, update);
|
||||
}
|
||||
return {
|
||||
kind: "goto",
|
||||
block: testBlock.id,
|
||||
variant: GotoVariant.Break,
|
||||
id: makeInstructionId(0),
|
||||
};
|
||||
});
|
||||
|
||||
const bodyBlock = builder.enter((blockId) => {
|
||||
return builder.loop(label, updateBlock, continuationBlock.id, () => {
|
||||
lowerStatement(builder, stmt.get("body"));
|
||||
return {
|
||||
kind: "goto",
|
||||
block: updateBlock.id,
|
||||
block: updateBlock,
|
||||
variant: GotoVariant.Continue,
|
||||
id: makeInstructionId(0),
|
||||
};
|
||||
});
|
||||
});
|
||||
// End the block leading up to the loop, jumping to the conditional block
|
||||
|
||||
builder.terminateWithContinuation(
|
||||
{
|
||||
kind: "goto",
|
||||
block: conditionalBlock.id,
|
||||
variant: GotoVariant.Break,
|
||||
kind: "for",
|
||||
init: initBlock,
|
||||
test: testBlock.id,
|
||||
update: updateBlock,
|
||||
loop: bodyBlock,
|
||||
fallthrough: continuationBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
},
|
||||
conditionalBlock
|
||||
testBlock
|
||||
);
|
||||
|
||||
let terminal: Terminal;
|
||||
const test = stmt.get("test");
|
||||
if (test.hasNode()) {
|
||||
/**
|
||||
* Terminate the conditional block with the test conditional of the for statement:
|
||||
* if the condition is true enter the loop block, else exit to the continuation
|
||||
*/
|
||||
terminal = {
|
||||
todoInvariant(test.hasNode(), "ForStatement without test");
|
||||
builder.terminateWithContinuation(
|
||||
{
|
||||
kind: "if",
|
||||
test: lowerExpressionToPlace(builder, test),
|
||||
consequent: loopBlock,
|
||||
consequent: bodyBlock,
|
||||
alternate: continuationBlock.id,
|
||||
fallthrough: continuationBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
};
|
||||
} else {
|
||||
/**
|
||||
* If there is no test, then the "conditional" block unconditionally re-enters the loop.
|
||||
* this will create an indirection, but `shrink()` will eliminate this in post-processing.
|
||||
*/
|
||||
terminal = {
|
||||
kind: "goto",
|
||||
block: loopBlock,
|
||||
variant: GotoVariant.Break,
|
||||
id: makeInstructionId(0),
|
||||
};
|
||||
}
|
||||
builder.terminateWithContinuation(terminal, continuationBlock);
|
||||
},
|
||||
continuationBlock
|
||||
);
|
||||
return;
|
||||
}
|
||||
case "DoWhileStatement": {
|
||||
@@ -1278,6 +1265,22 @@ function lowerExpressionToPlace(
|
||||
return place;
|
||||
}
|
||||
|
||||
function lowerExpressionToVoid(
|
||||
builder: HIRBuilder,
|
||||
exprPath: NodePath<t.Expression>
|
||||
): void {
|
||||
const instr = lowerExpression(builder, exprPath);
|
||||
if (instr.kind !== "Identifier") {
|
||||
const exprLoc = exprPath.node.loc ?? GeneratedSource;
|
||||
builder.push({
|
||||
id: makeInstructionId(0),
|
||||
value: instr,
|
||||
loc: exprLoc,
|
||||
lvalue: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function lowerLVal(builder: HIRBuilder, exprPath: NodePath<t.LVal>): Place {
|
||||
const exprNode = exprPath.node;
|
||||
const exprLoc = exprNode.loc ?? GeneratedSource;
|
||||
|
||||
@@ -190,6 +190,14 @@ class CodegenVisitor
|
||||
case "while": {
|
||||
return createWhileStatement(terminal.loc, terminal.test, terminal.loop);
|
||||
}
|
||||
case "for": {
|
||||
return t.forStatement(
|
||||
terminal.init as any, // TODO: make sure it's a variable declaration
|
||||
terminal.test,
|
||||
terminal.update,
|
||||
terminal.loop
|
||||
);
|
||||
}
|
||||
case "return": {
|
||||
const createReturnStatement = withLoc(t.returnStatement);
|
||||
if (terminal.value !== null) {
|
||||
|
||||
@@ -109,6 +109,7 @@ export type Terminal =
|
||||
| GotoTerminal
|
||||
| IfTerminal
|
||||
| SwitchTerminal
|
||||
| ForTerminal
|
||||
| WhileTerminal;
|
||||
|
||||
export type ThrowTerminal = { kind: "throw"; value: Place; id: InstructionId };
|
||||
@@ -158,6 +159,16 @@ export type WhileTerminal = {
|
||||
id: InstructionId;
|
||||
};
|
||||
|
||||
export type ForTerminal = {
|
||||
kind: "for";
|
||||
init: BlockId;
|
||||
test: BlockId;
|
||||
update: BlockId;
|
||||
loop: BlockId;
|
||||
fallthrough: BlockId;
|
||||
id: InstructionId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Instructions generally represent expressions but with all nesting flattened away,
|
||||
* such that all operands to each instruction are either primitive values OR are
|
||||
|
||||
@@ -365,7 +365,8 @@ function shrink(func: HIR): HIR {
|
||||
if (target !== null) {
|
||||
return target;
|
||||
}
|
||||
const block = func.blocks.get(blockId)!;
|
||||
const block = func.blocks.get(blockId);
|
||||
invariant(block != null, "expected block %s to exist", blockId);
|
||||
target = getTargetIfIndirection(block);
|
||||
if (target !== null) {
|
||||
// the target might also be a simple goto, recurse
|
||||
@@ -485,6 +486,10 @@ function reversePostorderBlocks(func: HIR): HIR {
|
||||
visit(terminal.test);
|
||||
break;
|
||||
}
|
||||
case "for": {
|
||||
visit(terminal.init);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
terminal,
|
||||
|
||||
@@ -311,6 +311,113 @@ class Driver<TBlock, TValue, TItem, TCase> {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "for": {
|
||||
const loopId =
|
||||
!this.cx.isScheduled(terminal.loop) &&
|
||||
terminal.loop !== terminal.fallthrough
|
||||
? terminal.loop
|
||||
: null;
|
||||
|
||||
const fallthroughId =
|
||||
terminal.fallthrough !== null &&
|
||||
!this.cx.isScheduled(terminal.fallthrough)
|
||||
? terminal.fallthrough
|
||||
: null;
|
||||
|
||||
const scheduleId = this.cx.scheduleLoop(
|
||||
terminal.fallthrough,
|
||||
terminal.update,
|
||||
terminal.loop
|
||||
);
|
||||
scheduleIds.push(scheduleId);
|
||||
|
||||
const initBlock = this.cx.ir.blocks.get(terminal.init)!;
|
||||
const initTerminal = initBlock.terminal;
|
||||
invariant(
|
||||
initTerminal.kind === "goto",
|
||||
"Expected for loop init block to end in a goto"
|
||||
);
|
||||
|
||||
let initItem;
|
||||
for (const instr of initBlock.instructions) {
|
||||
const value = this.visitor.visitValue(instr.value, instr.id);
|
||||
initItem = this.visitor.visitInstruction(instr, value);
|
||||
}
|
||||
|
||||
const testBlock = this.cx.ir.blocks.get(terminal.test)!;
|
||||
const testTerminal = testBlock.terminal;
|
||||
invariant(
|
||||
testTerminal.kind === "if",
|
||||
"Expected for loop test block to end in an if"
|
||||
);
|
||||
const testValueBlock = this.visitor.enterValueBlock();
|
||||
for (const instr of testBlock.instructions) {
|
||||
const value = this.visitor.visitValue(instr.value, instr.id);
|
||||
const item = this.visitor.visitInstruction(instr, value);
|
||||
this.visitor.appendBlock(testValueBlock, item);
|
||||
}
|
||||
const testValueLast = this.visitor.visitValue(
|
||||
testTerminal.test,
|
||||
testTerminal.id
|
||||
);
|
||||
const testValue = this.visitor.leaveValueBlock(
|
||||
testValueBlock,
|
||||
testValueLast
|
||||
);
|
||||
|
||||
const updateBlock = this.cx.ir.blocks.get(terminal.update)!;
|
||||
const updateTerminal = updateBlock.terminal;
|
||||
invariant(
|
||||
updateTerminal.kind === "goto",
|
||||
"Expected for loop update block to end in a goto"
|
||||
);
|
||||
let updateValue;
|
||||
for (const instr of updateBlock.instructions) {
|
||||
updateValue = this.visitor.visitValue(instr.value, instr.id);
|
||||
}
|
||||
|
||||
let loopBody: TItem;
|
||||
if (loopId) {
|
||||
loopBody = this.traverseBlock(this.cx.ir.blocks.get(loopId)!);
|
||||
} else {
|
||||
const break_ = this.visitBreak(terminal.loop);
|
||||
invariant(
|
||||
break_ !== null,
|
||||
"If loop body is already scheduled it must be a break"
|
||||
);
|
||||
const body = this.visitor.enterBlock();
|
||||
this.visitor.appendBlock(body, break_);
|
||||
loopBody = this.visitor.leaveBlock(body);
|
||||
}
|
||||
|
||||
this.cx.unscheduleAll(scheduleIds);
|
||||
if (fallthroughId !== null) {
|
||||
this.visitor.appendBlock(
|
||||
blockValue,
|
||||
this.visitor.visitTerminal({
|
||||
kind: "for",
|
||||
init: initItem as any,
|
||||
test: testValue,
|
||||
update: updateValue as any,
|
||||
loop: loopBody,
|
||||
}),
|
||||
fallthroughId
|
||||
);
|
||||
this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
|
||||
} else {
|
||||
this.visitor.appendBlock(
|
||||
blockValue,
|
||||
this.visitor.visitTerminal({
|
||||
kind: "for",
|
||||
init: initItem as any,
|
||||
test: testValue,
|
||||
update: updateValue as any,
|
||||
loop: loopBody,
|
||||
})
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "goto": {
|
||||
this.visitor.visitTerminalId(terminal.id);
|
||||
switch (terminal.variant) {
|
||||
@@ -722,5 +829,12 @@ export type BlockTerminal<TBlock, TValue, TItem, TCase> =
|
||||
test: TValue;
|
||||
loop: TItem;
|
||||
}
|
||||
| {
|
||||
kind: "for";
|
||||
init: TItem;
|
||||
test: TValue;
|
||||
update: TValue;
|
||||
loop: TItem;
|
||||
}
|
||||
| { kind: "break"; label: BlockId | null }
|
||||
| { kind: "continue"; label: BlockId | null };
|
||||
|
||||
@@ -102,6 +102,7 @@ class ScopeDependenciesVisitor
|
||||
switch (terminal.kind) {
|
||||
case "if":
|
||||
case "switch":
|
||||
case "for":
|
||||
case "while": {
|
||||
for (const operand of eachInstructionValueOperand(terminal.test)) {
|
||||
this.#addTerminalDependency(operand);
|
||||
|
||||
@@ -82,6 +82,7 @@ export function printMixedHIR(
|
||||
case "switch":
|
||||
case "throw":
|
||||
case "while":
|
||||
case "for":
|
||||
case "goto": {
|
||||
const terminal = printTerminal(value);
|
||||
if (Array.isArray(terminal)) {
|
||||
@@ -165,6 +166,10 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
|
||||
} fallthrough=${terminal.fallthrough ? `bb${terminal.fallthrough}` : ""}`;
|
||||
break;
|
||||
}
|
||||
case "for": {
|
||||
value = `[${terminal.id}] For init=bb${terminal.init} test=bb${terminal.test} loop=bb${terminal.loop} update=bb${terminal.update} fallthrough=bb${terminal.fallthrough}`;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
terminal,
|
||||
|
||||
@@ -94,6 +94,12 @@ class PrintVisitor implements Visitor<Array<string>, string, string, string> {
|
||||
value = `While (${terminal.test}) ${terminal.loop.trimStart()}`;
|
||||
break;
|
||||
}
|
||||
case "for": {
|
||||
value = `For (TODO) (${
|
||||
terminal.test
|
||||
}) (TODO) ${terminal.loop.trimStart()}`;
|
||||
break;
|
||||
}
|
||||
case "return": {
|
||||
if (terminal.value !== null) {
|
||||
value = `Return ${terminal.value}`;
|
||||
|
||||
@@ -98,6 +98,9 @@ function printTerminalLabel(terminal: Terminal): string {
|
||||
case "while":
|
||||
buffer.push("While");
|
||||
break;
|
||||
case "for":
|
||||
buffer.push("For");
|
||||
break;
|
||||
default:
|
||||
assertExhaustive(terminal, `unhandled terminal ${terminal}`);
|
||||
}
|
||||
@@ -147,6 +150,14 @@ function printTerminalArrows(blockId: BlockId, terminal: Terminal): string {
|
||||
buffer.push(printJumpArrow(blockId, terminal.fallthrough, "fallthrough"));
|
||||
break;
|
||||
}
|
||||
case "for": {
|
||||
buffer.push(printJumpArrow(blockId, terminal.init, "init"));
|
||||
buffer.push(printJumpArrow(blockId, terminal.test, "test"));
|
||||
buffer.push(printJumpArrow(blockId, terminal.update, "update"));
|
||||
buffer.push(printJumpArrow(blockId, terminal.loop, "loop"));
|
||||
buffer.push(printJumpArrow(blockId, terminal.fallthrough, "fallthrough"));
|
||||
break;
|
||||
}
|
||||
case "throw":
|
||||
case "return": {
|
||||
break;
|
||||
|
||||
@@ -214,6 +214,22 @@ export function mapTerminalSuccessors(
|
||||
id: makeInstructionId(0),
|
||||
};
|
||||
}
|
||||
case "for": {
|
||||
const init = fn(terminal.init);
|
||||
const test = fn(terminal.test);
|
||||
const update = fn(terminal.update);
|
||||
const loop = fn(terminal.loop);
|
||||
const fallthrough = fn(terminal.fallthrough);
|
||||
return {
|
||||
kind: "for",
|
||||
init,
|
||||
test,
|
||||
update,
|
||||
loop,
|
||||
fallthrough,
|
||||
id: makeInstructionId(0),
|
||||
};
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
terminal,
|
||||
@@ -255,6 +271,10 @@ export function* eachTerminalSuccessor(terminal: Terminal): Iterable<BlockId> {
|
||||
yield terminal.test;
|
||||
break;
|
||||
}
|
||||
case "for": {
|
||||
yield terminal.init;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
terminal,
|
||||
@@ -291,6 +311,7 @@ export function mapTerminalOperands(
|
||||
break;
|
||||
}
|
||||
case "while":
|
||||
case "for":
|
||||
case "goto": {
|
||||
// no-op
|
||||
break;
|
||||
@@ -328,6 +349,7 @@ export function* eachTerminalOperand(terminal: Terminal): Iterable<Place> {
|
||||
break;
|
||||
}
|
||||
case "while":
|
||||
case "for":
|
||||
case "goto": {
|
||||
// no-op
|
||||
break;
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
```javascript
|
||||
function foo() {
|
||||
let x = 0;
|
||||
for (; x < 10; ) {
|
||||
x = x + 1;
|
||||
let x = 1;
|
||||
for (let i = 0; i < 10; update()) {
|
||||
x += 1;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
@@ -16,23 +16,32 @@ function foo() {
|
||||
|
||||
```
|
||||
bb0:
|
||||
[1] Let mutate x$5_@0[0:8] = 0
|
||||
[2] Goto bb1
|
||||
[1] Let mutate x$7_@0[0:13] = 1
|
||||
[2] For init=bb3 test=bb1 loop=bb5 update=bb4 fallthrough=bb2
|
||||
bb3:
|
||||
predecessor blocks: bb0
|
||||
[3] Let mutate i$8_@1[3:13] = 0
|
||||
[4] Goto bb1
|
||||
bb1:
|
||||
predecessor blocks: bb0 bb4
|
||||
x$7_@0[0:8]: phi(bb0: x$5_@0, bb4: x$10_@0)
|
||||
[3] Const mutate $6_@1 = 10
|
||||
[4] Const mutate $8_@2 = Binary read x$7_@0 < read $6_@1
|
||||
[5] If (read $8_@2) then:bb4 else:bb2
|
||||
bb4:
|
||||
predecessor blocks: bb3 bb4
|
||||
x$13_@0[0:13]: phi(bb3: x$7_@0, bb4: x$14_@0)
|
||||
[5] Const mutate $9_@2 = 10
|
||||
[6] Const mutate $11_@3[6:8] = Binary read i$8_@1 < read $9_@2
|
||||
[7] If (read $11_@3) then:bb5 else:bb2
|
||||
bb5:
|
||||
predecessor blocks: bb1
|
||||
[6] Const mutate $9_@3 = 1
|
||||
[7] Reassign mutate x$10_@0[0:8] = Binary read x$7_@0 + read $9_@3
|
||||
[8] Goto(Continue) bb1
|
||||
[8] Const mutate $12_@4 = 1
|
||||
[9] Reassign mutate x$14_@0[0:13] = Binary read x$13_@0 + read $12_@4
|
||||
[10] Goto(Continue) bb4
|
||||
bb4:
|
||||
predecessor blocks: bb5
|
||||
[11] Call mutate update$3_@5()
|
||||
[12] Goto bb1
|
||||
bb2:
|
||||
predecessor blocks: bb1
|
||||
[9] Return read x$7_@0
|
||||
|
||||
[13] Return read x$13_@0
|
||||
scope3 [6:8]:
|
||||
- read $9_@2
|
||||
```
|
||||
|
||||
### CFG
|
||||
@@ -42,32 +51,50 @@ flowchart TB
|
||||
%% Basic Blocks
|
||||
subgraph bb0
|
||||
bb0_instrs["
|
||||
[1] Let mutate x$5_@0[0:8] = 0
|
||||
[1] Let mutate x$7_@0[0:13] = 1
|
||||
"]
|
||||
bb0_instrs --> bb0_terminal(["Goto"])
|
||||
bb0_instrs --> bb0_terminal(["For"])
|
||||
end
|
||||
subgraph bb3
|
||||
bb3_instrs["
|
||||
[3] Let mutate i$8_@1[3:13] = 0
|
||||
"]
|
||||
bb3_instrs --> bb3_terminal(["Goto"])
|
||||
end
|
||||
subgraph bb1
|
||||
bb1_instrs["
|
||||
[3] Const mutate $6_@1 = 10
|
||||
[4] Const mutate $8_@2 = Binary read x$7_@0 < read $6_@1
|
||||
[5] Const mutate $9_@2 = 10
|
||||
[6] Const mutate $11_@3[6:8] = Binary read i$8_@1 < read $9_@2
|
||||
"]
|
||||
bb1_instrs --> bb1_terminal(["If (read $8_@2)"])
|
||||
bb1_instrs --> bb1_terminal(["If (read $11_@3)"])
|
||||
end
|
||||
subgraph bb5
|
||||
bb5_instrs["
|
||||
[8] Const mutate $12_@4 = 1
|
||||
[9] Reassign mutate x$14_@0[0:13] = Binary read x$13_@0 + read $12_@4
|
||||
"]
|
||||
bb5_instrs --> bb5_terminal(["Goto"])
|
||||
end
|
||||
subgraph bb4
|
||||
bb4_instrs["
|
||||
[6] Const mutate $9_@3 = 1
|
||||
[7] Reassign mutate x$10_@0[0:8] = Binary read x$7_@0 + read $9_@3
|
||||
[11] Call mutate update$3_@5()
|
||||
"]
|
||||
bb4_instrs --> bb4_terminal(["Goto"])
|
||||
end
|
||||
subgraph bb2
|
||||
bb2_terminal(["Return read x$7_@0"])
|
||||
bb2_terminal(["Return read x$13_@0"])
|
||||
end
|
||||
|
||||
%% Jumps
|
||||
bb0_terminal --> bb1
|
||||
bb1_terminal -- "then" --> bb4
|
||||
bb0_terminal -- "init" --> bb3
|
||||
bb0_terminal -- "test" --> bb1
|
||||
bb0_terminal -- "update" --> bb4
|
||||
bb0_terminal -- "loop" --> bb5
|
||||
bb0_terminal -- "fallthrough" --> bb2
|
||||
bb3_terminal --> bb1
|
||||
bb1_terminal -- "then" --> bb5
|
||||
bb1_terminal -- "else" --> bb2
|
||||
bb5_terminal --> bb4
|
||||
bb4_terminal --> bb1
|
||||
|
||||
```
|
||||
@@ -76,7 +103,12 @@ flowchart TB
|
||||
|
||||
```javascript
|
||||
function foo$0() {
|
||||
let x$1 = 0;
|
||||
let x$1 = 1;
|
||||
bb2: for (let i$2 = 0; i$2 < 10; update$3()) {
|
||||
x$1 = x$1 + 1;
|
||||
}
|
||||
|
||||
return x$1;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
function foo() {
|
||||
let x = 0;
|
||||
for (; x < 10; ) {
|
||||
x = x + 1;
|
||||
let x = 1;
|
||||
for (let i = 0; i < 10; update()) {
|
||||
x += 1;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user