/**
* 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,
});
}
}
}