fix for while (...) { break } edge case

Small adjustment to the previous PR for a special case: 

```javascript 

while (cond) { 

break; 

} 

``` 

The loop body is an indirection to the fallthrough, so shrink() collapses that 
and makes the while.loop === while.fallthrough. We now detect that this is the 
case in codegen and correctly emit a `break` rather than trying to write the 
fallthrough block inside the loop.
This commit is contained in:
Joe Savona
2022-11-09 20:52:08 -08:00
parent 656b8c9a0e
commit f57bcacced
4 changed files with 71 additions and 18 deletions
+27 -12
View File
@@ -96,7 +96,10 @@ class Context {
*/ */
schedule(block: BlockId, type: "if" | "switch" | "case"): number { schedule(block: BlockId, type: "if" | "switch" | "case"): number {
const id = this.#nextScheduleId++; const id = this.#nextScheduleId++;
invariant(!this.#scheduled.has(block), "Block is already scheduled"); invariant(
!this.#scheduled.has(block),
`Break block is already scheduled: bb${block}`
);
this.#scheduled.add(block); this.#scheduled.add(block);
this.#controlFlowStack.push({ block, id, type }); this.#controlFlowStack.push({ block, id, type });
return id; return id;
@@ -112,11 +115,12 @@ class Context {
this.#scheduled.add(fallthroughBlock); this.#scheduled.add(fallthroughBlock);
invariant( invariant(
!this.#scheduled.has(continueBlock), !this.#scheduled.has(continueBlock),
"Block is already scheduled" `Continue block is already scheduled: bb${continueBlock}`
); );
this.#scheduled.add(continueBlock); this.#scheduled.add(continueBlock);
let ownsLoop = false;
if (loopBlock !== null) { if (loopBlock !== null) {
invariant(!this.#scheduled.has(loopBlock), "Block is already scheduled"); ownsLoop = !this.#scheduled.has(loopBlock);
this.#scheduled.add(loopBlock); this.#scheduled.add(loopBlock);
} }
@@ -127,6 +131,7 @@ class Context {
type: "loop", type: "loop",
continueBlock, continueBlock,
loopBlock, loopBlock,
ownsLoop,
}); });
return id; return id;
} }
@@ -145,7 +150,7 @@ class Context {
} }
if (last.type === "loop") { if (last.type === "loop") {
this.#scheduled.delete(last.continueBlock); this.#scheduled.delete(last.continueBlock);
if (last.loopBlock !== null) { if (last.ownsLoop && last.loopBlock !== null) {
this.#scheduled.delete(last.loopBlock); this.#scheduled.delete(last.loopBlock);
} }
} }
@@ -266,21 +271,22 @@ type ControlFlowTarget =
ownsBlock: boolean; ownsBlock: boolean;
continueBlock: BlockId; continueBlock: BlockId;
loopBlock: BlockId | null; loopBlock: BlockId | null;
ownsLoop: boolean;
id: number; id: number;
}; };
function codegenBlock(cx: Context, block: BasicBlock): t.BlockStatement { function codegenBlock(cx: Context, block: BasicBlock): t.BlockStatement {
invariant(
!cx.emitted.has(block.id),
`Cannot emit the same block twice: bb${block.id}`
);
cx.emitted.add(block.id);
const body: Array<t.Statement> = []; const body: Array<t.Statement> = [];
writeBlock(cx, block, body); writeBlock(cx, block, body);
return t.blockStatement(body); return t.blockStatement(body);
} }
function writeBlock(cx: Context, block: BasicBlock, body: Array<t.Statement>) { function writeBlock(cx: Context, block: BasicBlock, body: Array<t.Statement>) {
invariant(
!cx.emitted.has(block.id),
`Cannot emit the same block twice: bb${block.id}`
);
cx.emitted.add(block.id);
for (const instr of block.instructions) { for (const instr of block.instructions) {
writeInstr(cx, instr, body); writeInstr(cx, instr, body);
} }
@@ -440,6 +446,10 @@ function writeBlock(cx: Context, block: BasicBlock, body: Array<t.Statement>) {
terminal.fallthrough !== null && !cx.isScheduled(terminal.fallthrough) terminal.fallthrough !== null && !cx.isScheduled(terminal.fallthrough)
? terminal.fallthrough ? terminal.fallthrough
: null; : null;
const loopId =
!cx.isScheduled(terminal.loop) && terminal.loop !== terminal.fallthrough
? terminal.loop
: null;
const scheduleId = cx.scheduleLoop( const scheduleId = cx.scheduleLoop(
terminal.fallthrough, terminal.fallthrough,
terminal.test, terminal.test,
@@ -448,10 +458,15 @@ function writeBlock(cx: Context, block: BasicBlock, body: Array<t.Statement>) {
scheduleIds.push(scheduleId); scheduleIds.push(scheduleId);
let loopBody: t.Statement; let loopBody: t.Statement;
if (terminal.loop !== null) { if (loopId) {
loopBody = codegenBlock(cx, cx.ir.blocks.get(terminal.loop)!); loopBody = codegenBlock(cx, cx.ir.blocks.get(loopId)!);
} else { } else {
loopBody = t.blockStatement([]); const break_ = codegenBreak(cx, terminal.loop);
invariant(
break_ !== null,
"If loop body is already scheduled it must be a break"
);
loopBody = t.blockStatement([break_]);
} }
cx.unscheduleAll(scheduleIds); cx.unscheduleAll(scheduleIds);
-6
View File
@@ -403,12 +403,6 @@ function shrink(func: HIR): HIR {
block.terminal.fallthrough = null; block.terminal.fallthrough = null;
} }
} }
if (block.terminal.kind === "while") {
invariant(
resolveBlockTarget(block.terminal.loop) === block.terminal.loop,
`Expected while loop body to remain after shrinking`
);
}
} }
return { blocks, entry: func.entry }; return { blocks, entry: func.entry };
} }
@@ -0,0 +1,38 @@
## Input
```javascript
function foo(a, b) {
while (a) {
break;
}
return b;
}
```
## HIR
```
bb0:
While test=bb1 loop=bb2 fallthrough=bb2
bb1:
predecessor blocks: bb0
If (read a$3) then:bb2 else:bb2
bb2:
predecessor blocks: bb1
Return read b$4
```
## Code
```javascript
function foo$0(a$3, b$4) {
bb2: while (a$3) {
break;
}
return b$4;
}
```
@@ -0,0 +1,6 @@
function foo(a, b) {
while (a) {
break;
}
return b;
}