/
githubmr
/
facebook-react
Обзор
Документация
Войти
/
githubmr
/
facebook-react
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
compiler/packages/babel-plugin-react-compiler/src/HIR/MergeOverlappingReactiveScopesHIR.ts
313 строк
10 KB
lauren
[compiler] Add missing copyrights (#33004)
24 апр 2025, 05:04
Не верифицирован
24 апр 2025, 05:04
ad09027
Код
Авторство
О чём код?
/** * 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 { HIRFunction, InstructionId, Place, ReactiveScope, makeInstructionId, } from '.'; import {getPlaceScope} from '../HIR/HIR'; import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables'; import DisjointSet from '../Utils/DisjointSet'; import {getOrInsertDefault} from '../Utils/utils'; import { eachInstructionLValue, eachInstructionOperand, eachTerminalOperand, } from './visitors'; /** * While previous passes ensure that reactive scopes span valid sets of program * blocks, pairs of reactive scopes may still be inconsistent with respect to * each other. * * (a) Reactive scopes ranges must form valid blocks in the resulting javascript * program. Any two scopes must either be entirely disjoint or one scope must be * nested within the other. * ```js * // Scopes 1:3 and 3:5 are valid because they contain no common instructions * [1] ⌝ * [2] ⌟ * [3] ⌝ * [4] ⌟ * // Scopes 1:3 and 1:5 are valid because the former is nested within the other * [1] ⌝ ⌝ * [2] ⌟ | * [3] | * [4] ⌟ * // Scopes 1:4 and 2:5 are invalid because we cannot produce if-else memo * // blocks representing these scopes in the output program. * [1] ⌝ * [2] | ⌝ * [3] ⌟ | * [4] ⌟ * ``` * * (b) A scope's own instructions may only mutate that scope. * For each reactive scope, we currently produce exactly one if-block which * spans the instruction range of the scope. In this simple example, instr [2] * does not mutate any values but is included within scope @0. * ```js * // IR instructions * [1] (writes to scope @0's values) * [2] (does not mutate anything) * [3] (writes to scope @0's values) * * // javascript output * if (( scope @0's dependencies changed )) { * [1] * [2] * [3] * } * ``` * Nested scopes may be modeled as a tree in which child scopes are contained * within parent scopes. This corresponds to nested if-else memo blocks in the * output program). An instruction may only mutate its own "active" scope. * ```js * // Active scopes for a simple program * scope @0 { * [0] (active scope=@0) * scope @1 { * [1] (active scope=@1) * [2] (active scope=@1) * } * [3] (active scope=@0) * } * [4] (no active scope) * * // In this example, scopes @0 and @1 must be merged because instr [2]'s * // active scope is scope@1 but it mutates scope@0. * scope @0, produces x { * [0] x = [] * scope @1, produces y { * [1] y = [] * [2] x.push(2) * [3] y.push(3) * } * [3] x.push(1) * } * ``` * * As mentioned, these constraints arise entirely from the current design of * compiler output. * - instruction ordering is preserved (otherwise, disjoint ranges for scopes * may be produced by reordering their mutating instructions) * - exactly one if-else block per scope, which does not allow the composition * of a reactive scope from disconnected instruction ranges. */ export function mergeOverlappingReactiveScopesHIR(fn: HIRFunction): void { /** * Collect all scopes eagerly because some scopes begin before the first * instruction that references them (due to alignReactiveScopesToBlocks) */ const scopesInfo = collectScopeInfo(fn); /** * Iterate through scopes and instructions to find which should be merged */ const joinedScopes = getOverlappingReactiveScopes(fn, scopesInfo); /** * Merge scopes and rewrite all references */ 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 [place, originalScope] of scopesInfo.placeScopes) { const nextScope = joinedScopes.find(originalScope); if (nextScope !== null && nextScope !== originalScope) { place.identifier.scope = nextScope; } } } type ScopeInfo = { scopeStarts: Array<{id: InstructionId; scopes: Set<ReactiveScope>}>; scopeEnds: Array<{id: InstructionId; scopes: Set<ReactiveScope>}>; placeScopes: Map<Place, ReactiveScope>; }; type TraversalState = { joined: DisjointSet<ReactiveScope>; activeScopes: Array<ReactiveScope>; }; function collectScopeInfo(fn: HIRFunction): ScopeInfo { const scopeStarts: Map<InstructionId, Set<ReactiveScope>> = new Map(); const scopeEnds: Map<InstructionId, Set<ReactiveScope>> = new Map(); const placeScopes: Map<Place, ReactiveScope> = new Map(); function collectPlaceScope(place: Place): void { const scope = place.identifier.scope; if (scope != null) { placeScopes.set(place, scope); /** * Record both mutating and non-mutating scopes to merge scopes with * still-mutating values with inner scopes that alias those values * (see `nonmutating-capture-in-unsplittable-memo-block`) * * Note that this isn't perfect, as it also leads to merging of mutating * scopes with JSX single-instruction scopes (see `mutation-within-jsx`) */ if (scope.range.start !== scope.range.end) { getOrInsertDefault(scopeStarts, scope.range.start, new Set()).add( scope, ); getOrInsertDefault(scopeEnds, scope.range.end, new Set()).add(scope); } } } for (const [, block] of fn.body.blocks) { for (const instr of block.instructions) { for (const operand of eachInstructionLValue(instr)) { collectPlaceScope(operand); } for (const operand of eachInstructionOperand(instr)) { collectPlaceScope(operand); } } for (const operand of eachTerminalOperand(block.terminal)) { collectPlaceScope(operand); } } return { scopeStarts: [...scopeStarts.entries()] .map(([id, scopes]) => ({id, scopes})) .sort((a, b) => b.id - a.id), scopeEnds: [...scopeEnds.entries()] .map(([id, scopes]) => ({id, scopes})) .sort((a, b) => b.id - a.id), placeScopes, }; } function visitInstructionId( id: InstructionId, {scopeEnds, scopeStarts}: ScopeInfo, {activeScopes, joined}: TraversalState, ): void { /** * Handle all scopes that end at this instruction. */ const scopeEndTop = scopeEnds.at(-1); if (scopeEndTop != null && scopeEndTop.id <= id) { scopeEnds.pop(); /** * Match scopes that end at this instruction with our stack of active * scopes (from traversal state). We need to sort these in descending * order of start IDs because the scopes stack is ordered as such */ const scopesSortedStartDescending = [...scopeEndTop.scopes].sort( (a, b) => b.range.start - a.range.start, ); for (const scope of scopesSortedStartDescending) { const idx = activeScopes.indexOf(scope); if (idx !== -1) { /** * Detect and merge all overlapping scopes. `activeScopes` is ordered * by scope start, so every active scope between a completed scope s * and the top of the stack (1) started later than s and (2) completes after s. */ if (idx !== activeScopes.length - 1) { joined.union([scope, ...activeScopes.slice(idx + 1)]); } activeScopes.splice(idx, 1); } } } /** * Handle all scopes that begin at this instruction by adding them * to the scopes stack */ const scopeStartTop = scopeStarts.at(-1); if (scopeStartTop != null && scopeStartTop.id <= id) { scopeStarts.pop(); const scopesSortedEndDescending = [...scopeStartTop.scopes].sort( (a, b) => b.range.end - a.range.end, ); activeScopes.push(...scopesSortedEndDescending); /** * Merge all identical scopes (ones with the same start and end), * as they end up with the same reactive block */ for (let i = 1; i < scopesSortedEndDescending.length; i++) { const prev = scopesSortedEndDescending[i - 1]; const curr = scopesSortedEndDescending[i]; if (prev.range.end === curr.range.end) { joined.union([prev, curr]); } } } } function visitPlace( id: InstructionId, place: Place, {activeScopes, joined}: TraversalState, ): void { /** * If an instruction mutates an outer scope, flatten all scopes from the top * of the stack to the mutated outer scope. */ const placeScope = getPlaceScope(id, place); if (placeScope != null && isMutable({id}, place)) { const placeScopeIdx = activeScopes.indexOf(placeScope); if (placeScopeIdx !== -1 && placeScopeIdx !== activeScopes.length - 1) { joined.union([placeScope, ...activeScopes.slice(placeScopeIdx + 1)]); } } } function getOverlappingReactiveScopes( fn: HIRFunction, context: ScopeInfo, ): DisjointSet<ReactiveScope> { const state: TraversalState = { joined: new DisjointSet<ReactiveScope>(), activeScopes: [], }; for (const [, block] of fn.body.blocks) { for (const instr of block.instructions) { visitInstructionId(instr.id, context, state); for (const place of eachInstructionOperand(instr)) { if ( (instr.value.kind === 'FunctionExpression' || instr.value.kind === 'ObjectMethod') && place.identifier.type.kind === 'Primitive' ) { continue; } visitPlace(instr.id, place, state); } for (const place of eachInstructionLValue(instr)) { visitPlace(instr.id, place, state); } } visitInstructionId(block.terminal.id, context, state); for (const place of eachTerminalOperand(block.terminal)) { visitPlace(block.terminal.id, place, state); } } return state.joined; }