[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:
Jan Kassens
2022-12-06 12:01:32 -05:00
parent db740366f8
commit 4837e21de3
12 changed files with 310 additions and 92 deletions
+64 -61
View File
@@ -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;
+8
View File
@@ -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) {
+11
View File
@@ -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
+6 -1
View File
@@ -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,
+114
View File
@@ -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);
+5
View File
@@ -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,
+6
View File
@@ -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;
+22
View File
@@ -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;
}