mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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:
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user