[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:
Mofei Zhang
2024-09-06 13:02:08 -04:00
committed by Mofei Zhang
parent 802fb1391d
commit d9709cbc1c
11 changed files with 20 additions and 929 deletions
@@ -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
@@ -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';
@@ -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,
});
}
}
}
@@ -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;
/*
@@ -14,7 +14,7 @@ import {
ReactiveScopeBlock,
ScopeId,
} from '../HIR';
import {getPlaceScope} from './BuildReactiveBlocks';
import {getPlaceScope} from '../HIR/HIR';
import {ReactiveFunctionVisitor} from './visitors';
/*
@@ -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;
}
@@ -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}\``,
);
}
}
}
}
@@ -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;
}
}
}
}
@@ -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;
}
}
}
}
@@ -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';