From c7e3bc4d413fd5a17597bd345dc8766ceed983ea Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 8 Feb 2023 14:07:00 -0800 Subject: [PATCH] Visitor extension for transforming ReactiveFunction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a subclass of ReactiveFunctionVisitor, ReactiveFunctionTransform, which makes it easier to write passes that change the shape of a ReactiveFunction. The two use-cases converted so far are both flattening away certain categories of reactive scopes — this will make it easier to add a similar pass to prune scopes that contain hook calls. --- .../ReactiveScopes/FlattenReactiveLoops.ts | 162 +++++------------- .../src/ReactiveScopes/PruneUnusedScopes.ts | 66 +++---- .../forget/src/ReactiveScopes/visitors.ts | 91 ++++++++++ 3 files changed, 159 insertions(+), 160 deletions(-) diff --git a/compiler/forget/src/ReactiveScopes/FlattenReactiveLoops.ts b/compiler/forget/src/ReactiveScopes/FlattenReactiveLoops.ts index 741ce7b9c9..21d00a0c8a 100644 --- a/compiler/forget/src/ReactiveScopes/FlattenReactiveLoops.ts +++ b/compiler/forget/src/ReactiveScopes/FlattenReactiveLoops.ts @@ -6,142 +6,66 @@ */ import { - ReactiveBlock, ReactiveFunction, ReactiveScopeBlock, + ReactiveStatement, + ReactiveTerminal, + ReactiveTerminalStatement, } from "../HIR/HIR"; import { assertExhaustive } from "../Utils/utils"; +import { + ReactiveFunctionTransform, + Transformed, + visitReactiveFunction, +} from "./visitors"; /** * Given a reactive function, flattens any scopes contained within a loop construct. * We won't initially support memoization within loops though this is possible in the future. */ export function flattenReactiveLoops(fn: ReactiveFunction): void { - visit(fn.body, false); + visitReactiveFunction(fn, new Transform(), false); } -function visit(block: ReactiveBlock, shouldFlatten: boolean): void { - let i = 0; - while (i < block.length) { - const item = block[i]!; - switch (item.kind) { - case "scope": { - if (shouldFlatten) { - const successors = block.splice(i + 1); - block.pop(); // remove the current element - flatten(item, block); - i = block.length; - block.push(...successors); - } else { - visit(item.instructions, false); - i++; - } +class Transform extends ReactiveFunctionTransform { + override transformScope( + scope: ReactiveScopeBlock, + isWithinLoop: boolean + ): Transformed { + this.visitScope(scope, isWithinLoop); + if (isWithinLoop) { + return { kind: "replace-many", value: scope.instructions }; + } else { + return { kind: "keep" }; + } + } + + override visitTerminal( + stmt: ReactiveTerminalStatement, + isWithinLoop: boolean + ): void { + switch (stmt.terminal.kind) { + // Loop terminals flatten nested scopes + case "while": + case "for": { + this.traverseTerminal(stmt, true); break; } - case "instruction": { - i++; - break; - } - case "terminal": { - const terminal = item.terminal; - switch (terminal.kind) { - case "break": - case "continue": - case "return": - case "throw": { - break; - } - case "for": { - visit(terminal.loop, true); - break; - } - case "while": { - visit(terminal.loop, true); - break; - } - case "if": { - visit(terminal.consequent, shouldFlatten); - if (terminal.alternate !== null) { - visit(terminal.alternate, shouldFlatten); - } - break; - } - case "switch": { - for (const case_ of terminal.cases) { - if (case_.block !== undefined) { - visit(case_.block, shouldFlatten); - } - } - break; - } - default: { - assertExhaustive( - terminal, - `Unexpected terminal kind '${(terminal as any).kind}'` - ); - } - } - i++; + // Non-loop terminals passthrough is contextual, inherits the parent isWithinScope + case "break": + case "continue": + case "if": + case "return": + case "switch": + case "throw": { + this.traverseTerminal(stmt, isWithinLoop); break; } default: { - assertExhaustive(item, `Unexpected item`); - } - } - } -} - -function flatten(scope: ReactiveScopeBlock, block: ReactiveBlock): void { - for (const item of scope.instructions) { - switch (item.kind) { - case "scope": { - flatten(item, block); - break; - } - case "terminal": { - const terminal = item.terminal; - switch (terminal.kind) { - case "break": - case "continue": - case "return": - case "throw": { - break; - } - case "for": { - visit(terminal.loop, true); - break; - } - case "while": { - visit(terminal.loop, true); - break; - } - case "if": { - visit(terminal.consequent, true); - if (terminal.alternate !== null) { - visit(terminal.alternate, true); - } - break; - } - case "switch": { - for (const case_ of terminal.cases) { - if (case_.block !== undefined) { - visit(case_.block, true); - } - } - break; - } - default: { - assertExhaustive( - terminal, - `Unexpected terminal kind '${(terminal as any).kind}'` - ); - } - } - block.push(item); - break; - } - default: { - block.push(item); + assertExhaustive( + stmt.terminal, + `Unexpected terminal kind '${(stmt.terminal as any).kind}'` + ); } } } diff --git a/compiler/forget/src/ReactiveScopes/PruneUnusedScopes.ts b/compiler/forget/src/ReactiveScopes/PruneUnusedScopes.ts index 6632981500..8537add22a 100644 --- a/compiler/forget/src/ReactiveScopes/PruneUnusedScopes.ts +++ b/compiler/forget/src/ReactiveScopes/PruneUnusedScopes.ts @@ -5,54 +5,38 @@ * LICENSE file in the root directory of this source tree. */ -import { ReactiveBlock, ReactiveFunction } from "../HIR/HIR"; -import { assertExhaustive } from "../Utils/utils"; -import { mapTerminalBlocks } from "./visitors"; +import { + ReactiveFunction, + ReactiveScopeBlock, + ReactiveStatement, +} from "../HIR/HIR"; +import { + ReactiveFunctionTransform, + Transformed, + visitReactiveFunction, +} from "./visitors"; /** * Converts scopes without outputs into regular blocks. */ export function pruneUnusedScopes(fn: ReactiveFunction): void { - fn.body = visitBlock(fn.body); + visitReactiveFunction(fn, new Transform(), undefined); } -function visitBlock(block: ReactiveBlock): ReactiveBlock { - let nextBlock: ReactiveBlock | null = null; - for (let i = 0; i < block.length; i++) { - const stmt = block[i]!; - switch (stmt.kind) { - case "terminal": { - mapTerminalBlocks(stmt.terminal, visitBlock); - break; - } - case "instruction": { - break; - } - case "scope": { - stmt.instructions = visitBlock(stmt.instructions); - // If a scope doesn't have declarations but reassigns a value, the scope shouldn't be pruned - // as we still want to generate a memo block for that scope - if ( - stmt.scope.declarations.size === 0 && - (stmt.scope.dependencies.size === 0 || - stmt.scope.reassignments.size === 0) - ) { - nextBlock ??= block.slice(0, i); - nextBlock.push(...stmt.instructions); - continue; - } - break; - } - default: { - assertExhaustive( - stmt, - `Unexpected statement kind '${(stmt as any).kind}'` - ); - } - } - if (nextBlock !== null) { - nextBlock.push(stmt); +class Transform extends ReactiveFunctionTransform { + override transformScope( + scopeBlock: ReactiveScopeBlock, + state: void + ): Transformed { + this.visitScope(scopeBlock, state); + if ( + scopeBlock.scope.declarations.size === 0 && + (scopeBlock.scope.dependencies.size === 0 || + scopeBlock.scope.reassignments.size === 0) + ) { + return { kind: "replace-many", value: scopeBlock.instructions }; + } else { + return { kind: "keep" }; } } - return nextBlock ?? block; } diff --git a/compiler/forget/src/ReactiveScopes/visitors.ts b/compiler/forget/src/ReactiveScopes/visitors.ts index 989835485e..d6a493d05c 100644 --- a/compiler/forget/src/ReactiveScopes/visitors.ts +++ b/compiler/forget/src/ReactiveScopes/visitors.ts @@ -13,6 +13,7 @@ import { ReactiveFunction, ReactiveInstruction, ReactiveScopeBlock, + ReactiveStatement, ReactiveTerminal, ReactiveTerminalStatement, ReactiveValue, @@ -175,6 +176,96 @@ export class ReactiveFunctionVisitor { } } +export type Transformed = + | { kind: "remove" } + | { kind: "keep" } + | { kind: "replace"; value: T } + | { kind: "replace-many"; value: Array }; + +export class ReactiveFunctionTransform< + TState = void +> extends ReactiveFunctionVisitor { + override traverseBlock(block: ReactiveBlock, state: TState): void { + let nextBlock: ReactiveBlock | null = null; + for (let i = 0; i < block.length; i++) { + const instr = block[i]!; + let transformed: Transformed; + switch (instr.kind) { + case "instruction": { + transformed = this.transformInstruction(instr.instruction, state); + break; + } + case "scope": { + transformed = this.transformScope(instr, state); + break; + } + case "terminal": { + transformed = this.transformTerminal(instr, state); + break; + } + default: { + assertExhaustive( + instr, + `Unexpected instruction kind '${(instr as any).kind}'` + ); + } + } + switch (transformed.kind) { + case "keep": { + if (nextBlock !== null) { + nextBlock.push(instr); + } + break; + } + case "remove": { + if (nextBlock === null) { + nextBlock = block.slice(0, i); + } + break; + } + case "replace": { + nextBlock ??= block.slice(0, i); + nextBlock.push(transformed.value); + break; + } + case "replace-many": { + nextBlock ??= block.slice(0, i); + nextBlock.push(...transformed.value); + break; + } + } + } + if (nextBlock !== null) { + block.length = 0; + block.push(...nextBlock); + } + } + + transformInstruction( + instruction: ReactiveInstruction, + state: TState + ): Transformed { + this.visitInstruction(instruction, state); + return { kind: "keep" }; + } + + transformTerminal( + stmt: ReactiveTerminalStatement, + state: TState + ): Transformed { + this.visitTerminal(stmt, state); + return { kind: "keep" }; + } + + transformScope( + scope: ReactiveScopeBlock, + state: TState + ): Transformed { + this.visitScope(scope, state); + return { kind: "keep" }; + } +} + export function* eachReactiveValueOperand( instrValue: ReactiveValue ): Iterable {