/
githubmr
/
facebook-react
Обзор
Документация
Войти
/
githubmr
/
facebook-react
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
compiler/packages/babel-plugin-react-compiler/src/HIR/BuildReactiveScopeTerminalsHIR.ts
318 строк
8 KB
lauren
[compiler] Migrate CompilerError.invariant to new CompilerDiagnostic infra (#34403)
06 сен 2025, 19:58
Не верифицирован
06 сен 2025, 19:58
474f258
Код
Авторство
О чём код?
/** * 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 {getScopes, recursivelyTraverseItems} from './AssertValidBlockNesting'; import {Environment} from './Environment'; import { BasicBlock, BlockId, GeneratedSource, GotoTerminal, GotoVariant, HIRFunction, InstructionId, ReactiveScope, ReactiveScopeTerminal, ScopeId, } from './HIR'; import { fixScopeAndIdentifierRanges, markInstructionIds, markPredecessors, reversePostorderBlocks, } from './HIRBuilder'; /** * This pass assumes that all program blocks are properly nested with respect to fallthroughs * (e.g. a valid javascript AST). * Given a function whose reactive scope ranges have been correctly aligned and merged, * this pass rewrites blocks to introduce ReactiveScopeTerminals and their fallthrough blocks. * e.g. * ```js * // source * [0] ... * [1] const x = []; ⌝ scope range * [2] if (cond) { | * [3] x.push(a); | * } | * [4] x.push(b); ⌟ * [5] ... * * // before this pass * bb0: * [0] * [1] * [2] * If ($2) then bb1 else bb2 (fallthrough=bb2) * bb1: * [3] * Goto bb2 * bb2: * [4] * [5] * * // after this pass * bb0: * [0] * ScopeTerminal goto=bb3 (fallthrough=bb4) <-- new * bb3: <-- new * [1] * [2] * If ($2) then bb1 else bb2 (fallthrough=bb2) * bb1: * [3] * Goto bb2 * bb2: * [4] * Goto bb4 <-- new * bb4: <-- new * [5] * ``` */ export function buildReactiveScopeTerminalsHIR(fn: HIRFunction): void { /** * Step 1: * Traverse all blocks to build up a list of rewrites. We also pre-allocate the * fallthrough ID here as scope start terminals and scope end terminals both * require a fallthrough block. */ const queuedRewrites: Array<TerminalRewriteInfo> = []; recursivelyTraverseItems( [...getScopes(fn)], scope => scope.range, { fallthroughs: new Map(), rewrites: queuedRewrites, env: fn.env, }, pushStartScopeTerminal, pushEndScopeTerminal, ); /** * Step 2: * Traverse all blocks to apply rewrites. Here, we split blocks as described at * the top of this file to add scope terminals and fallthroughs. */ const rewrittenFinalBlocks = new Map<BlockId, BlockId>(); const nextBlocks = new Map<BlockId, BasicBlock>(); /** * reverse queuedRewrites to pop off the end as we traverse instructions in * ascending order */ queuedRewrites.reverse(); for (const [, block] of fn.body.blocks) { const context: RewriteContext = { nextBlockId: block.id, rewrites: [], nextPreds: block.preds, instrSliceIdx: 0, source: block, }; /** * Handle queued terminal rewrites at their nearest instruction ID. * Note that multiple terminal rewrites may map to the same instruction ID. */ for (let i = 0; i < block.instructions.length + 1; i++) { const instrId = i < block.instructions.length ? block.instructions[i].id : block.terminal.id; let rewrite = queuedRewrites.at(-1); while (rewrite != null && rewrite.instrId <= instrId) { handleRewrite(rewrite, i, context); queuedRewrites.pop(); rewrite = queuedRewrites.at(-1); } } if (context.rewrites.length > 0) { const finalBlock: BasicBlock = { id: context.nextBlockId, kind: block.kind, preds: context.nextPreds, terminal: block.terminal, instructions: block.instructions.slice(context.instrSliceIdx), phis: new Set(), }; context.rewrites.push(finalBlock); for (const b of context.rewrites) { nextBlocks.set(b.id, b); } rewrittenFinalBlocks.set(block.id, finalBlock.id); } else { nextBlocks.set(block.id, block); } } const originalBlocks = fn.body.blocks; fn.body.blocks = nextBlocks; /** * Step 3: * Repoint phis when they refer to a rewritten block. */ for (const [, block] of originalBlocks) { for (const phi of block.phis) { for (const [originalId, value] of phi.operands) { const newId = rewrittenFinalBlocks.get(originalId); if (newId != null) { phi.operands.delete(originalId); phi.operands.set(newId, value); } } } } /** * Step 4: * Fixup the HIR to restore RPO, ensure correct predecessors, and * renumber instructions. Note that the renumbering instructions * invalidates scope and identifier ranges, so we fix them in the * next step. */ reversePostorderBlocks(fn.body); markPredecessors(fn.body); markInstructionIds(fn.body); /** * Step 5: * Fix scope and identifier ranges to account for renumbered instructions */ fixScopeAndIdentifierRanges(fn.body); } type TerminalRewriteInfo = | { kind: 'StartScope'; blockId: BlockId; fallthroughId: BlockId; instrId: InstructionId; scope: ReactiveScope; } | { kind: 'EndScope'; instrId: InstructionId; fallthroughId: BlockId; }; /** * Helpers for reversing scope ranges to gather terminal rewrite information */ type ScopeTraversalContext = { // cache allocated fallthroughs for start/end scope terminal pairs fallthroughs: Map<ScopeId, BlockId>; rewrites: Array<TerminalRewriteInfo>; env: Environment; }; function pushStartScopeTerminal( scope: ReactiveScope, context: ScopeTraversalContext, ): void { const blockId = context.env.nextBlockId; const fallthroughId = context.env.nextBlockId; context.rewrites.push({ kind: 'StartScope', blockId, fallthroughId, instrId: scope.range.start, scope, }); context.fallthroughs.set(scope.id, fallthroughId); } function pushEndScopeTerminal( scope: ReactiveScope, context: ScopeTraversalContext, ): void { const fallthroughId = context.fallthroughs.get(scope.id); CompilerError.invariant(fallthroughId != null, { reason: 'Expected scope to exist', description: null, details: [ { kind: 'error', loc: GeneratedSource, message: null, }, ], }); context.rewrites.push({ kind: 'EndScope', fallthroughId, instrId: scope.range.end, }); } type RewriteContext = { source: BasicBlock; instrSliceIdx: number; nextPreds: Set<BlockId>; nextBlockId: BlockId; rewrites: Array<BasicBlock>; }; /** * Create a block rewrite by slicing a set of instructions from source. * Since scope start-ends always end with a GOTO to the next instruction * from the source block, we directly connect rewritten blocks using state * from `context`. * * Source: * bb0: * instr1, instr2, instr3, instr4, [[ original terminal ]] * Rewritten: * bb0: * instr1, [[ scope start block=bb1]] * bb1: * instr2, instr3, [[ scope end goto=bb2 ]] * bb2: * instr4, [[ original terminal ]] */ function handleRewrite( terminalInfo: TerminalRewriteInfo, idx: number, context: RewriteContext, ): void { // TODO make consistent instruction IDs instead of reusing const terminal: ReactiveScopeTerminal | GotoTerminal = terminalInfo.kind === 'StartScope' ? { kind: 'scope', fallthrough: terminalInfo.fallthroughId, block: terminalInfo.blockId, scope: terminalInfo.scope, id: terminalInfo.instrId, loc: GeneratedSource, } : { kind: 'goto', variant: GotoVariant.Break, block: terminalInfo.fallthroughId, id: terminalInfo.instrId, loc: GeneratedSource, }; const currBlockId = context.nextBlockId; context.rewrites.push({ kind: context.source.kind, id: currBlockId, instructions: context.source.instructions.slice(context.instrSliceIdx, idx), preds: context.nextPreds, // Only the first rewrite should reuse source block phis phis: context.rewrites.length === 0 ? context.source.phis : new Set(), terminal, }); context.nextPreds = new Set([currBlockId]); context.nextBlockId = terminalInfo.kind === 'StartScope' ? terminalInfo.blockId : terminalInfo.fallthroughId; context.instrSliceIdx = idx; }