Autogenerated summaries of each of the compiler passes which allow agents to get the key ideas of a compiler pass, including key input/output invariants, without having to reprocess the file each time. In the subsequent diff this seemed to help. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35595). * #35607 * #35298 * #35596 * #35573 * __->__ #35595 * #35539
6.0 KiB
buildReactiveScopeTerminalsHIR
File
src/HIR/BuildReactiveScopeTerminalsHIR.ts
Purpose
This pass transforms the HIR by inserting ReactiveScopeTerminal nodes to explicitly demarcate the boundaries of reactive scopes within the control flow graph. It converts the implicit scope ranges (stored on identifiers as identifier.scope.range) into explicit control flow structure by:
- Inserting a
scopeterminal at the start of each reactive scope - Inserting a
gototerminal at the end of each reactive scope - Creating fallthrough blocks to properly connect the scopes to the rest of the CFG
This transformation makes scope boundaries first-class elements in the CFG, which is essential for later passes that generate the memoization code (the if ($[n] !== dep) checks).
Input Invariants
- Properly nested scopes and blocks: The pass assumes
assertValidBlockNestinghas passed, meaning all program blocks and reactive scopes form a proper tree hierarchy - Aligned scope ranges: Reactive scope ranges have been correctly aligned and merged by previous passes
- Valid instruction IDs: All instructions have sequential IDs that define the scope boundaries
- Scopes attached to identifiers: Reactive scopes are found by traversing all
Placeoperands and collecting unique non-empty scopes
Output Guarantees
- Explicit scope terminals: Each reactive scope is represented in the CFG as a
ReactiveScopeTerminalwith:block- The BlockId containing the scope's instructionsfallthrough- The BlockId that executes after the scope
- Proper block structure: Original blocks are split at scope boundaries
- Restored HIR invariants: The pass restores RPO ordering, predecessor sets, instruction IDs, and scope/identifier ranges
- Updated phi nodes: Phi operands are repointed when their source blocks are split
Algorithm
Step 1: Collect Scope Rewrites
for each reactive scope (in range pre-order):
push StartScope rewrite at scope.range.start
push EndScope rewrite at scope.range.end
The recursivelyTraverseItems helper traverses scopes in pre-order (outer scopes before inner scopes).
Step 2: Apply Rewrites by Splitting Blocks
reverse queuedRewrites (to pop in ascending instruction order)
for each block:
for each instruction (or terminal):
while there are rewrites <= current instruction ID:
split block at current index
insert scope terminal (for start) or goto terminal (for end)
emit final block segment with original terminal
Step 3: Repoint Phi Nodes
When a block is split, its final segment gets a new BlockId. Phi operands that referenced the original block are updated to reference the new final block.
Step 4: Restore HIR Invariants
- Recompute RPO (reverse post-order) block traversal
- Recalculate predecessor sets
- Renumber instruction IDs
- Fix scope and identifier ranges to match new instruction IDs
Key Data Structures
TerminalRewriteInfo
type TerminalRewriteInfo =
| {
kind: 'StartScope';
blockId: BlockId; // New block for scope content
fallthroughId: BlockId; // Block after scope ends
instrId: InstructionId; // Where to insert
scope: ReactiveScope; // The scope being created
}
| {
kind: 'EndScope';
instrId: InstructionId; // Where to insert
fallthroughId: BlockId; // Same as corresponding StartScope
};
RewriteContext
type RewriteContext = {
source: BasicBlock; // Original block being split
instrSliceIdx: number; // Current slice start index
nextPreds: Set<BlockId>; // Predecessors for next emitted block
nextBlockId: BlockId; // BlockId for next emitted block
rewrites: Array<BasicBlock>; // Accumulated split blocks
};
ScopeTraversalContext
type ScopeTraversalContext = {
fallthroughs: Map<ScopeId, BlockId>; // Cache: scope -> its fallthrough block
rewrites: Array<TerminalRewriteInfo>;
env: Environment;
};
Edge Cases
Multiple Rewrites at Same Instruction ID
The while loop in Step 2 handles multiple scope start/ends at the same instruction ID.
Nested Scopes
The pre-order traversal ensures outer scopes are processed before inner scopes, creating proper nesting in the CFG.
Empty Blocks After Split
When a scope boundary falls at the start of a block, the split may create a block with no instructions (only a terminal).
Control Flow Within Scopes
The pass preserves existing control flow (if/else, loops) within scopes; it only adds scope entry/exit points.
Early Returns
When a return occurs within a scope, the scope terminal still has a fallthrough block, but that block may contain Unreachable terminal.
TODOs
Line 283-284:
// TODO make consistent instruction IDs instead of reusing
Example
Fixture: reactive-scopes-if.js
Before BuildReactiveScopeTerminalsHIR:
bb0 (block):
[1] $29_@0[1:22] = Array [] // x with scope @0 range [1:22]
[2] StoreLocal x$30_@0 = $29_@0
[3] $32 = LoadLocal a$26
[4] If ($32) then:bb2 else:bb3 fallthrough=bb1
bb2:
[5] $33_@1[5:11] = Array [] // y with scope @1 range [5:11]
...
After BuildReactiveScopeTerminalsHIR:
bb0 (block):
[1] Scope @0 [1:28] block=bb9 fallthrough=bb10 // <-- scope terminal inserted
bb9:
[2] $29_@0 = Array []
[3] StoreLocal x$30_@0 = $29_@0
[4] $32 = LoadLocal a$26
[5] If ($32) then:bb2 else:bb3 fallthrough=bb1
bb2:
[6] Scope @1 [6:14] block=bb11 fallthrough=bb12 // <-- nested scope terminal
bb11:
[7] $33_@1 = Array []
...
[13] Goto bb12 // <-- scope end goto
bb12:
...
bb1:
[27] Goto bb10 // <-- scope @0 end goto
bb10:
[28] $50 = LoadLocal x$30_@0
[29] Return $50
The key transformation is that scope boundaries become explicit control flow: a Scope terminal enters the scope content block, and a Goto terminal exits to the fallthrough block. This structure is later used to generate the memoization checks.