From 1d2edc2486ee8cfb7c896f2f5dec403ebf65f87f Mon Sep 17 00:00:00 2001 From: Mofei Zhang Date: Thu, 5 Sep 2024 20:09:35 -0400 Subject: [PATCH] Update [ghstack-poisoned] --- .../src/HIR/HIR.ts | 17 +- .../HIR/MergeOverlappingReactiveScopesHIR.ts | 2 +- .../AlignReactiveScopesToBlockScopes.ts | 187 ------------ .../AlignReactiveScopesToBlockScopesHIR.ts | 2 +- .../AssertScopeInstructionsWithinScope.ts | 2 +- .../src/ReactiveScopes/BuildReactiveBlocks.ts | 244 --------------- .../ReactiveScopes/FlattenReactiveLoops.ts | 84 ------ .../FlattenScopesWithHooksOrUse.ts | 123 -------- .../MergeOverlappingReactiveScopes.ts | 281 ------------------ .../ReactiveScopes/PruneNonEscapingScopes.ts | 2 +- .../src/ReactiveScopes/index.ts | 5 - 11 files changed, 20 insertions(+), 929 deletions(-) delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopes.ts delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveBlocks.ts delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/FlattenReactiveLoops.ts delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/FlattenScopesWithHooksOrUse.ts delete mode 100644 compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeOverlappingReactiveScopes.ts diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index 3a04a4c3c9d..930dd79f2fd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -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 diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeOverlappingReactiveScopesHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeOverlappingReactiveScopesHIR.ts index 98645d5c549..a3740539b29 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeOverlappingReactiveScopesHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeOverlappingReactiveScopesHIR.ts @@ -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'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopes.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopes.ts deleted file mode 100644 index 132788f0d41..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopes.ts +++ /dev/null @@ -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 { - 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> = []; - - /* - * 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 = []; - - /* - * 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 = new Set(); - - currentScopes(): Array { - 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, - }); - } - } -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts index 3e8329679cf..2b4e890a40d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts @@ -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; /* diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AssertScopeInstructionsWithinScope.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AssertScopeInstructionsWithinScope.ts index 2bce1000501..718e28f6101 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AssertScopeInstructionsWithinScope.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AssertScopeInstructionsWithinScope.ts @@ -14,7 +14,7 @@ import { ReactiveScopeBlock, ScopeId, } from '../HIR'; -import {getPlaceScope} from './BuildReactiveBlocks'; +import {getPlaceScope} from '../HIR/HIR'; import {ReactiveFunctionVisitor} from './visitors'; /* diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveBlocks.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveBlocks.ts deleted file mode 100644 index 7737423e5e1..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveBlocks.ts +++ /dev/null @@ -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 = []; - #scopes: Set = 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; -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/FlattenReactiveLoops.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/FlattenReactiveLoops.ts deleted file mode 100644 index 2119b8c1672..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/FlattenReactiveLoops.ts +++ /dev/null @@ -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 { - override transformScope( - scope: ReactiveScopeBlock, - isWithinLoop: boolean, - ): Transformed { - 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, - 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}\``, - ); - } - } - } -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/FlattenScopesWithHooksOrUse.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/FlattenScopesWithHooksOrUse.ts deleted file mode 100644 index 753cd3d6e87..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/FlattenScopesWithHooksOrUse.ts +++ /dev/null @@ -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 { - override transformScope( - scope: ReactiveScopeBlock, - outerState: State, - ): Transformed { - 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; - } - } - } -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeOverlappingReactiveScopes.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeOverlappingReactiveScopes.ts deleted file mode 100644 index 733730fdec5..00000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeOverlappingReactiveScopes.ts +++ /dev/null @@ -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(
{y}
); ⎮ - * } ⌟ - * } - * ``` - * - * 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 { - 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 = new Set(); - scopes: Array = []; -} - -type ShadowableReactiveScope = { - scope: ReactiveScope; - shadowedBy: ReactiveScope | null; -}; - -class Context { - scopes: Array = []; - seenScopes: Set = new Set(); - joinedScopes: DisjointSet = new DisjointSet(); - operandScopes: Map = 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; - } - } - } -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts index 8033d05e2b3..5a9aa6b2a73 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts @@ -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, diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/index.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/index.ts index 55f67fc2f7d..eb778305611 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/index.ts @@ -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';