Visitor extension for transforming ReactiveFunction

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.
This commit is contained in:
Joe Savona
2023-02-08 14:07:00 -08:00
parent 394666118d
commit c7e3bc4d41
3 changed files with 159 additions and 160 deletions
@@ -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<boolean> {
override transformScope(
scope: ReactiveScopeBlock,
isWithinLoop: boolean
): Transformed<ReactiveStatement> {
this.visitScope(scope, isWithinLoop);
if (isWithinLoop) {
return { kind: "replace-many", value: scope.instructions };
} else {
return { kind: "keep" };
}
}
override visitTerminal(
stmt: ReactiveTerminalStatement<ReactiveTerminal>,
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}'`
);
}
}
}
@@ -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<void> {
override transformScope(
scopeBlock: ReactiveScopeBlock,
state: void
): Transformed<ReactiveStatement> {
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;
}
@@ -13,6 +13,7 @@ import {
ReactiveFunction,
ReactiveInstruction,
ReactiveScopeBlock,
ReactiveStatement,
ReactiveTerminal,
ReactiveTerminalStatement,
ReactiveValue,
@@ -175,6 +176,96 @@ export class ReactiveFunctionVisitor<TState = void> {
}
}
export type Transformed<T> =
| { kind: "remove" }
| { kind: "keep" }
| { kind: "replace"; value: T }
| { kind: "replace-many"; value: Array<T> };
export class ReactiveFunctionTransform<
TState = void
> extends ReactiveFunctionVisitor<TState> {
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<ReactiveStatement>;
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<ReactiveStatement> {
this.visitInstruction(instruction, state);
return { kind: "keep" };
}
transformTerminal(
stmt: ReactiveTerminalStatement,
state: TState
): Transformed<ReactiveStatement> {
this.visitTerminal(stmt, state);
return { kind: "keep" };
}
transformScope(
scope: ReactiveScopeBlock,
state: TState
): Transformed<ReactiveStatement> {
this.visitScope(scope, state);
return { kind: "keep" };
}
}
export function* eachReactiveValueOperand(
instrValue: ReactiveValue
): Iterable<Place> {