mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
[compiler][cleanup] Delete now-unused reactive scope fork
Followup to #30891 ghstack-source-id: 6b42055b5d28da39d99a235bcd86a82eb7c270f4 Pull Request resolved: https://github.com/facebook/react/pull/30892
This commit is contained in:
@@ -1492,6 +1492,7 @@ export type ReactiveScopeDeclaration = {
|
||||
scope: ReactiveScope; // the scope in which the variable was originally declared
|
||||
};
|
||||
|
||||
export type DependencyPath = Array<{property: string; optional: boolean}>;
|
||||
export type ReactiveScopeDependency = {
|
||||
identifier: Identifier;
|
||||
path: DependencyPath;
|
||||
@@ -1506,7 +1507,21 @@ export function areEqualPaths(a: DependencyPath, b: DependencyPath): boolean {
|
||||
)
|
||||
);
|
||||
}
|
||||
export type DependencyPath = Array<{property: string; optional: boolean}>;
|
||||
|
||||
export function getPlaceScope(
|
||||
id: InstructionId,
|
||||
place: Place,
|
||||
): ReactiveScope | null {
|
||||
const scope = place.identifier.scope;
|
||||
if (scope !== null && isScopeActive(scope, id)) {
|
||||
return scope;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isScopeActive(scope: ReactiveScope, id: InstructionId): boolean {
|
||||
return id >= scope.range.start && id < scope.range.end;
|
||||
}
|
||||
|
||||
/*
|
||||
* Simulated opaque type for BlockIds to prevent using normal numbers as block ids
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import {
|
||||
ReactiveScope,
|
||||
makeInstructionId,
|
||||
} from '.';
|
||||
import {getPlaceScope} from '../ReactiveScopes/BuildReactiveBlocks';
|
||||
import {getPlaceScope} from '../HIR/HIR';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
import {getOrInsertDefault} from '../Utils/utils';
|
||||
|
||||
-187
@@ -1,187 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
InstructionId,
|
||||
Place,
|
||||
ReactiveBlock,
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
ReactiveScope,
|
||||
ScopeId,
|
||||
makeInstructionId,
|
||||
} from '../HIR/HIR';
|
||||
import {getPlaceScope} from './BuildReactiveBlocks';
|
||||
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
|
||||
|
||||
/*
|
||||
* Note: this is the 2nd of 4 passes that determine how to break a function into discrete
|
||||
* reactive scopes (independently memoizeable units of code):
|
||||
* 1. InferReactiveScopeVariables (on HIR) determines operands that mutate together and assigns
|
||||
* them a unique reactive scope.
|
||||
* 2. AlignReactiveScopesToBlockScopes (this pass, on ReactiveFunction) aligns reactive scopes
|
||||
* to block scopes.
|
||||
* 3. MergeOverlappingReactiveScopes (on ReactiveFunction) ensures that reactive scopes do not
|
||||
* overlap, merging any such scopes.
|
||||
* 4. BuildReactiveBlocks (on ReactiveFunction) groups the statements for each scope into
|
||||
* a ReactiveScopeBlock.
|
||||
*
|
||||
* Prior inference passes assign a reactive scope to each operand, but the ranges of these
|
||||
* scopes are based on specific instructions at arbitrary points in the control-flow graph.
|
||||
* However, to codegen blocks around the instructions in each scope, the scopes must be
|
||||
* aligned to block-scope boundaries - we can't memoize half of a loop!
|
||||
*
|
||||
* This pass updates reactive scope boundaries to align to control flow boundaries, for
|
||||
* example:
|
||||
*
|
||||
* ```javascript
|
||||
* function foo(cond, a) {
|
||||
* ⌵ original scope
|
||||
* ⌵ expanded scope
|
||||
* const x = []; ⌝ ⌝
|
||||
* if (cond) { ⎮ ⎮
|
||||
* ... ⎮ ⎮
|
||||
* x.push(a); ⌟ ⎮
|
||||
* ... ⎮
|
||||
* } ⌟
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Here the original scope for `x` ended partway through the if consequent, but we can't
|
||||
* memoize part of that block. This pass would align the scope to the end of the consequent.
|
||||
*
|
||||
* The more general rule is that a reactive scope may only end at the same block scope as it
|
||||
* began: this pass therefore finds, for each scope, the block where that scope started and
|
||||
* finds the first instruction after the scope's mutable range in that same block scope (which
|
||||
* will be the updated end for that scope).
|
||||
*/
|
||||
|
||||
export function alignReactiveScopesToBlockScopes(fn: ReactiveFunction): void {
|
||||
const context = new Context();
|
||||
visitReactiveFunction(fn, new Visitor(), context);
|
||||
}
|
||||
|
||||
class Visitor extends ReactiveFunctionVisitor<Context> {
|
||||
override visitID(id: InstructionId, state: Context): void {
|
||||
state.visitId(id);
|
||||
}
|
||||
override visitPlace(id: InstructionId, place: Place, state: Context): void {
|
||||
const scope = getPlaceScope(id, place);
|
||||
if (scope !== null) {
|
||||
state.visitScope(scope);
|
||||
}
|
||||
}
|
||||
override visitLValue(id: InstructionId, lvalue: Place, state: Context): void {
|
||||
const scope = getPlaceScope(id, lvalue);
|
||||
if (scope !== null) {
|
||||
state.visitScope(scope);
|
||||
}
|
||||
}
|
||||
|
||||
override visitInstruction(instr: ReactiveInstruction, state: Context): void {
|
||||
switch (instr.value.kind) {
|
||||
case 'OptionalExpression':
|
||||
case 'SequenceExpression':
|
||||
case 'ConditionalExpression':
|
||||
case 'LogicalExpression': {
|
||||
const prevScopeCount = state.currentScopes().length;
|
||||
this.traverseInstruction(instr, state);
|
||||
|
||||
/**
|
||||
* These compound value types can have nested sequences of instructions
|
||||
* with scopes that start "partway" through a block-level instruction.
|
||||
* This would cause the start of the scope to not align with any block-level
|
||||
* instruction and get skipped by the later BuildReactiveBlocks pass.
|
||||
*
|
||||
* Here we detect scopes created within compound instructions and align the
|
||||
* start of these scopes to the outer instruction id to ensure the scopes
|
||||
* aren't skipped.
|
||||
*/
|
||||
const scopes = state.currentScopes();
|
||||
for (let i = prevScopeCount; i < scopes.length; i++) {
|
||||
const scope = scopes[i];
|
||||
scope.scope.range.start = makeInstructionId(
|
||||
Math.min(instr.id, scope.scope.range.start),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
this.traverseInstruction(instr, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override visitBlock(block: ReactiveBlock, state: Context): void {
|
||||
state.enter(() => {
|
||||
this.traverseBlock(block, state);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type PendingReactiveScope = {active: boolean; scope: ReactiveScope};
|
||||
|
||||
class Context {
|
||||
/*
|
||||
* For each block scope (outer array) stores a list of ReactiveScopes that start
|
||||
* in that block scope.
|
||||
*/
|
||||
#blockScopes: Array<Array<PendingReactiveScope>> = [];
|
||||
|
||||
/*
|
||||
* ReactiveScopes whose declaring block scope has ended but may still need to
|
||||
* be "closed" (ie have their range.end be updated). A given scope can be in
|
||||
* blockScopes OR this array but not both.
|
||||
*/
|
||||
#unclosedScopes: Array<PendingReactiveScope> = [];
|
||||
|
||||
/*
|
||||
* Set of all scope ids that have been seen so far, regardless of which of
|
||||
* the above data structures they're in, to avoid tracking the same scope twice.
|
||||
*/
|
||||
#seenScopes: Set<ScopeId> = new Set();
|
||||
|
||||
currentScopes(): Array<PendingReactiveScope> {
|
||||
return this.#blockScopes.at(-1) ?? [];
|
||||
}
|
||||
|
||||
enter(fn: () => void): void {
|
||||
this.#blockScopes.push([]);
|
||||
fn();
|
||||
const lastScope = this.#blockScopes.pop()!;
|
||||
for (const scope of lastScope) {
|
||||
if (scope.active) {
|
||||
this.#unclosedScopes.push(scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visitId(id: InstructionId): void {
|
||||
const currentScopes = this.#blockScopes.at(-1)!;
|
||||
const scopes = [...currentScopes, ...this.#unclosedScopes];
|
||||
for (const pending of scopes) {
|
||||
if (!pending.active) {
|
||||
continue;
|
||||
}
|
||||
if (id >= pending.scope.range.end) {
|
||||
pending.active = false;
|
||||
pending.scope.range.end = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visitScope(scope: ReactiveScope): void {
|
||||
if (!this.#seenScopes.has(scope.id)) {
|
||||
const currentScopes = this.#blockScopes.at(-1)!;
|
||||
this.#seenScopes.add(scope.id);
|
||||
currentScopes.push({
|
||||
active: true,
|
||||
scope,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -13,6 +13,7 @@ import {
|
||||
MutableRange,
|
||||
Place,
|
||||
ReactiveScope,
|
||||
getPlaceScope,
|
||||
makeInstructionId,
|
||||
} from '../HIR/HIR';
|
||||
import {
|
||||
@@ -23,7 +24,6 @@ import {
|
||||
terminalFallthrough,
|
||||
} from '../HIR/visitors';
|
||||
import {retainWhere_Set} from '../Utils/utils';
|
||||
import {getPlaceScope} from './BuildReactiveBlocks';
|
||||
|
||||
type InstructionRange = MutableRange;
|
||||
/*
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import {
|
||||
ReactiveScopeBlock,
|
||||
ScopeId,
|
||||
} from '../HIR';
|
||||
import {getPlaceScope} from './BuildReactiveBlocks';
|
||||
import {getPlaceScope} from '../HIR/HIR';
|
||||
import {ReactiveFunctionVisitor} from './visitors';
|
||||
|
||||
/*
|
||||
|
||||
-244
@@ -1,244 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
BlockId,
|
||||
InstructionId,
|
||||
Place,
|
||||
ReactiveBlock,
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
ReactiveScope,
|
||||
ReactiveScopeBlock,
|
||||
ReactiveStatement,
|
||||
ScopeId,
|
||||
} from '../HIR';
|
||||
import {eachInstructionLValue} from '../HIR/visitors';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {eachReactiveValueOperand, mapTerminalBlocks} from './visitors';
|
||||
|
||||
/*
|
||||
* Note: this is the 4th of 4 passes that determine how to break a function into discrete
|
||||
* reactive scopes (independently memoizeable units of code):
|
||||
* 1. InferReactiveScopeVariables (on HIR) determines operands that mutate together and assigns
|
||||
* them a unique reactive scope.
|
||||
* 2. AlignReactiveScopesToBlockScopes (on ReactiveFunction) aligns reactive scopes
|
||||
* to block scopes.
|
||||
* 3. MergeOverlappingReactiveScopes (this pass, on ReactiveFunction) ensures that reactive
|
||||
* scopes do not overlap, merging any such scopes.
|
||||
* 4. BuildReactiveBlocks (on ReactiveFunction) groups the statements for each scope into
|
||||
* a ReactiveScopeBlock.
|
||||
*
|
||||
* Given a function where the reactive scopes have been correctly aligned and merged,
|
||||
* this pass groups the instructions for each reactive scope into ReactiveBlocks.
|
||||
*/
|
||||
export function buildReactiveBlocks(fn: ReactiveFunction): void {
|
||||
const context = new Context();
|
||||
fn.body = context.enter(() => {
|
||||
visitBlock(context, fn.body);
|
||||
});
|
||||
}
|
||||
|
||||
class Context {
|
||||
#builders: Array<Builder> = [];
|
||||
#scopes: Set<ScopeId> = new Set();
|
||||
|
||||
visitId(id: InstructionId): void {
|
||||
const builder = this.#builders.at(-1)!;
|
||||
builder.visitId(id);
|
||||
}
|
||||
|
||||
visitScope(scope: ReactiveScope): void {
|
||||
if (this.#scopes.has(scope.id)) {
|
||||
return;
|
||||
}
|
||||
this.#scopes.add(scope.id);
|
||||
this.#builders.at(-1)!.startScope(scope);
|
||||
}
|
||||
|
||||
append(
|
||||
stmt: ReactiveStatement,
|
||||
label: {id: BlockId; implicit: boolean} | null,
|
||||
): void {
|
||||
this.#builders.at(-1)!.append(stmt, label);
|
||||
}
|
||||
|
||||
enter(fn: () => void): ReactiveBlock {
|
||||
const builder = new Builder();
|
||||
this.#builders.push(builder);
|
||||
fn();
|
||||
const popped = this.#builders.pop();
|
||||
CompilerError.invariant(popped === builder, {
|
||||
reason: 'Expected push/pop to be called 1:1',
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
return builder.complete();
|
||||
}
|
||||
}
|
||||
|
||||
class Builder {
|
||||
#instructions: ReactiveBlock;
|
||||
#stack: Array<
|
||||
| {kind: 'scope'; block: ReactiveScopeBlock}
|
||||
| {kind: 'block'; block: ReactiveBlock}
|
||||
>;
|
||||
|
||||
constructor() {
|
||||
const block: ReactiveBlock = [];
|
||||
this.#instructions = block;
|
||||
this.#stack = [{kind: 'block', block}];
|
||||
}
|
||||
|
||||
append(
|
||||
item: ReactiveStatement,
|
||||
label: {id: BlockId; implicit: boolean} | null,
|
||||
): void {
|
||||
if (label !== null) {
|
||||
CompilerError.invariant(item.kind === 'terminal', {
|
||||
reason: 'Only terminals may have a label',
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
item.label = label;
|
||||
}
|
||||
this.#instructions.push(item);
|
||||
}
|
||||
|
||||
startScope(scope: ReactiveScope): void {
|
||||
const block: ReactiveScopeBlock = {
|
||||
kind: 'scope',
|
||||
scope,
|
||||
instructions: [],
|
||||
};
|
||||
this.append(block, null);
|
||||
this.#instructions = block.instructions;
|
||||
this.#stack.push({kind: 'scope', block});
|
||||
}
|
||||
|
||||
visitId(id: InstructionId): void {
|
||||
for (let i = 0; i < this.#stack.length; i++) {
|
||||
const entry = this.#stack[i]!;
|
||||
if (entry.kind === 'scope' && id >= entry.block.scope.range.end) {
|
||||
this.#stack.length = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const last = this.#stack[this.#stack.length - 1]!;
|
||||
if (last.kind === 'block') {
|
||||
this.#instructions = last.block;
|
||||
} else {
|
||||
this.#instructions = last.block.instructions;
|
||||
}
|
||||
}
|
||||
|
||||
complete(): ReactiveBlock {
|
||||
/*
|
||||
* TODO: @josephsavona debug violations of this invariant
|
||||
* invariant(
|
||||
* this.#stack.length === 1,
|
||||
* "Expected all scopes to be closed when exiting a block"
|
||||
* );
|
||||
*/
|
||||
const first = this.#stack[0]!;
|
||||
CompilerError.invariant(first.kind === 'block', {
|
||||
reason: 'Expected first stack item to be a basic block',
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
return first.block;
|
||||
}
|
||||
}
|
||||
|
||||
function visitBlock(context: Context, block: ReactiveBlock): void {
|
||||
for (const stmt of block) {
|
||||
switch (stmt.kind) {
|
||||
case 'instruction': {
|
||||
context.visitId(stmt.instruction.id);
|
||||
const scope = getInstructionScope(stmt.instruction);
|
||||
if (scope !== null) {
|
||||
context.visitScope(scope);
|
||||
}
|
||||
context.append(stmt, null);
|
||||
break;
|
||||
}
|
||||
case 'terminal': {
|
||||
const id = stmt.terminal.id;
|
||||
if (id !== null) {
|
||||
context.visitId(id);
|
||||
}
|
||||
mapTerminalBlocks(stmt.terminal, block => {
|
||||
return context.enter(() => {
|
||||
visitBlock(context, block);
|
||||
});
|
||||
});
|
||||
context.append(stmt, stmt.label);
|
||||
break;
|
||||
}
|
||||
case 'pruned-scope':
|
||||
case 'scope': {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Expected the function to not have scopes already assigned',
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
stmt,
|
||||
`Unexpected statement kind \`${(stmt as any).kind}\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getInstructionScope(
|
||||
instr: ReactiveInstruction,
|
||||
): ReactiveScope | null {
|
||||
CompilerError.invariant(instr.lvalue !== null, {
|
||||
reason:
|
||||
'Expected lvalues to not be null when assigning scopes. ' +
|
||||
'Pruning lvalues too early can result in missing scope information.',
|
||||
description: null,
|
||||
loc: instr.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
for (const operand of eachInstructionLValue(instr)) {
|
||||
const operandScope = getPlaceScope(instr.id, operand);
|
||||
if (operandScope !== null) {
|
||||
return operandScope;
|
||||
}
|
||||
}
|
||||
for (const operand of eachReactiveValueOperand(instr.value)) {
|
||||
const operandScope = getPlaceScope(instr.id, operand);
|
||||
if (operandScope !== null) {
|
||||
return operandScope;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getPlaceScope(
|
||||
id: InstructionId,
|
||||
place: Place,
|
||||
): ReactiveScope | null {
|
||||
const scope = place.identifier.scope;
|
||||
if (scope !== null && isScopeActive(scope, id)) {
|
||||
return scope;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isScopeActive(scope: ReactiveScope, id: InstructionId): boolean {
|
||||
return id >= scope.range.start && id < scope.range.end;
|
||||
}
|
||||
-84
@@ -1,84 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
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 {
|
||||
visitReactiveFunction(fn, new Transform(), false);
|
||||
}
|
||||
|
||||
class Transform extends ReactiveFunctionTransform<boolean> {
|
||||
override transformScope(
|
||||
scope: ReactiveScopeBlock,
|
||||
isWithinLoop: boolean,
|
||||
): Transformed<ReactiveStatement> {
|
||||
this.visitScope(scope, isWithinLoop);
|
||||
if (isWithinLoop) {
|
||||
return {
|
||||
kind: 'replace',
|
||||
value: {
|
||||
kind: 'pruned-scope',
|
||||
scope: scope.scope,
|
||||
instructions: scope.instructions,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {kind: 'keep'};
|
||||
}
|
||||
}
|
||||
|
||||
override visitTerminal(
|
||||
stmt: ReactiveTerminalStatement<ReactiveTerminal>,
|
||||
isWithinLoop: boolean,
|
||||
): void {
|
||||
switch (stmt.terminal.kind) {
|
||||
// Loop terminals flatten nested scopes
|
||||
case 'do-while':
|
||||
case 'while':
|
||||
case 'for':
|
||||
case 'for-of':
|
||||
case 'for-in': {
|
||||
this.traverseTerminal(stmt, true);
|
||||
break;
|
||||
}
|
||||
// Non-loop terminals passthrough is contextual, inherits the parent isWithinScope
|
||||
case 'try':
|
||||
case 'label':
|
||||
case 'break':
|
||||
case 'continue':
|
||||
case 'if':
|
||||
case 'return':
|
||||
case 'switch':
|
||||
case 'throw': {
|
||||
this.traverseTerminal(stmt, isWithinLoop);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
stmt.terminal,
|
||||
`Unexpected terminal kind \`${(stmt.terminal as any).kind}\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-123
@@ -1,123 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
Environment,
|
||||
InstructionId,
|
||||
ReactiveFunction,
|
||||
ReactiveScopeBlock,
|
||||
ReactiveStatement,
|
||||
ReactiveValue,
|
||||
getHookKind,
|
||||
isUseOperator,
|
||||
} from '../HIR';
|
||||
import {
|
||||
ReactiveFunctionTransform,
|
||||
Transformed,
|
||||
visitReactiveFunction,
|
||||
} from './visitors';
|
||||
|
||||
/**
|
||||
* For simplicity the majority of compiler passes do not treat hooks specially. However, hooks are different
|
||||
* from regular functions in two key ways:
|
||||
* - They can introduce reactivity even when their arguments are non-reactive (accounted for in InferReactivePlaces)
|
||||
* - They cannot be called conditionally
|
||||
*
|
||||
* The `use` operator is similar:
|
||||
* - It can access context, and therefore introduce reactivity
|
||||
* - It can be called conditionally, but _it must be called if the component needs the return value_. This is because
|
||||
* React uses the fact that use was called to remember that the component needs the value, and that changes to the
|
||||
* input should invalidate the component itself.
|
||||
*
|
||||
* This pass accounts for the "can't call conditionally" aspect of both hooks and use. Though the reasoning is slightly
|
||||
* different for reach, the result is that we can't memoize scopes that call hooks or use since this would make them
|
||||
* called conditionally in the output.
|
||||
*
|
||||
* The pass finds and removes any scopes that transitively contain a hook or use call. By running all
|
||||
* the reactive scope inference first, agnostic of hooks, we know that the reactive scopes accurately
|
||||
* describe the set of values which "construct together", and remove _all_ that memoization in order
|
||||
* to ensure the hook call does not inadvertently become conditional.
|
||||
*/
|
||||
export function flattenScopesWithHooksOrUse(fn: ReactiveFunction): void {
|
||||
visitReactiveFunction(fn, new Transform(), {
|
||||
env: fn.env,
|
||||
hasHook: false,
|
||||
});
|
||||
}
|
||||
|
||||
type State = {
|
||||
env: Environment;
|
||||
hasHook: boolean;
|
||||
};
|
||||
|
||||
class Transform extends ReactiveFunctionTransform<State> {
|
||||
override transformScope(
|
||||
scope: ReactiveScopeBlock,
|
||||
outerState: State,
|
||||
): Transformed<ReactiveStatement> {
|
||||
const innerState: State = {
|
||||
env: outerState.env,
|
||||
hasHook: false,
|
||||
};
|
||||
this.visitScope(scope, innerState);
|
||||
outerState.hasHook ||= innerState.hasHook;
|
||||
if (innerState.hasHook) {
|
||||
if (scope.instructions.length === 1) {
|
||||
/*
|
||||
* This was a scope just for a hook call, which doesn't need memoization.
|
||||
* flatten it away
|
||||
*/
|
||||
return {
|
||||
kind: 'replace-many',
|
||||
value: scope.instructions,
|
||||
};
|
||||
}
|
||||
/*
|
||||
* else this scope had multiple instructions and produced some other value:
|
||||
* mark it as pruned
|
||||
*/
|
||||
return {
|
||||
kind: 'replace',
|
||||
value: {
|
||||
kind: 'pruned-scope',
|
||||
scope: scope.scope,
|
||||
instructions: scope.instructions,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {kind: 'keep'};
|
||||
}
|
||||
}
|
||||
|
||||
override visitValue(
|
||||
id: InstructionId,
|
||||
value: ReactiveValue,
|
||||
state: State,
|
||||
): void {
|
||||
this.traverseValue(id, value, state);
|
||||
switch (value.kind) {
|
||||
case 'CallExpression': {
|
||||
if (
|
||||
getHookKind(state.env, value.callee.identifier) != null ||
|
||||
isUseOperator(value.callee.identifier)
|
||||
) {
|
||||
state.hasHook = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MethodCall': {
|
||||
if (
|
||||
getHookKind(state.env, value.property.identifier) != null ||
|
||||
isUseOperator(value.property.identifier)
|
||||
) {
|
||||
state.hasHook = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-281
@@ -1,281 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
InstructionId,
|
||||
makeInstructionId,
|
||||
Place,
|
||||
ReactiveBlock,
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
ReactiveScope,
|
||||
ScopeId,
|
||||
} from '../HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
import {retainWhere} from '../Utils/utils';
|
||||
import {getPlaceScope} from './BuildReactiveBlocks';
|
||||
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
|
||||
|
||||
/*
|
||||
* Note: this is the 3rd of 4 passes that determine how to break a function into discrete
|
||||
* reactive scopes (independently memoizeable units of code):
|
||||
* 1. InferReactiveScopeVariables (on HIR) determines operands that mutate together and assigns
|
||||
* them a unique reactive scope.
|
||||
* 2. AlignReactiveScopesToBlockScopes (on ReactiveFunction) aligns reactive scopes
|
||||
* to block scopes.
|
||||
* 3. MergeOverlappingReactiveScopes (this pass, on ReactiveFunction) ensures that reactive
|
||||
* scopes do not overlap, merging any such scopes.
|
||||
* 4. BuildReactiveBlocks (on ReactiveFunction) groups the statements for each scope into
|
||||
* a ReactiveScopeBlock.
|
||||
*
|
||||
* Previous passes may leave "overlapping" scopes, ie where one or more instructions are within
|
||||
* the mutable range of multiple reactive scopes. We prefer to avoid executing instructions twice
|
||||
* for performance reasons (side effects are less of a concern bc components are required to be
|
||||
* idempotent), so we cannot simply repeat the instruction once for each scope. Instead, the only
|
||||
* option is to combine the two scopes into one. This is an area where an eventual Forget IDE
|
||||
* could provide real-time feedback to the developer that two computations are accidentally merged.
|
||||
*
|
||||
* ## Detailed Walkthrough
|
||||
*
|
||||
* Two scopes overlap if there is one or more instruction that is inside the range
|
||||
* of both scopes. In general, overlapping scopes are merged togther. The only
|
||||
* exception to this is when one scope *shadows* another scope. For example:
|
||||
*
|
||||
* ```javascript
|
||||
* function foo(cond, a) {
|
||||
* ⌵ scope for x
|
||||
* let x = []; ⌝
|
||||
* if (cond) { ⎮
|
||||
* ⌵ scope for y ⎮
|
||||
* let y = []; ⌝ ⎮
|
||||
* if (b) { ⎮ ⎮
|
||||
* y.push(b); ⌟ ⎮
|
||||
* } ⎮
|
||||
* x.push(<div>{y}</div>); ⎮
|
||||
* } ⌟
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* In this example the two scopes overlap, but mutation of the two scopes is not
|
||||
* interleaved. Specifically within the y scope there are no instructions that
|
||||
* modify any other scope: the inner scope "shadows" the outer one. This category
|
||||
* of overlap does *NOT* merge the scopes together.
|
||||
*
|
||||
* The implementation is inspired by the Rust notion of "stacked borrows". We traverse
|
||||
* the control-flow graph in tree form, at each point keeping track of which scopes are
|
||||
* active. So initially we see
|
||||
*
|
||||
* `let x = []`
|
||||
* active scopes: [x]
|
||||
*
|
||||
* and mark the x scope as active.
|
||||
*
|
||||
* Then we later encounter
|
||||
*
|
||||
* `let y = [];`
|
||||
* active scopes: [x, y]
|
||||
*
|
||||
* Here we first check to see if 'y' is already in the list of active scopes. It isn't,
|
||||
* so we push it to the stop of the stack.
|
||||
*
|
||||
* Then
|
||||
*
|
||||
* `y.push(b)`
|
||||
* active scopes: [x, y]
|
||||
*
|
||||
* Mutates y, so we check if y is the top of the stack. It is, so no merging must occur.
|
||||
*
|
||||
* If instead we saw eg
|
||||
*
|
||||
* `x.push(b)`
|
||||
* active scopes: [x, y]
|
||||
*
|
||||
* Then we would see that 'x' is active, but that it is shadowed. The two scopes would have
|
||||
* to be merged.
|
||||
*/
|
||||
export function mergeOverlappingReactiveScopes(fn: ReactiveFunction): void {
|
||||
const context = new Context();
|
||||
visitReactiveFunction(fn, new Visitor(), context);
|
||||
context.complete();
|
||||
}
|
||||
|
||||
class Visitor extends ReactiveFunctionVisitor<Context> {
|
||||
override visitID(id: InstructionId, state: Context): void {
|
||||
state.visitId(id);
|
||||
}
|
||||
override visitPlace(id: InstructionId, place: Place, state: Context): void {
|
||||
state.visitPlace(id, place);
|
||||
}
|
||||
override visitLValue(id: InstructionId, lvalue: Place, state: Context): void {
|
||||
state.visitPlace(id, lvalue);
|
||||
}
|
||||
override visitBlock(block: ReactiveBlock, state: Context): void {
|
||||
state.enter(() => {
|
||||
this.traverseBlock(block, state);
|
||||
});
|
||||
}
|
||||
override visitInstruction(
|
||||
instruction: ReactiveInstruction,
|
||||
state: Context,
|
||||
): void {
|
||||
if (
|
||||
instruction.value.kind === 'ConditionalExpression' ||
|
||||
instruction.value.kind === 'LogicalExpression' ||
|
||||
instruction.value.kind === 'OptionalExpression'
|
||||
) {
|
||||
state.enter(() => {
|
||||
super.visitInstruction(instruction, state);
|
||||
});
|
||||
} else {
|
||||
super.visitInstruction(instruction, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BlockScope {
|
||||
seen: Set<ScopeId> = new Set();
|
||||
scopes: Array<ShadowableReactiveScope> = [];
|
||||
}
|
||||
|
||||
type ShadowableReactiveScope = {
|
||||
scope: ReactiveScope;
|
||||
shadowedBy: ReactiveScope | null;
|
||||
};
|
||||
|
||||
class Context {
|
||||
scopes: Array<BlockScope> = [];
|
||||
seenScopes: Set<ScopeId> = new Set();
|
||||
joinedScopes: DisjointSet<ReactiveScope> = new DisjointSet();
|
||||
operandScopes: Map<Place, ReactiveScope> = new Map();
|
||||
|
||||
visitId(id: InstructionId): void {
|
||||
const currentBlock = this.scopes[this.scopes.length - 1]!;
|
||||
retainWhere(currentBlock.scopes, pending => {
|
||||
if (pending.scope.range.end > id) {
|
||||
return true;
|
||||
} else {
|
||||
currentBlock.seen.delete(pending.scope.id);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
visitPlace(id: InstructionId, place: Place): void {
|
||||
const scope = getPlaceScope(id, place);
|
||||
if (scope === null) {
|
||||
return;
|
||||
}
|
||||
this.operandScopes.set(place, scope);
|
||||
const currentBlock = this.scopes[this.scopes.length - 1]!;
|
||||
// Fast-path for the first time we see a new scope
|
||||
if (!this.seenScopes.has(scope.id)) {
|
||||
this.seenScopes.add(scope.id);
|
||||
currentBlock.seen.add(scope.id);
|
||||
currentBlock.scopes.push({shadowedBy: null, scope});
|
||||
return;
|
||||
}
|
||||
// Scope has already been seen, find it in the current block or a parent
|
||||
let index = this.scopes.length - 1;
|
||||
let nextBlock = currentBlock;
|
||||
while (!nextBlock.seen.has(scope.id)) {
|
||||
/*
|
||||
* scopes that cross control-flow boundaries are merged with overlapping
|
||||
* scopes
|
||||
*/
|
||||
this.joinedScopes.union([scope, ...nextBlock.scopes.map(s => s.scope)]);
|
||||
index--;
|
||||
if (index < 0) {
|
||||
/*
|
||||
* TODO: handle reassignments in multiple branches. these create new identifiers that
|
||||
* add an entry to this.seenScopes but which are then removed when their blocks exit.
|
||||
* this is also wrong for codegen, different versions of an identifier could be cached
|
||||
* differently and so a reassigned version of a variable needs a separate declaration.
|
||||
* console.log(`scope ${scope.id} not found`);
|
||||
*/
|
||||
|
||||
/*
|
||||
* for (let i = this.scopes.length - 1; i > index; i--) {
|
||||
* const s = this.scopes[i];
|
||||
* console.log(
|
||||
* JSON.stringify(
|
||||
* {
|
||||
* seen: Array.from(s.seen),
|
||||
* scopes: s.scopes,
|
||||
* },
|
||||
* null,
|
||||
* 2
|
||||
* )
|
||||
* );
|
||||
* }
|
||||
*/
|
||||
currentBlock.seen.add(scope.id);
|
||||
currentBlock.scopes.push({shadowedBy: null, scope});
|
||||
return;
|
||||
}
|
||||
nextBlock = this.scopes[index]!;
|
||||
}
|
||||
|
||||
// Handle interleaving within a given block scope
|
||||
let found = false;
|
||||
for (let i = 0; i < nextBlock.scopes.length; i++) {
|
||||
const current = nextBlock.scopes[i]!;
|
||||
if (current.scope.id === scope.id) {
|
||||
found = true;
|
||||
if (current.shadowedBy !== null) {
|
||||
this.joinedScopes.union([current.shadowedBy, current.scope]);
|
||||
}
|
||||
} else if (found && current.shadowedBy === null) {
|
||||
// `scope` is shadowing `current` and may interleave
|
||||
current.shadowedBy = scope;
|
||||
if (current.scope.range.end > scope.range.end) {
|
||||
/*
|
||||
* Current is shadowed by `scope`, and we know that `current` will mutate
|
||||
* again (per its range), so the scopes are already known to interleave.
|
||||
*
|
||||
* Eagerly extend the ranges of the scopes so that we don't prematurely end
|
||||
* a scope relative to its eventual post-merge mutable range
|
||||
*/
|
||||
const end = makeInstructionId(
|
||||
Math.max(current.scope.range.end, scope.range.end),
|
||||
);
|
||||
current.scope.range.end = end;
|
||||
scope.range.end = end;
|
||||
this.joinedScopes.union([current.scope, scope]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!currentBlock.seen.has(scope.id)) {
|
||||
currentBlock.seen.add(scope.id);
|
||||
currentBlock.scopes.push({shadowedBy: null, scope});
|
||||
}
|
||||
}
|
||||
|
||||
enter(fn: () => void): void {
|
||||
this.scopes.push(new BlockScope());
|
||||
fn();
|
||||
this.scopes.pop();
|
||||
}
|
||||
|
||||
complete(): void {
|
||||
this.joinedScopes.forEach((scope, groupScope) => {
|
||||
if (scope !== groupScope) {
|
||||
groupScope.range.start = makeInstructionId(
|
||||
Math.min(groupScope.range.start, scope.range.start),
|
||||
);
|
||||
groupScope.range.end = makeInstructionId(
|
||||
Math.max(groupScope.range.end, scope.range.end),
|
||||
);
|
||||
}
|
||||
});
|
||||
for (const [operand, originalScope] of this.operandScopes) {
|
||||
const mergedScope = this.joinedScopes.find(originalScope);
|
||||
if (mergedScope !== null) {
|
||||
operand.identifier.scope = mergedScope;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -26,7 +26,7 @@ import {
|
||||
} from '../HIR';
|
||||
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
|
||||
import {assertExhaustive, getOrInsertDefault} from '../Utils/utils';
|
||||
import {getPlaceScope} from './BuildReactiveBlocks';
|
||||
import {getPlaceScope} from '../HIR/HIR';
|
||||
import {
|
||||
ReactiveFunctionTransform,
|
||||
ReactiveFunctionVisitor,
|
||||
|
||||
@@ -6,18 +6,13 @@
|
||||
*/
|
||||
|
||||
export {alignObjectMethodScopes} from './AlignObjectMethodScopes';
|
||||
export {alignReactiveScopesToBlockScopes} from './AlignReactiveScopesToBlockScopes';
|
||||
export {assertScopeInstructionsWithinScopes} from './AssertScopeInstructionsWithinScope';
|
||||
export {assertWellFormedBreakTargets} from './AssertWellFormedBreakTargets';
|
||||
export {buildReactiveBlocks} from './BuildReactiveBlocks';
|
||||
export {buildReactiveFunction} from './BuildReactiveFunction';
|
||||
export {codegenFunction, type CodegenFunction} from './CodegenReactiveFunction';
|
||||
export {extractScopeDeclarationsFromDestructuring} from './ExtractScopeDeclarationsFromDestructuring';
|
||||
export {flattenReactiveLoops} from './FlattenReactiveLoops';
|
||||
export {flattenScopesWithHooksOrUse} from './FlattenScopesWithHooksOrUse';
|
||||
export {inferReactiveScopeVariables} from './InferReactiveScopeVariables';
|
||||
export {memoizeFbtAndMacroOperandsInSameScope} from './MemoizeFbtAndMacroOperandsInSameScope';
|
||||
export {mergeOverlappingReactiveScopes} from './MergeOverlappingReactiveScopes';
|
||||
export {mergeReactiveScopesThatInvalidateTogether} from './MergeReactiveScopesThatInvalidateTogether';
|
||||
export {printReactiveFunction} from './PrintReactiveFunction';
|
||||
export {promoteUsedTemporaries} from './PromoteUsedTemporaries';
|
||||
|
||||
Reference in New Issue
Block a user