From 4837e21de37098dc16fa78ad2f24495524f3aa90 Mon Sep 17 00:00:00 2001 From: Jan Kassens Date: Tue, 6 Dec 2022 12:01:32 -0500 Subject: [PATCH] [HIR] add `for` terminal The changes here are pretty significant and there's a bunch more left: - support for with any of ``, `` or `` empty. - support for with `` as `Expression` instead of VariableDeclaration` node - support assignment expressions in ``, this seems like it might require further new abstractions to allow something like a block to codegen into a single expression. --- compiler/forget/src/HIR/BuildHIR.ts | 125 +++++++++--------- compiler/forget/src/HIR/Codegen.ts | 8 ++ compiler/forget/src/HIR/HIR.ts | 11 ++ compiler/forget/src/HIR/HIRBuilder.ts | 7 +- compiler/forget/src/HIR/HIRTreeVisitor.ts | 114 ++++++++++++++++ .../src/HIR/InferReactiveScopeDependencies.ts | 1 + compiler/forget/src/HIR/PrintHIR.ts | 5 + compiler/forget/src/HIR/PrintHIRTree.ts | 6 + .../forget/src/HIR/VisualizeHIRMermaid.ts | 11 ++ compiler/forget/src/HIR/visitors.ts | 22 +++ .../__tests__/fixtures/hir/ssa-for.expect.md | 86 ++++++++---- .../src/__tests__/fixtures/hir/ssa-for.js | 6 +- 12 files changed, 310 insertions(+), 92 deletions(-) diff --git a/compiler/forget/src/HIR/BuildHIR.ts b/compiler/forget/src/HIR/BuildHIR.ts index 255feade3f..a2bd83581f 100644 --- a/compiler/forget/src/HIR/BuildHIR.ts +++ b/compiler/forget/src/HIR/BuildHIR.ts @@ -317,90 +317,77 @@ function lowerStatement( } case "ForStatement": { const stmt = stmtPath as NodePath; - /** - * 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); - 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; + 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 +): 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): Place { const exprNode = exprPath.node; const exprLoc = exprNode.loc ?? GeneratedSource; diff --git a/compiler/forget/src/HIR/Codegen.ts b/compiler/forget/src/HIR/Codegen.ts index 624d24c6b1..2fc72377ac 100644 --- a/compiler/forget/src/HIR/Codegen.ts +++ b/compiler/forget/src/HIR/Codegen.ts @@ -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) { diff --git a/compiler/forget/src/HIR/HIR.ts b/compiler/forget/src/HIR/HIR.ts index 8857be4912..01692cd791 100644 --- a/compiler/forget/src/HIR/HIR.ts +++ b/compiler/forget/src/HIR/HIR.ts @@ -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 diff --git a/compiler/forget/src/HIR/HIRBuilder.ts b/compiler/forget/src/HIR/HIRBuilder.ts index 1bf104869b..15d1308eea 100644 --- a/compiler/forget/src/HIR/HIRBuilder.ts +++ b/compiler/forget/src/HIR/HIRBuilder.ts @@ -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, diff --git a/compiler/forget/src/HIR/HIRTreeVisitor.ts b/compiler/forget/src/HIR/HIRTreeVisitor.ts index 17644976fa..399bd64442 100644 --- a/compiler/forget/src/HIR/HIRTreeVisitor.ts +++ b/compiler/forget/src/HIR/HIRTreeVisitor.ts @@ -311,6 +311,113 @@ class Driver { } 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 = test: TValue; loop: TItem; } + | { + kind: "for"; + init: TItem; + test: TValue; + update: TValue; + loop: TItem; + } | { kind: "break"; label: BlockId | null } | { kind: "continue"; label: BlockId | null }; diff --git a/compiler/forget/src/HIR/InferReactiveScopeDependencies.ts b/compiler/forget/src/HIR/InferReactiveScopeDependencies.ts index 1798d9d11f..1b6e473732 100644 --- a/compiler/forget/src/HIR/InferReactiveScopeDependencies.ts +++ b/compiler/forget/src/HIR/InferReactiveScopeDependencies.ts @@ -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); diff --git a/compiler/forget/src/HIR/PrintHIR.ts b/compiler/forget/src/HIR/PrintHIR.ts index ba5895065a..337c535c1c 100644 --- a/compiler/forget/src/HIR/PrintHIR.ts +++ b/compiler/forget/src/HIR/PrintHIR.ts @@ -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 { } 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, diff --git a/compiler/forget/src/HIR/PrintHIRTree.ts b/compiler/forget/src/HIR/PrintHIRTree.ts index a2c21f1b25..f64deaaec0 100644 --- a/compiler/forget/src/HIR/PrintHIRTree.ts +++ b/compiler/forget/src/HIR/PrintHIRTree.ts @@ -94,6 +94,12 @@ class PrintVisitor implements Visitor, 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}`; diff --git a/compiler/forget/src/HIR/VisualizeHIRMermaid.ts b/compiler/forget/src/HIR/VisualizeHIRMermaid.ts index f884ab547f..dea5d6339b 100644 --- a/compiler/forget/src/HIR/VisualizeHIRMermaid.ts +++ b/compiler/forget/src/HIR/VisualizeHIRMermaid.ts @@ -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; diff --git a/compiler/forget/src/HIR/visitors.ts b/compiler/forget/src/HIR/visitors.ts index ed2a73c97e..e65c27a323 100644 --- a/compiler/forget/src/HIR/visitors.ts +++ b/compiler/forget/src/HIR/visitors.ts @@ -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 { 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 { break; } case "while": + case "for": case "goto": { // no-op break; diff --git a/compiler/forget/src/__tests__/fixtures/hir/ssa-for.expect.md b/compiler/forget/src/__tests__/fixtures/hir/ssa-for.expect.md index 71aa8c44c4..ca49eecaa4 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/ssa-for.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/ssa-for.expect.md @@ -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; } ``` diff --git a/compiler/forget/src/__tests__/fixtures/hir/ssa-for.js b/compiler/forget/src/__tests__/fixtures/hir/ssa-for.js index 0336b601a8..950e2450e7 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/ssa-for.js +++ b/compiler/forget/src/__tests__/fixtures/hir/ssa-for.js @@ -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; }