@samitouri / QOS-React / commits / 870cccd656

[compiler] Summaries of the compiler passes to assist agents in development (#35595)

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

Joseph Savona committed Jan 23, 2026 at 11:26 UTC 870cccd656bc0188c04ef8b53e8f7f1b78d486d9
57 files changed +10072
compiler/CLAUDE.md
+2
@@ -4,6 +4,8 @@ This document contains knowledge about the React Compiler gathered during develo
4
5 ## Project Structure
6
7 +When modifying the compiler, you MUST read the documentation about that pass in `compiler/packages/babel-plugin-react-compiler/docs/passes/` to learn more about the role of that pass within the compiler.
8 +
9 - `packages/babel-plugin-react-compiler/` - Main compiler package
10 - `src/HIR/` - High-level Intermediate Representation types and utilities
11 - `src/Inference/` - Effect inference passes (aliasing, mutation, etc.)
compiler/packages/babel-plugin-react-compiler/docs/passes/01-lower.md new
+157
@@ -0,0 +1,157 @@
1 +# lower (BuildHIR)
2 +
3 +## File
4 +`src/HIR/BuildHIR.ts`
5 +
6 +## Purpose
7 +Converts a Babel AST function node into a High-level Intermediate Representation (HIR), which represents code as a control-flow graph (CFG) with basic blocks, instructions, and terminals. This is the first major transformation pass in the React Compiler pipeline, enabling precise expression-level memoization analysis.
8 +
9 +## Input Invariants
10 +- Input must be a valid Babel `NodePath<t.Function>` (FunctionDeclaration, FunctionExpression, or ArrowFunctionExpression)
11 +- The function must be a component or hook (determined by the environment)
12 +- Babel scope analysis must be available for binding resolution
13 +- An `Environment` instance must be provided with compiler configuration
14 +- Optional `bindings` map for nested function lowering (recursive calls)
15 +- Optional `capturedRefs` map for context variables captured from outer scope
16 +
17 +## Output Guarantees
18 +- Returns `Result<HIRFunction, CompilerError>` - either a successfully lowered function or compilation errors
19 +- The HIR function contains:
20 + - A complete CFG with basic blocks (`body.blocks: Map<BlockId, BasicBlock>`)
21 + - Each block has an array of instructions and exactly one terminal
22 + - All control flow is explicit (if/else, loops, switch, logical operators, ternary)
23 + - Parameters are converted to `Place` or `SpreadPattern`
24 + - Context captures are tracked in `context` array
25 + - Function metadata (id, async, generator, directives)
26 +- All identifiers get unique `IdentifierId` values
27 +- Instructions have placeholder instruction IDs (set to 0, assigned later)
28 +- Effects are null (populated by later inference passes)
29 +
30 +## Algorithm
31 +The lowering algorithm uses a recursive descent pattern with a `HIRBuilder` helper class:
32 +
33 +1. **Initialization**: Create an `HIRBuilder` with environment and optional bindings. Process captured context variables.
34 +
35 +2. **Parameter Processing**: For each function parameter:
36 + - Simple identifiers: resolve binding and create Place
37 + - Patterns (object/array): create temporary Place, then emit destructuring assignments
38 + - Rest elements: wrap in SpreadPattern
39 + - Unsupported: emit Todo error
40 +
41 +3. **Body Processing**:
42 + - Arrow function expressions: lower body expression to temporary, emit implicit return
43 + - Block statements: recursively lower each statement
44 +
45 +4. **Statement Lowering** (`lowerStatement`): Handle each statement type:
46 + - **Control flow**: Create separate basic blocks for branches, loops connect back to conditional blocks
47 + - **Variable declarations**: Create `DeclareLocal`/`DeclareContext` or `StoreLocal`/`StoreContext` instructions
48 + - **Expressions**: Lower to temporary and discard result
49 + - **Hoisting**: Detect forward references and emit `DeclareContext` for hoisted identifiers
50 +
51 +5. **Expression Lowering** (`lowerExpression`): Convert expressions to `InstructionValue`:
52 + - **Identifiers**: Create `LoadLocal`, `LoadContext`, or `LoadGlobal` based on binding
53 + - **Literals**: Create `Primitive` values
54 + - **Operators**: Create `BinaryExpression`, `UnaryExpression` etc.
55 + - **Calls**: Distinguish `CallExpression` vs `MethodCall` (member expression callee)
56 + - **Control flow expressions**: Create separate value blocks for branches (ternary, logical, optional chaining)
57 + - **JSX**: Lower to `JsxExpression` with lowered tag, props, and children
58 +
59 +6. **Block Management**: The builder maintains:
60 + - A current work-in-progress block accumulating instructions
61 + - Completed blocks map
62 + - Scope stack for break/continue resolution
63 + - Exception handler stack for try/catch
64 +
65 +7. **Termination**: Add implicit void return at end if no explicit return
66 +
67 +## Key Data Structures
68 +
69 +### HIRBuilder (from HIRBuilder.ts)
70 +- `#current: WipBlock` - Work-in-progress block being populated
71 +- `#completed: Map<BlockId, BasicBlock>` - Finished blocks
72 +- `#scopes: Array<Scope>` - Stack for break/continue target resolution (LoopScope, LabelScope, SwitchScope)
73 +- `#exceptionHandlerStack: Array<BlockId>` - Stack of catch handlers for try/catch
74 +- `#bindings: Bindings` - Map of variable names to their identifiers
75 +- `#context: Map<t.Identifier, SourceLocation>` - Captured context variables
76 +- Methods: `push()`, `reserve()`, `enter()`, `terminate()`, `terminateWithContinuation()`
77 +
78 +### Core HIR Types
79 +- **BasicBlock**: Contains `instructions: Array<Instruction>`, `terminal: Terminal`, `preds: Set<BlockId>`, `phis: Set<Phi>`, `kind: BlockKind`
80 +- **Instruction**: Contains `id`, `lvalue` (Place), `value` (InstructionValue), `effects` (null initially), `loc`
81 +- **Terminal**: Block terminator - `if`, `branch`, `goto`, `return`, `throw`, `for`, `while`, `switch`, `ternary`, `logical`, etc.
82 +- **Place**: Reference to a value - `{kind: 'Identifier', identifier, effect, reactive, loc}`
83 +- **InstructionValue**: The operation - `LoadLocal`, `StoreLocal`, `CallExpression`, `BinaryExpression`, `FunctionExpression`, etc.
84 +
85 +### Block Kinds
86 +- `block` - Regular sequential block
87 +- `loop` - Loop header/test block
88 +- `value` - Block that produces a value (ternary/logical branches)
89 +- `sequence` - Sequence expression block
90 +- `catch` - Exception handler block
91 +
92 +## Edge Cases
93 +
94 +1. **Hoisting**: Forward references to `let`/`const`/`function` declarations emit `DeclareContext` before the reference, enabling correct temporal dead zone handling
95 +
96 +2. **Context Variables**: Variables captured by nested functions use `LoadContext`/`StoreContext` instead of `LoadLocal`/`StoreLocal`
97 +
98 +3. **For-of/For-in Loops**: Synthesize iterator instructions (`GetIterator`, `IteratorNext`, `NextPropertyOf`)
99 +
100 +4. **Optional Chaining**: Creates nested `OptionalTerminal` structures with short-circuit branches
101 +
102 +5. **Logical Expressions**: Create branching structures where left side stores to temporary, right side only evaluated if needed
103 +
104 +6. **Try/Catch**: Adds `MaybeThrowTerminal` after each instruction in try block, modeling potential control flow to handler
105 +
106 +7. **JSX in fbt**: Tracks `fbtDepth` counter to handle whitespace differently in fbt/fbs tags
107 +
108 +8. **Unsupported Syntax**: `var` declarations, `with` statements, inline `class` declarations, `eval` - emit appropriate errors
109 +
110 +## TODOs
111 +- `returnTypeAnnotation: null, // TODO: extract the actual return type node if present`
112 +- `TODO(gsn): In the future, we could only pass in the context identifiers that are actually used by this function and its nested functions`
113 +- Multiple `// TODO remove type cast` in destructuring pattern handling
114 +- `// TODO: should JSX namespaced names be handled here as well?`
115 +
116 +## Example
117 +Input JavaScript:
118 +```javascript
119 +export default function foo(x, y) {
120 + if (x) {
121 + return foo(false, y);
122 + }
123 + return [y * 10];
124 +}
125 +```
126 +
127 +Output HIR (simplified):
128 +```
129 +foo(<unknown> x$0, <unknown> y$1): <unknown> $12
130 +bb0 (block):
131 + [1] <unknown> $6 = LoadLocal <unknown> x$0
132 + [2] If (<unknown> $6) then:bb2 else:bb1 fallthrough=bb1
133 +
134 +bb2 (block):
135 + predecessor blocks: bb0
136 + [3] <unknown> $2 = LoadGlobal(module) foo
137 + [4] <unknown> $3 = false
138 + [5] <unknown> $4 = LoadLocal <unknown> y$1
139 + [6] <unknown> $5 = Call <unknown> $2(<unknown> $3, <unknown> $4)
140 + [7] Return Explicit <unknown> $5
141 +
142 +bb1 (block):
143 + predecessor blocks: bb0
144 + [8] <unknown> $7 = LoadLocal <unknown> y$1
145 + [9] <unknown> $8 = 10
146 + [10] <unknown> $9 = Binary <unknown> $7 * <unknown> $8
147 + [11] <unknown> $10 = Array [<unknown> $9]
148 + [12] Return Explicit <unknown> $10
149 +```
150 +
151 +Key observations:
152 +- The function has 3 basic blocks: entry (bb0), consequent (bb2), alternate/fallthrough (bb1)
153 +- The if statement creates an `IfTerminal` at the end of bb0
154 +- Each branch ends with its own `ReturnTerminal`
155 +- All values are stored in temporaries (`$N`) or named identifiers (`x$0`, `y$1`)
156 +- Instructions have sequential IDs within blocks
157 +- Types and effects are `<unknown>` at this stage (populated by later passes)
compiler/packages/babel-plugin-react-compiler/docs/passes/02-enterSSA.md new
+182
@@ -0,0 +1,182 @@
1 +# enterSSA
2 +
3 +## File
4 +`src/SSA/EnterSSA.ts`
5 +
6 +## Purpose
7 +Converts the HIR from a non-SSA form (where variables can be reassigned) into Static Single Assignment (SSA) form, where each variable is defined exactly once and phi nodes are inserted at control flow join points to merge values from different paths.
8 +
9 +## Input Invariants
10 +- The HIR must have blocks in reverse postorder (predecessors visited before successors, except for back-edges)
11 +- Block predecessor information (`block.preds`) must be populated correctly
12 +- The function's `context` array must be empty for the root function (outer function declarations)
13 +- Identifiers may be reused across multiple definitions/assignments (non-SSA form)
14 +
15 +## Output Guarantees
16 +- Each identifier has a unique `IdentifierId` - no identifier is defined more than once
17 +- All operand references use the SSA-renamed identifiers
18 +- Phi nodes are inserted at join points where values from different control flow paths converge
19 +- Function parameters are SSA-renamed
20 +- Nested functions (FunctionExpression, ObjectMethod) are recursively converted to SSA form
21 +- Context variables (captured from outer scopes) are handled specially and not redefined
22 +
23 +## Algorithm
24 +The pass uses the Braun et al. algorithm ("Simple and Efficient Construction of Static Single Assignment Form") with adaptations for handling loops and nested functions.
25 +
26 +### Key Steps:
27 +1. **Block Traversal**: Iterate through blocks in order (assumed reverse postorder from previous passes)
28 +2. **Definition Tracking**: Maintain a per-block `defs` map from original identifiers to their SSA-renamed versions
29 +3. **Renaming**:
30 + - When a value is **defined** (lvalue), create a new SSA identifier with fresh `IdentifierId`
31 + - When a value is **used** (operand), look up the current SSA identifier via `getIdAt`
32 +4. **Phi Node Insertion**: When looking up an identifier at a block with multiple predecessors:
33 + - If all predecessors have been visited, create a phi node collecting values from each predecessor
34 + - If some predecessors are unvisited (back-edge/loop), create an "incomplete phi" that will be fixed later
35 +5. **Incomplete Phi Resolution**: When all predecessors of a block are finally visited, fix any incomplete phi nodes by populating their operands
36 +6. **Nested Function Handling**: Recursively apply SSA transformation to nested functions, temporarily adding a fake predecessor edge to enable identifier lookup from the enclosing scope
37 +
38 +### Phi Node Placement Logic (`getIdAt`):
39 +- If the identifier is defined locally in the current block, return it
40 +- If at entry block with no predecessors and not found, mark as unknown (global)
41 +- If some predecessors are unvisited (loop), create incomplete phi
42 +- If exactly one predecessor, recursively look up in that predecessor
43 +- If multiple predecessors, create phi node with operands from all predecessors
44 +
45 +## Key Data Structures
46 +- **SSABuilder**: Main class managing the transformation
47 + - `#states: Map<BasicBlock, State>` - Per-block state (defs map and incomplete phis)
48 + - `unsealedPreds: Map<BasicBlock, number>` - Count of unvisited predecessors per block
49 + - `#unknown: Set<Identifier>` - Identifiers assumed to be globals
50 + - `#context: Set<Identifier>` - Context variables that should not be redefined
51 +- **State**: Per-block state containing:
52 + - `defs: Map<Identifier, Identifier>` - Maps original identifiers to SSA-renamed versions
53 + - `incompletePhis: Array<IncompletePhi>` - Phi nodes waiting for predecessor values
54 +- **IncompletePhi**: Tracks a phi node created before all predecessors were visited
55 + - `oldPlace: Place` - Original place being phi'd
56 + - `newPlace: Place` - SSA-renamed phi result place
57 +- **Phi**: The actual phi node in the HIR
58 + - `place: Place` - The result of the phi
59 + - `operands: Map<BlockId, Place>` - Maps predecessor block to the place providing the value
60 +
61 +## Edge Cases
62 +- **Loops (back-edges)**: When a variable is used in a loop header before the loop body assigns it, an incomplete phi is created and later fixed when the loop body block is visited
63 +- **Globals**: If an identifier is used but never defined (reaching the entry block without a definition), it's assumed to be a global and not renamed
64 +- **Context variables**: Variables captured from an outer function scope are tracked specially and not redefined when reassigned
65 +- **Nested functions**: Function expressions and object methods are processed recursively with a temporary predecessor edge linking them to the enclosing block
66 +
67 +## TODOs
68 +- `[hoisting] EnterSSA: Expected identifier to be defined before being used` - Handles cases where hoisting causes an identifier to be used before definition (throws a Todo error for graceful bailout)
69 +
70 +## Example
71 +
72 +### Input (simple reassignment with control flow):
73 +```javascript
74 +function foo() {
75 + let y = 2;
76 + if (y > 1) {
77 + y = 1;
78 + } else {
79 + y = 2;
80 + }
81 + let x = y;
82 +}
83 +```
84 +
85 +### Before SSA (HIR):
86 +```
87 +bb0 (block):
88 + [1] $0 = 2
89 + [2] $2 = StoreLocal Let y$1 = $0
90 + [3] $7 = LoadLocal y$1
91 + [4] $8 = 1
92 + [5] $9 = Binary $7 > $8
93 + [6] If ($9) then:bb2 else:bb3 fallthrough=bb1
94 +
95 +bb2 (block):
96 + predecessor blocks: bb0
97 + [7] $3 = 1
98 + [8] $4 = StoreLocal Reassign y$1 = $3 // Same y$1 reassigned
99 + [9] Goto bb1
100 +
101 +bb3 (block):
102 + predecessor blocks: bb0
103 + [10] $5 = 2
104 + [11] $6 = StoreLocal Reassign y$1 = $5 // Same y$1 reassigned
105 + [12] Goto bb1
106 +
107 +bb1 (block):
108 + predecessor blocks: bb2 bb3
109 + [13] $10 = LoadLocal y$1 // Which y$1?
110 + [14] $12 = StoreLocal Let x$11 = $10
111 +```
112 +
113 +### After SSA:
114 +```
115 +bb0 (block):
116 + [1] $15 = 2
117 + [2] $17 = StoreLocal Let y$16 = $15 // y$16: initial definition
118 + [3] $18 = LoadLocal y$16
119 + [4] $19 = 1
120 + [5] $20 = Binary $18 > $19
121 + [6] If ($20) then:bb2 else:bb3 fallthrough=bb1
122 +
123 +bb2 (block):
124 + predecessor blocks: bb0
125 + [7] $21 = 1
126 + [8] $23 = StoreLocal Reassign y$22 = $21 // y$22: new SSA name
127 + [9] Goto bb1
128 +
129 +bb3 (block):
130 + predecessor blocks: bb0
131 + [10] $24 = 2
132 + [11] $26 = StoreLocal Reassign y$25 = $24 // y$25: new SSA name
133 + [12] Goto bb1
134 +
135 +bb1 (block):
136 + predecessor blocks: bb2 bb3
137 + y$27: phi(bb2: y$22, bb3: y$25) // PHI NODE: merges y$22 and y$25
138 + [13] $28 = LoadLocal y$27 // Uses phi result
139 + [14] $30 = StoreLocal Let x$29 = $28
140 +```
141 +
142 +### Loop Example (while loop with back-edge):
143 +```javascript
144 +function foo() {
145 + let x = 1;
146 + while (x < 10) {
147 + x = x + 1;
148 + }
149 + return x;
150 +}
151 +```
152 +
153 +### After SSA:
154 +```
155 +bb0 (block):
156 + [1] $13 = 1
157 + [2] $15 = StoreLocal Let x$14 = $13 // x$14: initial definition
158 + [3] While test=bb1 loop=bb3 fallthrough=bb2
159 +
160 +bb1 (loop):
161 + predecessor blocks: bb0 bb3
162 + x$16: phi(bb0: x$14, bb3: x$23) // PHI merges initial and loop-updated values
163 + [4] $17 = LoadLocal x$16
164 + [5] $18 = 10
165 + [6] $19 = Binary $17 < $18
166 + [7] Branch ($19) then:bb3 else:bb2
167 +
168 +bb3 (block):
169 + predecessor blocks: bb1
170 + [8] $20 = LoadLocal x$16 // Uses phi result
171 + [9] $21 = 1
172 + [10] $22 = Binary $20 + $21
173 + [11] $24 = StoreLocal Reassign x$23 = $22 // x$23: new SSA name in loop body
174 + [12] Goto(Continue) bb1
175 +
176 +bb2 (block):
177 + predecessor blocks: bb1
178 + [13] $25 = LoadLocal x$16 // Uses phi result
179 + [14] Return Explicit $25
180 +```
181 +
182 +The phi node at `bb1` (the loop header) is initially created as an "incomplete phi" when first visited because `bb3` (the loop body) hasn't been visited yet. Once `bb3` is processed and its terminal is handled, the incomplete phi is fixed by calling `fixIncompletePhis` to populate the operand from `bb3`.
compiler/packages/babel-plugin-react-compiler/docs/passes/03-eliminateRedundantPhi.md new
+90
@@ -0,0 +1,90 @@
1 +# eliminateRedundantPhi
2 +
3 +## File
4 +`src/SSA/EliminateRedundantPhi.ts`
5 +
6 +## Purpose
7 +Eliminates phi nodes whose operands are trivially the same, replacing all usages of the phi's output identifier with the single source identifier. This simplifies the HIR by removing unnecessary join points that do not actually merge distinct values.
8 +
9 +## Input Invariants
10 +- The function must be in SSA form (i.e., `enterSSA` has already run)
11 +- Blocks are in reverse postorder (guaranteed by the HIR structure)
12 +- Phi nodes exist at the start of blocks where control flow merges
13 +
14 +## Output Guarantees
15 +- All redundant phi nodes are removed from the HIR
16 +- All references to eliminated phi identifiers are rewritten to the source identifier
17 +- Non-redundant phi nodes (those merging two or more distinct values) are preserved
18 +- Nested function expressions (FunctionExpression, ObjectMethod) also have their redundant phis eliminated and contexts rewritten
19 +
20 +## Algorithm
21 +A phi node is considered redundant when:
22 +1. **All operands are the same identifier**: e.g., `x2 = phi(x1, x1, x1)` - the phi is replaced with `x1`
23 +2. **All operands are either the same identifier OR the phi's output**: e.g., `x2 = phi(x1, x2, x1, x2)` - this handles loop back-edges where the phi references itself
24 +
25 +The algorithm works as follows:
26 +1. Visit blocks in reverse postorder, building a rewrite table (`Map<Identifier, Identifier>`)
27 +2. For each phi node in a block:
28 + - First rewrite operands using any existing rewrites (to handle cascading eliminations)
29 + - Check if all operands (excluding self-references) point to the same identifier
30 + - If so, add a mapping from the phi's output to that identifier and delete the phi
31 +3. After processing phis, rewrite all instruction lvalues, operands, and terminal operands
32 +4. For nested functions, recursively call `eliminateRedundantPhi` with shared rewrites
33 +5. If the CFG has back-edges (loops) and new rewrites were added, repeat the entire process
34 +
35 +The loop termination condition `rewrites.size > size && hasBackEdge` ensures:
36 +- Without loops: completes in a single pass (reverse postorder guarantees forward propagation)
37 +- With loops: repeats until no new rewrites are found (fixpoint)
38 +
39 +## Key Data Structures
40 +- **`Phi`** (from `src/HIR/HIR.ts`): Represents a phi node with:
41 + - `place: Place` - the output identifier
42 + - `operands: Map<BlockId, Place>` - maps predecessor block IDs to source places
43 +- **`rewrites: Map<Identifier, Identifier>`**: Maps eliminated phi outputs to their replacement identifier
44 +- **`visited: Set<BlockId>`**: Tracks visited blocks to detect back-edges (loops)
45 +
46 +## Edge Cases
47 +- **Loop back-edges**: When a block has a predecessor that hasn't been visited yet (in reverse postorder), that predecessor is a back-edge. The algorithm handles self-referential phis like `x2 = phi(x1, x2)` by ignoring operands equal to the phi's output.
48 +- **Cascading eliminations**: When one phi's output is used in another phi's operands, the algorithm rewrites operands before checking redundancy, enabling transitive elimination in a single pass (for non-loop cases).
49 +- **Nested functions**: FunctionExpression and ObjectMethod values contain nested HIR that may have their own phis. The algorithm recursively processes these with a shared rewrite table, ensuring context captures are also rewritten.
50 +- **Empty phi check**: The algorithm includes an invariant check that phi operands are never empty (which would be invalid HIR).
51 +
52 +## TODOs
53 +(None found in the source code)
54 +
55 +## Example
56 +
57 +Consider this fixture from `rewrite-phis-in-lambda-capture-context.js`:
58 +
59 +```javascript
60 +function Component() {
61 + const x = 4;
62 + const get4 = () => {
63 + while (bar()) {
64 + if (baz) { bar(); }
65 + }
66 + return () => x;
67 + };
68 + return get4;
69 +}
70 +```
71 +
72 +**After SSA pass**, the inner function has redundant phis due to the loop:
73 +
74 +```
75 +bb2 (loop):
76 + predecessor blocks: bb1 bb5
77 + x$29: phi(bb1: x$21, bb5: x$30) // Loop header phi
78 + ...
79 +bb5 (block):
80 + predecessor blocks: bb6 bb4
81 + x$30: phi(bb6: x$29, bb4: x$29) // Redundant: both operands are x$29
82 + ...
83 +```
84 +
85 +**After EliminateRedundantPhi**:
86 +- `x$30 = phi(x$29, x$29)` is eliminated because both operands are `x$29`
87 +- `x$29 = phi(x$21, x$30)` becomes `x$29 = phi(x$21, x$29)` after rewriting, which is also redundant (one operand is the phi itself, the other is `x$21`)
88 +- Both phis are eliminated, and all uses of `x$29` and `x$30` are rewritten to `x$21`
89 +
90 +The result: the context capture `@context[x$29]` becomes `@context[x$21]`, correctly propagating that `x` is never modified inside the loop.
compiler/packages/babel-plugin-react-compiler/docs/passes/04-constantPropagation.md new
+110
@@ -0,0 +1,110 @@
1 +# constantPropagation
2 +
3 +## File
4 +`src/Optimization/ConstantPropagation.ts`
5 +
6 +## Purpose
7 +Applies Sparse Conditional Constant Propagation (SCCP) to fold compile-time evaluable expressions to constant values, propagate those constants through the program, and eliminate unreachable branches when conditionals have known constant values.
8 +
9 +## Input Invariants
10 +- HIR must be in SSA form (runs after `enterSSA`)
11 +- Redundant phi nodes should be eliminated (runs after `eliminateRedundantPhi`)
12 +- Consistent identifiers must be ensured (`assertConsistentIdentifiers`)
13 +- Terminal successors must exist (`assertTerminalSuccessorsExist`)
14 +
15 +## Output Guarantees
16 +- Instructions with compile-time evaluable operands are replaced with `Primitive` constants
17 +- `ComputedLoad`/`ComputedStore` with constant string/number properties are converted to `PropertyLoad`/`PropertyStore`
18 +- `LoadLocal` and `StoreLocal` propagate known constant values
19 +- `IfTerminal` with constant boolean test values are replaced with `goto` terminals
20 +- Unreachable blocks are removed and the CFG is minimized
21 +- Phi nodes with unreachable predecessor operands are pruned
22 +- Nested functions (`FunctionExpression`, `ObjectMethod`) are recursively processed
23 +
24 +## Algorithm
25 +The pass uses Sparse Conditional Constant Propagation (SCCP) with fixpoint iteration:
26 +
27 +1. **Data Structure**: A `Constants` map (`Map<IdentifierId, Constant>`) tracks known constant values (either `Primitive` or `LoadGlobal`)
28 +
29 +2. **Single Pass per Iteration**: Visits all blocks in order:
30 + - Evaluates phi nodes - if all operands have the same constant value, the phi result is constant
31 + - Evaluates instructions - replaces evaluable expressions with constants
32 + - Evaluates terminals - if an `IfTerminal` test is a constant, replaces it with a `goto`
33 +
34 +3. **Fixpoint Loop**: If any terminals changed (branch elimination):
35 + - Recomputes block ordering (`reversePostorderBlocks`)
36 + - Removes unreachable code (`removeUnreachableForUpdates`, `removeDeadDoWhileStatements`, `removeUnnecessaryTryCatch`)
37 + - Renumbers instructions (`markInstructionIds`)
38 + - Updates predecessors (`markPredecessors`)
39 + - Prunes phi operands from unreachable predecessors
40 + - Eliminates newly-redundant phis (`eliminateRedundantPhi`)
41 + - Merges consecutive blocks (`mergeConsecutiveBlocks`)
42 + - Repeats until no more changes
43 +
44 +4. **Instruction Evaluation**: Handles various instruction types:
45 + - **Primitives/LoadGlobal**: Directly constant
46 + - **BinaryExpression**: Folds arithmetic (`+`, `-`, `*`, `/`, `%`, `**`), bitwise (`|`, `&`, `^`, `<<`, `>>`, `>>>`), and comparison (`<`, `<=`, `>`, `>=`, `==`, `===`, `!=`, `!==`) operators
47 + - **UnaryExpression**: Folds `!` (boolean negation) and `-` (numeric negation)
48 + - **PostfixUpdate/PrefixUpdate**: Folds `++`/`--` on constant numbers
49 + - **PropertyLoad**: Folds `.length` on constant strings
50 + - **TemplateLiteral**: Folds template strings with constant interpolations
51 + - **ComputedLoad/ComputedStore**: Converts to property access when property is constant string/number
52 +
53 +## Key Data Structures
54 +- `Constant = Primitive | LoadGlobal` - The lattice values (no top/bottom, absence means unknown)
55 +- `Constants = Map<IdentifierId, Constant>` - Maps identifier IDs to their known constant values
56 +- Uses HIR types: `Instruction`, `Phi`, `Place`, `Primitive`, `LoadGlobal`, `InstructionValue`
57 +
58 +## Edge Cases
59 +- **Last instruction of sequence blocks**: Skipped to preserve evaluation order
60 +- **Phi nodes with back-edges**: Single-pass analysis means loop back-edges won't have constant values propagated
61 +- **Template literals with Symbol**: Not folded (would throw at runtime)
62 +- **Template literals with objects/arrays**: Not folded (custom toString behavior)
63 +- **Division results**: Computed at compile time (may produce `NaN`, `Infinity`, etc.)
64 +- **LoadGlobal in phis**: Only propagated if all operands reference the same global name
65 +- **Nested functions**: Constants from outer scope are propagated into nested function expressions
66 +
67 +## TODOs
68 +- `// TODO: handle more cases` - The default case in `evaluateInstruction` has room for additional instruction types
69 +
70 +## Example
71 +
72 +**Input:**
73 +```javascript
74 +function Component() {
75 + let a = 1;
76 +
77 + let b;
78 + if (a === 1) {
79 + b = true;
80 + } else {
81 + b = false;
82 + }
83 +
84 + let c;
85 + if (b) {
86 + c = 'hello';
87 + } else {
88 + c = null;
89 + }
90 +
91 + return c;
92 +}
93 +```
94 +
95 +**After ConstantPropagation:**
96 +- `a === 1` evaluates to `true`
97 +- The `if (a === 1)` branch is eliminated, only consequent remains
98 +- `b` is known to be `true`
99 +- `if (b)` branch is eliminated, only consequent remains
100 +- `c` is known to be `'hello'`
101 +- All intermediate blocks are merged
102 +
103 +**Output:**
104 +```javascript
105 +function Component() {
106 + return "hello";
107 +}
108 +```
109 +
110 +The pass performs iterative simplification: first iteration determines `a === 1` is `true` and eliminates that branch. The CFG is updated, phi for `b` is pruned to single operand making `b = true`. Second iteration uses `b = true` to eliminate the next branch. This continues until no more branches can be eliminated.
compiler/packages/babel-plugin-react-compiler/docs/passes/05-deadCodeElimination.md new
+109
@@ -0,0 +1,109 @@
1 +# deadCodeElimination
2 +
3 +## File
4 +`src/Optimization/DeadCodeElimination.ts`
5 +
6 +## Purpose
7 +Eliminates instructions whose values are unused, reducing generated code size. The pass performs mark-and-sweep analysis to identify and remove dead code while preserving side effects and program semantics.
8 +
9 +## Input Invariants
10 +- Must run after `InferMutationAliasingEffects` because "dead" code may still affect effect inference
11 +- HIR is in SSA form with phi nodes
12 +- Unreachable blocks are already pruned during HIR construction
13 +
14 +## Output Guarantees
15 +- All instructions with unused lvalues (that are safe to prune) are removed
16 +- Unused phi nodes are deleted
17 +- Unused context variables are removed from `fn.context`
18 +- Destructuring patterns are rewritten to remove unused bindings
19 +- `StoreLocal` instructions with unused initializers are converted to `DeclareLocal`
20 +
21 +## Algorithm
22 +Two-phase mark-and-sweep with fixed-point iteration for loops:
23 +
24 +**Phase 1: Mark (findReferencedIdentifiers)**
25 +1. Detect if function has back-edges (loops)
26 +2. Iterate blocks in reverse postorder (successors before predecessors) to visit usages before declarations
27 +3. For each block:
28 + - Mark all terminal operands as referenced
29 + - Process instructions in reverse order:
30 + - If lvalue is used OR instruction is not pruneable, mark the lvalue and all operands as referenced
31 + - Special case for `StoreLocal`: only mark initializer if the SSA lvalue is actually read
32 + - Mark phi operands if the phi result is used
33 +4. If loops exist and new identifiers were marked, repeat until fixed point
34 +
35 +**Phase 2: Sweep**
36 +1. Remove unused phi nodes from each block
37 +2. Remove instructions with unused lvalues using `retainWhere`
38 +3. Rewrite retained instructions:
39 + - **Array destructuring**: Replace unused elements with holes, truncate trailing holes
40 + - **Object destructuring**: Remove unused properties (only if rest element is unused or absent)
41 + - **StoreLocal**: Convert to `DeclareLocal` if initializer value is never read
42 +4. Remove unused context variables
43 +
44 +## Key Data Structures
45 +- **State class**: Tracks referenced identifiers
46 + - `identifiers: Set<IdentifierId>` - SSA-specific usages
47 + - `named: Set<string>` - Named variable usages (any version)
48 + - `isIdOrNameUsed()` - Checks if identifier or any version of named variable is used
49 + - `isIdUsed()` - Checks if specific SSA id is used
50 +- **hasBackEdge/findBlocksWithBackEdges**: Detect loops requiring fixed-point iteration
51 +
52 +## Edge Cases
53 +- **Preserved even if unused:**
54 + - `debugger` statements (to not break debugging workflows)
55 + - Call expressions and method calls (may have side effects)
56 + - Await expressions
57 + - Store operations (ComputedStore, PropertyStore, StoreGlobal)
58 + - Delete operations (ComputedDelete, PropertyDelete)
59 + - Iterator operations (GetIterator, IteratorNext, NextPropertyOf)
60 + - Context operations (LoadContext, DeclareContext, StoreContext)
61 + - Memoization markers (StartMemoize, FinishMemoize)
62 +
63 +- **SSR mode special case:**
64 + - In SSR mode, unused `useState`, `useReducer`, and `useRef` hooks can be removed
65 +
66 +- **Object destructuring with rest:**
67 + - Cannot remove unused properties if rest element is used (would change rest's value)
68 +
69 +- **Block value instructions:**
70 + - Last instruction of value blocks (not 'block' kind) is never pruned as it's the block's value
71 +
72 +## TODOs
73 +- "TODO: we could be more precise and make this conditional on whether any arguments are actually modified" (for mutating instructions)
74 +
75 +## Example
76 +
77 +**Input:**
78 +```javascript
79 +function Component(props) {
80 + const _ = 42;
81 + return props.value;
82 +}
83 +```
84 +
85 +**After DeadCodeElimination:**
86 +The `const _ = 42` assignment is removed since `_` is never used:
87 +```javascript
88 +function Component(props) {
89 + return props.value;
90 +}
91 +```
92 +
93 +**Array destructuring example:**
94 +
95 +Input:
96 +```javascript
97 +function foo(props) {
98 + const [x, unused, y] = props.a;
99 + return x + y;
100 +}
101 +```
102 +
103 +Output (middle element becomes a hole):
104 +```javascript
105 +function foo(props) {
106 + const [x, , y] = props.a;
107 + return x + y;
108 +}
109 +```
compiler/packages/babel-plugin-react-compiler/docs/passes/06-inferTypes.md new
+127
@@ -0,0 +1,127 @@
1 +# inferTypes
2 +
3 +## File
4 +`src/TypeInference/InferTypes.ts`
5 +
6 +## Purpose
7 +Infers types for all identifiers in the HIR by generating type equations and solving them using unification. This pass annotates identifiers with concrete types (Primitive, Object, Function) based on the operations performed on them and the types of globals/hooks they interact with.
8 +
9 +## Input Invariants
10 +- The HIR must be in SSA form (the pass runs after `enterSSA` and `eliminateRedundantPhi`)
11 +- Constant propagation has already run
12 +- Global declarations and hook shapes are available via the Environment
13 +
14 +## Output Guarantees
15 +- All identifier types are resolved from type variables (`Type`) to concrete types where possible
16 +- Phi nodes have their operand types unified to produce a single result type
17 +- Function return types are inferred from the unified types of all return statements
18 +- Property accesses on known objects/hooks resolve to the declared property types
19 +- Component props parameters are typed as `TObject<BuiltInProps>`
20 +- Component ref parameters are typed as `TObject<BuiltInUseRefId>`
21 +
22 +## Algorithm
23 +The pass uses a classic constraint-based type inference approach with three phases:
24 +
25 +1. **Constraint Generation (`generate`)**: Traverses all instructions and generates type equations:
26 + - Primitives, literals, unary/binary operations -> `Primitive` type
27 + - Hook/function calls -> Function type with fresh return type variable
28 + - Property loads -> `Property` type that defers to object shape lookup
29 + - Destructuring -> Property types for each extracted element
30 + - Phi nodes -> `Phi` type with all operand types as candidates
31 + - JSX -> `Object<BuiltInJsx>`
32 + - Arrays -> `Object<BuiltInArray>`
33 + - Objects -> `Object<BuiltInObject>`
34 +
35 +2. **Unification (`Unifier.unify`)**: Solves constraints by unifying type equations:
36 + - Type variables are bound to concrete types via substitution
37 + - Property types are resolved by looking up the object's shape
38 + - Phi types are resolved by finding a common type among operands (or falling back to `Phi` if incompatible)
39 + - Function types are unified by unifying their return types
40 + - Occurs check prevents infinite types (cycles in type references)
41 +
42 +3. **Application (`apply`)**: Applies the computed substitutions to all identifiers in the HIR, replacing type variables with their resolved types.
43 +
44 +## Key Data Structures
45 +- **TypeVar** (`kind: 'Type'`): A type variable with a unique TypeId, used for unknowns
46 +- **Unifier**: Maintains a substitution map from TypeId to Type, with methods for unification and cycle detection
47 +- **TypeEquation**: A pair of types that should be equal, used as constraints
48 +- **PhiType** (`kind: 'Phi'`): Represents the join of multiple types from control flow merge points
49 +- **PropType** (`kind: 'Property'`): Deferred property lookup that resolves based on object shape
50 +- **FunctionType** (`kind: 'Function'`): Callable type with optional shapeId and return type
51 +- **ObjectType** (`kind: 'Object'`): Object with optional shapeId for shape lookup
52 +
53 +## Edge Cases
54 +
55 +### Phi Type Resolution
56 +When phi operands have incompatible types, the pass attempts to find a union:
57 +- `Union(Primitive | MixedReadonly) = MixedReadonly`
58 +- `Union(Array | MixedReadonly) = Array`
59 +- If no union is possible, the type remains as `Phi`
60 +
61 +### Ref-like Name Inference
62 +When `enableTreatRefLikeIdentifiersAsRefs` is enabled, property access on variables matching the pattern `/^(?:[a-zA-Z$_][a-zA-Z$_0-9]*)Ref$|^ref$/` with property name `current` infers:
63 +- Object type as `TObject<BuiltInUseRefId>`
64 +- Property type as `TObject<BuiltInRefValue>`
65 +
66 +### Cycle Detection
67 +The `occursCheck` method prevents infinite types by detecting when a type variable appears in its own substitution. When a cycle is detected, `tryResolveType` removes the cyclic reference from Phi operands.
68 +
69 +### Context Variables
70 +- `DeclareContext` and `LoadContext` generate no type equations (intentionally untyped)
71 +- `StoreContext` with `Const` kind does propagate the rvalue type to enable ref inference through context variables
72 +
73 +### Event Handler Inference
74 +When `enableInferEventHandlers` is enabled, JSX props starting with "on" (e.g., `onClick`) on built-in DOM elements (excluding web components with hyphens) are inferred as `Function<BuiltInEventHandlerId>`.
75 +
76 +## TODOs
77 +1. **Hook vs Function type ambiguity**:
78 + > "TODO: callee could be a hook or a function, so this type equation isn't correct. We should change Hook to a subtype of Function or change unifier logic."
79 +
80 +2. **PropertyStore rvalue inference**:
81 + > "TODO: consider using the rvalue type here" - Currently uses a dummy type for PropertyStore to avoid inferring rvalue types from lvalue assignments.
82 +
83 +## Example
84 +
85 +**Input (infer-phi-primitive.js):**
86 +```javascript
87 +function foo(a, b) {
88 + let x;
89 + if (a) {
90 + x = 1;
91 + } else {
92 + x = 2;
93 + }
94 + let y = x;
95 + return y;
96 +}
97 +```
98 +
99 +**Before InferTypes (SSA form):**
100 +```
101 +<unknown> x$26: phi(bb2: <unknown> x$21, bb3: <unknown> x$24)
102 +[10] <unknown> $27 = LoadLocal <unknown> x$26
103 +[11] <unknown> $29 = StoreLocal Let <unknown> y$28 = <unknown> $27
104 +```
105 +
106 +**After InferTypes:**
107 +```
108 +<unknown> x$26:TPrimitive: phi(bb2: <unknown> x$21:TPrimitive, bb3: <unknown> x$24:TPrimitive)
109 +[10] <unknown> $27:TPrimitive = LoadLocal <unknown> x$26:TPrimitive
110 +[11] <unknown> $29:TPrimitive = StoreLocal Let <unknown> y$28:TPrimitive = <unknown> $27:TPrimitive
111 +```
112 +
113 +The pass infers that:
114 +- Literals `1` and `2` are `TPrimitive`
115 +- The phi of two primitives is `TPrimitive`
116 +- Variables `x` and `y` are `TPrimitive`
117 +- The function return type is `TPrimitive`
118 +
119 +**Hook type inference example (useState):**
120 +```javascript
121 +const [x, setX] = useState(initialValue);
122 +```
123 +
124 +After InferTypes:
125 +- `useState` -> `TFunction<BuiltInUseState>:TObject<BuiltInUseState>`
126 +- Return value `$27` -> `TObject<BuiltInUseState>`
127 +- Destructured `setX` -> `TFunction<BuiltInSetState>:TPrimitive`
compiler/packages/babel-plugin-react-compiler/docs/passes/07-analyseFunctions.md new
+84
@@ -0,0 +1,84 @@
1 +# analyseFunctions
2 +
3 +## File
4 +`src/Inference/AnalyseFunctions.ts`
5 +
6 +## Purpose
7 +Recursively analyzes all nested function expressions and object methods in a function to infer their aliasing effect signatures, which describe how the function affects its captured variables when invoked.
8 +
9 +## Input Invariants
10 +- The HIR has been through SSA conversion and type inference
11 +- FunctionExpression and ObjectMethod instructions have an empty `aliasingEffects` array (`@aliasingEffects=[]`)
12 +- Context variables (captured variables from outer scope) exist on `fn.context` but do not have their effect populated
13 +
14 +## Output Guarantees
15 +- Every FunctionExpression and ObjectMethod has its `aliasingEffects` array populated with the effects the function performs when called (mutations, captures, aliasing to return value, etc.)
16 +- Each context variable's `effect` property is set to either `Effect.Capture` (if the variable is captured or mutated by the inner function) or `Effect.Read` (if only read)
17 +- Context variable mutable ranges are reset to `{start: 0, end: 0}` and scopes are set to `null` to prepare for the outer function's subsequent `inferMutationAliasingRanges` pass
18 +
19 +## Algorithm
20 +1. **Recursive traversal**: Iterates through all blocks and instructions looking for `FunctionExpression` or `ObjectMethod` instructions
21 +2. **Depth-first processing**: For each function expression found, calls `lowerWithMutationAliasing()` which:
22 + - Recursively calls `analyseFunctions()` on the inner function (handles nested functions)
23 + - Runs `inferMutationAliasingEffects()` on the inner function to determine effects
24 + - Runs `deadCodeElimination()` to clean up
25 + - Runs `inferMutationAliasingRanges()` to compute mutable ranges and extract externally-visible effects
26 + - Runs `rewriteInstructionKindsBasedOnReassignment()` and `inferReactiveScopeVariables()`
27 + - Stores the computed effects in `fn.aliasingEffects`
28 +3. **Context variable effect classification**: Scans the computed effects to determine which context variables are captured/mutated vs only read:
29 + - Effects like `Capture`, `Alias`, `Assign`, `MaybeAlias`, `CreateFrom` mark the source as captured
30 + - Mutation effects (`Mutate`, `MutateTransitive`, etc.) mark the target as captured
31 + - Sets `operand.effect = Effect.Capture` or `Effect.Read` accordingly
32 +4. **Range reset**: Resets mutable ranges and scopes on context variables to prepare for outer function analysis
33 +
34 +## Key Data Structures
35 +- **HIRFunction.aliasingEffects**: Array of `AliasingEffect` storing the externally-visible behavior of a function when called
36 +- **Place.effect**: Effect enum value (`Capture` or `Read`) describing how a context variable is used
37 +- **AliasingEffect**: Union type describing data flow (Capture, Alias, Assign, etc.) and mutations (Mutate, MutateTransitive, etc.)
38 +- **FunctionExpression/ObjectMethod.loweredFunc.func**: The inner HIRFunction to analyze
39 +
40 +## Edge Cases
41 +- **Nested functions**: Handled via recursive call to `analyseFunctions()` before processing the current function - innermost functions are analyzed first
42 +- **ObjectMethod**: Treated identically to FunctionExpression
43 +- **Apply effects invariant**: The pass asserts that no `Apply` effects remain in the function's signature - these should have been resolved to more precise effects by `inferMutationAliasingRanges()`
44 +- **Conditional mutations**: Effects like `MutateTransitiveConditionally` are tracked - a function that conditionally mutates a captured variable will have that effect in its signature
45 +- **Immutable captures**: `ImmutableCapture`, `Freeze`, `Create`, `Impure`, `Render` effects do not contribute to marking context variables as `Capture`
46 +
47 +## TODOs
48 +- No TODO comments in the pass itself
49 +
50 +## Example
51 +Consider a function that captures and conditionally mutates a variable:
52 +
53 +```javascript
54 +function useHook(a, b) {
55 + let z = {a};
56 + let y = b;
57 + let x = function () {
58 + if (y) {
59 + maybeMutate(z); // Unknown function, may mutate z
60 + }
61 + };
62 + return x;
63 +}
64 +```
65 +
66 +**Before AnalyseFunctions:**
67 +```
68 +Function @context[y$28, z$25] @aliasingEffects=[]
69 +```
70 +
71 +**After AnalyseFunctions:**
72 +```
73 +Function @context[read y$28, capture z$25] @aliasingEffects=[
74 + MutateTransitiveConditionally z$25,
75 + Create $14 = primitive
76 +]
77 +```
78 +
79 +The pass infers:
80 +- `y` is only read (used in the condition)
81 +- `z` is captured into the function and conditionally mutated transitively (because `maybeMutate()` is unknown)
82 +- The inner function's signature includes `MutateTransitiveConditionally z$25` to indicate this potential mutation
83 +
84 +This signature is then used by `InferMutationAliasingEffects` on the outer function to understand that creating this function captures `z`, and calling the function may mutate `z`.
compiler/packages/babel-plugin-react-compiler/docs/passes/08-inferMutationAliasingEffects.md new
+144
@@ -0,0 +1,144 @@
1 +# inferMutationAliasingEffects
2 +
3 +## File
4 +`src/Inference/InferMutationAliasingEffects.ts`
5 +
6 +## Purpose
7 +Infers the mutation and aliasing effects for all instructions and terminals in the HIR, making the effects of built-in instructions/functions as well as user-defined functions explicit. These effects form the basis for subsequent analysis to determine the mutable range of each value in the program and for validation against invalid code patterns like mutating frozen values.
8 +
9 +## Input Invariants
10 +- HIR must be in SSA form (run after SSA pass)
11 +- Types must be inferred (run after InferTypes pass)
12 +- Functions must be analyzed (run after AnalyseFunctions pass) - this provides `aliasingEffects` on FunctionExpressions
13 +- Each instruction must have an lvalue (destination place)
14 +
15 +## Output Guarantees
16 +- Every instruction has an `effects` array (or null if no effects) containing `AliasingEffect` objects
17 +- Terminals that affect data flow (return, try/catch) have their `effects` populated
18 +- Each instruction's lvalue is guaranteed to be defined in the inference state after visiting
19 +- Effects describe: creation of values, data flow (Assign, Alias, Capture), mutations (Mutate, MutateTransitive), freezing, and errors (MutateFrozen, MutateGlobal, Impure)
20 +
21 +## Algorithm
22 +The pass uses abstract interpretation with the following key phases:
23 +
24 +1. **Initialization**:
25 + - Create initial `InferenceState` mapping identifiers to abstract values
26 + - Initialize context variables as `ValueKind.Context`
27 + - Initialize parameters as `ValueKind.Frozen` (for top-level components/hooks) or `ValueKind.Mutable` (for function expressions)
28 +
29 +2. **Two-Phase Effect Processing**:
30 + - **Phase 1 - Signature Computation**: For each instruction, compute a "candidate signature" based purely on instruction semantics and types (cached per instruction via `computeSignatureForInstruction`)
31 + - **Phase 2 - Effect Application**: Apply the signature to the current abstract state via `applySignature`, which refines effects based on the actual runtime kinds of values
32 +
33 +3. **Fixed-Point Iteration**:
34 + - Process blocks in a worklist, queuing successors after each block
35 + - Merge states at control flow join points using lattice operations
36 + - Iterate until no changes occur (max 100 iterations as safety limit)
37 + - Phi nodes are handled by unioning the abstract values from all predecessors
38 +
39 +4. **Effect Refinement** (in `applyEffect`):
40 + - `MutateConditionally` effects are dropped if value is not mutable
41 + - `Capture` effects are downgraded to `ImmutableCapture` if source is frozen
42 + - `Mutate` on frozen values becomes `MutateFrozen` error
43 + - `Assign` from primitives/globals creates new values rather than aliasing
44 +
45 +## Key Data Structures
46 +
47 +### InferenceState
48 +Maintains two maps:
49 +- `#values: Map<InstructionValue, AbstractValue>` - Maps allocation sites to their abstract kind
50 +- `#variables: Map<IdentifierId, Set<InstructionValue>>` - Maps identifiers to the set of values they may point to (set to handle phi joins)
51 +
52 +### AbstractValue
53 +```typescript
54 +type AbstractValue = {
55 + kind: ValueKind;
56 + reason: ReadonlySet<ValueReason>;
57 +};
58 +```
59 +
60 +### ValueKind (lattice)
61 +```
62 +MaybeFrozen <- top (unknown if frozen or mutable)
63 + |
64 + Frozen <- immutable, cannot be mutated
65 + Mutable <- can be mutated locally
66 + Context <- mutable box (context variables)
67 + |
68 + Global <- global value
69 + Primitive <- copy-on-write semantics
70 +```
71 +
72 +The `mergeValueKinds` function implements the lattice join:
73 +- `Frozen | Mutable -> MaybeFrozen`
74 +- `Context | Mutable -> Context`
75 +- `Context | Frozen -> MaybeFrozen`
76 +
77 +### AliasingEffect Types
78 +Key effect kinds handled:
79 +- **Create**: Creates a new value at a place
80 +- **Assign**: Direct assignment (pointer copy)
81 +- **Alias**: Mutation of destination implies mutation of source
82 +- **Capture**: Information flow (MutateTransitive propagates through)
83 +- **MaybeAlias**: Possible aliasing for unknown function returns
84 +- **Mutate/MutateTransitive**: Direct/transitive mutation
85 +- **MutateConditionally/MutateTransitiveConditionally**: Conditional versions
86 +- **Freeze**: Marks value as immutable
87 +- **Apply**: Function call with complex data flow
88 +
89 +## Edge Cases
90 +
91 +1. **Spread Destructuring from Props**: The `findNonMutatedDestructureSpreads` pre-pass identifies spread patterns from frozen values that are never mutated, allowing them to be treated as frozen.
92 +
93 +2. **Hoisted Context Declarations**: Special handling for variables declared with hoisting (`HoistedConst`, `HoistedFunction`, `HoistedLet`) to detect access before declaration.
94 +
95 +3. **Try-Catch Aliasing**: When a `maybe-throw` terminal is reached, call return values are aliased into the catch binding since exceptions can throw return values.
96 +
97 +4. **Function Expressions**: Functions are considered mutable only if they have mutable captures or tracked side effects (MutateFrozen, MutateGlobal, Impure).
98 +
99 +5. **Iterator Mutation**: Non-builtin iterators may alias their collection and mutation of the iterator is conditional.
100 +
101 +6. **Array.push and Similar**: Uses legacy signature system with `Store` effect on receiver and `Capture` of arguments.
102 +
103 +## TODOs
104 +- `// TODO: using InstructionValue as a bit of a hack, but it's pragmatic` - context variable initialization
105 +- `// TODO: call applyEffect() instead` - try-catch aliasing
106 +- `// TODO: make sure we're also validating against global mutations somewhere` - global mutation validation for effects/event handlers
107 +- `// TODO; include "render" here?` - whether to track Render effects in function hasTrackedSideEffects
108 +- `// TODO: consider using persistent data structures to make clone cheaper` - performance optimization for state cloning
109 +- `// TODO check this` and `// TODO: what kind here???` - DeclareLocal value kinds
110 +
111 +## Example
112 +
113 +For the code:
114 +```javascript
115 +const arr = [];
116 +arr.push({});
117 +arr.push(x, y);
118 +```
119 +
120 +After `InferMutationAliasingEffects`, the effects are:
121 +
122 +```
123 +[10] $39 = Array []
124 + Create $39 = mutable // Array literal creates mutable value
125 +
126 +[11] $41 = StoreLocal arr$40 = $39
127 + Assign arr$40 = $39 // arr points to the array value
128 + Assign $41 = $39
129 +
130 +[15] $45 = MethodCall $42.push($44)
131 + Apply $45 = $42.$43($44) // Records the call
132 + Mutate $42 // push mutates the array
133 + Capture $42 <- $44 // {} is captured into array
134 + Create $45 = primitive // push returns number (length)
135 +
136 +[20] $50 = MethodCall $46.push($48, $49)
137 + Apply $50 = $46.$47($48, $49)
138 + Mutate $46 // push mutates the array
139 + Capture $46 <- $48 // x captured into array
140 + Capture $46 <- $49 // y captured into array
141 + Create $50 = primitive
142 +```
143 +
144 +The key insight is that `Mutate` effects extend the mutable range of the array, and `Capture` effects record data flow so that if the array is later frozen (e.g., returned from a component), the captured values are also considered frozen for validation purposes.
compiler/packages/babel-plugin-react-compiler/docs/passes/09-inferMutationAliasingRanges.md new
+149
@@ -0,0 +1,149 @@
1 +# inferMutationAliasingRanges
2 +
3 +## File
4 +`src/Inference/InferMutationAliasingRanges.ts`
5 +
6 +## Purpose
7 +This pass builds an abstract model of the heap and interprets the effects of the given function to determine: (1) the mutable ranges of all identifiers, (2) the externally-visible effects of the function (mutations of params/context-vars, aliasing relationships), and (3) the legacy `Effect` annotation for each Place.
8 +
9 +## Input Invariants
10 +- InferMutationAliasingEffects must have already run, populating `instr.effects` on each instruction with aliasing/mutation effects
11 +- SSA form must be established (identifiers are in SSA)
12 +- Type inference has been run (InferTypes)
13 +- Functions have been analyzed (AnalyseFunctions)
14 +- Dead code elimination has been performed
15 +
16 +## Output Guarantees
17 +- Every identifier has a populated `mutableRange` (start:end instruction IDs)
18 +- Every Place has a legacy `Effect` annotation (Read, Capture, Store, Freeze, etc.)
19 +- The function's `aliasingEffects` array is populated with externally-visible effects (mutations of params/context-vars, aliasing between params/context-vars/return)
20 +- Validation errors are collected for invalid effects like `MutateFrozen` or `MutateGlobal`
21 +
22 +## Algorithm
23 +The pass operates in three main phases:
24 +
25 +**Part 1: Build Data Flow Graph and Infer Mutable Ranges**
26 +1. Creates an `AliasingState` which maintains a `Node` for each identifier
27 +2. Iterates through all blocks and instructions, processing effects in program order
28 +3. For each effect:
29 + - `Create`/`CreateFunction`: Creates a new node in the graph
30 + - `CreateFrom`/`Assign`/`Alias`: Adds alias edges between nodes (with ordering index)
31 + - `MaybeAlias`: Adds conditional alias edges
32 + - `Capture`: Adds capture edges (for transitive mutations)
33 + - `Mutate*`: Queues mutations for later processing
34 + - `Render`: Queues render effects for later processing
35 +4. Phi node operands are connected once their predecessor blocks have been visited
36 +5. After the graph is built, mutations are processed:
37 + - Mutations propagate both forward (via edges) and backward (via aliases/captures)
38 + - Each mutation extends the `mutableRange.end` of affected identifiers
39 + - Transitive mutations also traverse capture edges backward
40 + - `MaybeAlias` edges downgrade mutations to `Conditional`
41 +6. Render effects are processed to mark values as rendered
42 +
43 +**Part 2: Populate Legacy Per-Place Effects**
44 +- Sets legacy effects on lvalues and operands based on instruction effects and mutable ranges
45 +- Fixes up mutable range start values for identifiers that are mutated after creation
46 +
47 +**Part 3: Infer Externally-Visible Function Effects**
48 +- Creates a `Create` effect for the return value
49 +- Simulates transitive mutations of each param/context-var/return to detect capture relationships
50 +- Produces `Alias`/`Capture` effects showing data flow between params/context-vars/return
51 +
52 +## Key Data Structures
53 +
54 +### `AliasingState`
55 +The main state class maintaining the data flow graph:
56 +- `nodes: Map<Identifier, Node>` - Maps identifiers to their graph nodes
57 +
58 +### `Node`
59 +Represents an identifier in the data flow graph:
60 +```typescript
61 +type Node = {
62 + id: Identifier;
63 + createdFrom: Map<Identifier, number>; // CreateFrom edges (source -> index)
64 + captures: Map<Identifier, number>; // Capture edges (source -> index)
65 + aliases: Map<Identifier, number>; // Alias/Assign edges (source -> index)
66 + maybeAliases: Map<Identifier, number>; // MaybeAlias edges (source -> index)
67 + edges: Array<{index, node, kind}>; // Forward edges to other nodes
68 + transitive: {kind: MutationKind; loc} | null; // Transitive mutation info
69 + local: {kind: MutationKind; loc} | null; // Local mutation info
70 + lastMutated: number; // Index of last mutation affecting this node
71 + mutationReason: MutationReason | null; // Reason for mutation
72 + value: {kind: 'Object'} | {kind: 'Phi'} | {kind: 'Function'; function: HIRFunction};
73 + render: Place | null; // Render context if used in JSX
74 +};
75 +```
76 +
77 +### `MutationKind`
78 +Enum describing mutation certainty:
79 +```typescript
80 +enum MutationKind {
81 + None = 0,
82 + Conditional = 1, // May mutate (e.g., via MaybeAlias or MutateConditionally)
83 + Definite = 2, // Definitely mutates
84 +}
85 +```
86 +
87 +## Edge Cases
88 +
89 +### Phi Nodes
90 +- Phi nodes are created as special `{kind: 'Phi'}` nodes
91 +- Phi operands from predecessor blocks are processed with pending edges until the predecessor is visited
92 +- When traversing "forwards" through edges and encountering a phi, backward traversal is stopped (prevents mutation from one phi input affecting other inputs)
93 +
94 +### Transitive vs Local Mutations
95 +- Local mutations (`Mutate`) only affect alias/assign edges backward
96 +- Transitive mutations (`MutateTransitive`) also affect capture edges backward
97 +- Both affect all forward edges
98 +
99 +### MaybeAlias
100 +- Mutations through MaybeAlias edges are downgraded to `Conditional`
101 +- This prevents false positive errors when we cannot be certain about aliasing
102 +
103 +### Function Values
104 +- Functions are tracked specially as `{kind: 'Function'}` nodes
105 +- When a function is mutated (transitively), errors from the function body are propagated
106 +- This handles cases where mutating a captured value in a function affects render safety
107 +
108 +### Render Effect Propagation
109 +- Render effects traverse backward through alias/capture/createFrom edges
110 +- Functions that have not been mutated are skipped during render traversal (except for JSX-returning functions)
111 +- Ref types (`isUseRefType`) stop render traversal
112 +
113 +## TODOs
114 +1. Assign effects should have an invariant that the node is not initialized yet. Currently `InferFunctionExpressionAliasingEffectSignatures` infers Assign effects that should be Alias, causing reinitialization.
115 +
116 +2. Phi place effects are not properly set today.
117 +
118 +3. Phi mutable range start calculation is imprecise - currently just sets it to the instruction before the block rather than computing the exact start.
119 +
120 +## Example
121 +
122 +Consider the following code:
123 +```javascript
124 +function foo() {
125 + let a = {}; // Create a (instruction 1)
126 + let b = {}; // Create b (instruction 3)
127 + a = b; // Assign a <- b (instruction 8)
128 + mutate(a, b); // MutateTransitiveConditionally a, b (instruction 16)
129 + return a;
130 +}
131 +```
132 +
133 +The pass builds a graph:
134 +1. Creates node for `{}` at instruction 1 (initially assigned to `a`)
135 +2. Creates node for `{}` at instruction 3 (initially assigned to `b`)
136 +3. At instruction 8, creates alias edge: `b -> a` with index 8
137 +4. At instruction 16, mutations are queued for `a` and `b`
138 +
139 +When processing the mutation of `a` at instruction 16:
140 +- Extends `a`'s mutableRange.end to 17
141 +- Traverses backward through alias edge to `b`, extends `b`'s mutableRange.end to 17
142 +- Since `a = b`, both objects must be considered mutable until instruction 17
143 +
144 +The output shows identifiers with range annotations like `$25[3:17]` meaning:
145 +- `$25` is the identifier
146 +- `3` is the instruction where it was created
147 +- `17` is the instruction after which it is no longer mutated
148 +
149 +For aliased values, the ranges are unified - all values that could be affected by a mutation have their ranges extended to include that mutation point.
compiler/packages/babel-plugin-react-compiler/docs/passes/10-inferReactivePlaces.md new
+169
@@ -0,0 +1,169 @@
1 +# inferReactivePlaces
2 +
3 +## File
4 +`src/Inference/InferReactivePlaces.ts`
5 +
6 +## Purpose
7 +Determines which `Place`s (identifiers and temporaries) in the HIR are **reactive** - meaning they may *semantically* change over the course of the component or hook's lifetime. This information is critical for memoization: reactive places form the dependencies that, when changed, should invalidate cached values.
8 +
9 +A place is reactive if it derives from any source of reactivity:
10 +1. **Props** - Component parameters may change between renders
11 +2. **Hooks** - Hooks can access state or context which can change
12 +3. **`use` operator** - Can access context which may change
13 +4. **Mutation with reactive operands** - Values mutated in instructions that have reactive operands become reactive themselves
14 +5. **Conditional assignment based on reactive control flow** - Values assigned in branches controlled by reactive conditions become reactive
15 +
16 +## Input Invariants
17 +- HIR is in SSA form with phi nodes at join points
18 +- `inferMutationAliasingEffects` and `inferMutationAliasingRanges` have run, establishing:
19 + - Effect annotations on operands (Effect.Capture, Effect.Store, Effect.Mutate, etc.)
20 + - Mutable ranges on identifiers
21 + - Aliasing relationships captured by `findDisjointMutableValues`
22 +- All operands have known effects (asserts on `Effect.Unknown`)
23 +
24 +## Output Guarantees
25 +- Every reactive Place has `place.reactive = true`
26 +- Reactivity is transitively complete (derived from reactive → reactive)
27 +- All identifiers in a mutable alias group share reactivity
28 +- Reactivity is propagated to operands used within nested function expressions
29 +
30 +## Algorithm
31 +The algorithm uses **fixpoint iteration** to propagate reactivity forward through the control-flow graph:
32 +
33 +### Initialization
34 +1. Create a `ReactivityMap` backed by disjoint sets of mutably-aliased identifiers
35 +2. Mark all function parameters as reactive (props are reactive by definition)
36 +3. Create a `ControlDominators` helper to identify blocks controlled by reactive conditions
37 +
38 +### Fixpoint Loop
39 +Iterate until no changes occur:
40 +
41 +For each block:
42 +1. **Phi Nodes**: Mark phi nodes reactive if:
43 + - Any operand is reactive, OR
44 + - Any predecessor block is controlled by a reactive condition (control-flow dependency)
45 +
46 +2. **Instructions**: For each instruction:
47 + - Track stable identifier sources (for hooks like `useRef`, `useState` dispatch)
48 + - Check if any operand is reactive
49 + - Hook calls and `use` operator are sources of reactivity
50 + - If instruction has reactive input:
51 + - Mark lvalues reactive (unless they are known-stable like `setState` functions)
52 + - If instruction has reactive input OR is in reactive-controlled block:
53 + - Mark mutable operands (Capture, Store, Mutate effects) as reactive
54 +
55 +3. **Terminals**: Check terminal operands for reactivity
56 +
57 +### Post-processing
58 +Propagate reactivity to inner functions (nested `FunctionExpression` and `ObjectMethod`).
59 +
60 +## Key Data Structures
61 +
62 +### ReactivityMap
63 +```typescript
64 +class ReactivityMap {
65 + hasChanges: boolean = false; // Tracks if fixpoint changed
66 + reactive: Set<IdentifierId> = new Set(); // Set of reactive identifiers
67 + aliasedIdentifiers: DisjointSet<Identifier>; // Mutable alias groups
68 +}
69 +```
70 +- Uses disjoint sets so that when one identifier in an alias group becomes reactive, they all are effectively reactive
71 +- `isReactive(place)` checks and marks `place.reactive = true` as a side effect
72 +- `snapshot()` resets change tracking and returns whether changes occurred
73 +
74 +### StableSidemap
75 +```typescript
76 +class StableSidemap {
77 + map: Map<IdentifierId, {isStable: boolean}> = new Map();
78 +}
79 +```
80 +Tracks sources of stability (e.g., `useState()[1]` dispatch function). Forward data-flow analysis that:
81 +- Records hook calls that return stable types
82 +- Propagates stability through PropertyLoad and Destructure from stable containers
83 +- Propagates through LoadLocal and StoreLocal
84 +
85 +### ControlDominators
86 +Uses post-dominator frontier analysis to determine which blocks are controlled by reactive branch conditions.
87 +
88 +## Edge Cases
89 +
90 +### Backward Reactivity Propagation via Mutable Aliasing
91 +```javascript
92 +const x = [];
93 +const z = [x];
94 +x.push(props.input);
95 +return <div>{z}</div>;
96 +```
97 +Here `z` aliases `x` which is later mutated with reactive data. The disjoint set ensures `z` becomes reactive even though the mutation happens after its creation.
98 +
99 +### Stable Types Are Not Reactive
100 +```javascript
101 +const [state, setState] = useState();
102 +// setState is stable - not marked reactive despite coming from reactive hook
103 +```
104 +The `StableSidemap` tracks these and skips marking them reactive.
105 +
106 +### Ternary with Stable Values Still Reactive
107 +```javascript
108 +props.cond ? setState1 : setState2
109 +```
110 +Even though both branches are stable types, the result depends on reactive control flow, so it cannot be marked non-reactive just based on type.
111 +
112 +### Phi Nodes with Reactive Predecessors
113 +When a phi's predecessor block is controlled by a reactive condition, the phi becomes reactive even if its operands are all non-reactive constants.
114 +
115 +## TODOs
116 +No explicit TODO comments are present in the source file. However, comments note:
117 +
118 +- **ComputedLoads not handled for stability**: Only PropertyLoad propagates stability from containers, not ComputedLoad. The comment notes this is safe because stable containers have differently-typed elements, but ComputedLoad handling could be added.
119 +
120 +## Example
121 +
122 +### Fixture: `reactive-dependency-fixpoint.js`
123 +
124 +**Input:**
125 +```javascript
126 +function Component(props) {
127 + let x = 0;
128 + let y = 0;
129 + while (x === 0) {
130 + x = y;
131 + y = props.value;
132 + }
133 + return [x];
134 +}
135 +```
136 +
137 +**Before InferReactivePlaces:**
138 +```
139 +bb1 (loop):
140 + store x$26:TPhi:TPhi: phi(bb0: read x$21:TPrimitive, bb3: read x$32:TPhi)
141 + store y$30:TPhi:TPhi: phi(bb0: read y$24:TPrimitive, bb3: read y$37)
142 + ...
143 +bb3 (block):
144 + [12] mutate? $35 = LoadLocal read props$19
145 + [13] mutate? $36 = PropertyLoad read $35.value
146 + [14] mutate? $38 = StoreLocal Reassign mutate? y$37 = read $36
147 +```
148 +
149 +**After InferReactivePlaces:**
150 +```
151 +bb1 (loop):
152 + store x$26:TPhi{reactive}:TPhi: phi(bb0: read x$21:TPrimitive, bb3: read x$32:TPhi{reactive})
153 + store y$30:TPhi{reactive}:TPhi: phi(bb0: read y$24:TPrimitive, bb3: read y$37{reactive})
154 + [6] mutate? $27:TPhi{reactive} = LoadLocal read x$26:TPhi{reactive}
155 + ...
156 +bb3 (block):
157 + [12] mutate? $35{reactive} = LoadLocal read props$19{reactive}
158 + [13] mutate? $36{reactive} = PropertyLoad read $35{reactive}.value
159 + [14] mutate? $38{reactive} = StoreLocal Reassign mutate? y$37{reactive} = read $36{reactive}
160 +```
161 +
162 +**Key observations:**
163 +- `props$19` is marked `{reactive}` as a function parameter
164 +- The reactivity propagates through the loop:
165 + - First iteration: `y$37` becomes reactive from `props.value`
166 + - Second iteration: `x$32` becomes reactive from `y$30` (which is reactive via the phi from `y$37`)
167 + - The phi nodes `x$26` and `y$30` become reactive because their bb3 operands are reactive
168 +- The fixpoint algorithm handles this backward propagation through the loop correctly
169 +- The final output `$40` is reactive, so the array `[x]` will be memoized with `x` as a dependency
compiler/packages/babel-plugin-react-compiler/docs/passes/11-inferReactiveScopeVariables.md new
+176
@@ -0,0 +1,176 @@
1 +# inferReactiveScopeVariables
2 +
3 +## File
4 +`src/ReactiveScopes/InferReactiveScopeVariables.ts`
5 +
6 +## Purpose
7 +This is the **1st of 4 passes** that determine how to break a React function into discrete reactive scopes (independently memoizable units of code). Its specific responsibilities are:
8 +
9 +1. **Identify operands that mutate together** - Variables that are mutated in the same instruction must be placed in the same reactive scope
10 +2. **Assign a unique ReactiveScope to each group** - Each disjoint set of co-mutating identifiers gets assigned a unique ScopeId
11 +3. **Compute the mutable range** - The scope's range is computed as the union of all member identifiers' mutable ranges
12 +
13 +The pass does NOT determine which instructions compute each scope, only which variables belong together.
14 +
15 +## Input Invariants
16 +- `InferMutationAliasingEffects` has run - Effects describe mutations, captures, and aliasing
17 +- `InferMutationAliasingRanges` has run - Each identifier has a valid `mutableRange` property
18 +- `InferReactivePlaces` has run - Places are marked as reactive or not
19 +- `RewriteInstructionKindsBasedOnReassignment` has run - Let/Const properly determined
20 +- All instructions have been numbered with valid `InstructionId` values
21 +- Phi nodes are properly constructed at block join points
22 +
23 +## Output Guarantees
24 +- Each identifier that is part of a mutable group has its `identifier.scope` property set to a `ReactiveScope` object
25 +- All identifiers in the same scope share the same `ReactiveScope` reference
26 +- The scope's `range` is the union (min start, max end) of all member mutable ranges
27 +- The scope's `range` is validated to be within [1, maxInstruction+1]
28 +- Identifiers that only have single-instruction lifetimes (read once) may not be assigned to a scope unless they allocate
29 +
30 +## Algorithm
31 +
32 +### Phase 1: Find Disjoint Mutable Values (`findDisjointMutableValues`)
33 +
34 +Uses a Union-Find (Disjoint Set) data structure to group identifiers that mutate together:
35 +
36 +1. **Handle Phi Nodes**: For each phi in each block:
37 + - If the phi's result is mutated after creation (mutableRange.end > first instruction in block), union the phi with all its operands
38 + - This ensures values that flow through control flow and are later mutated are grouped together
39 +
40 +2. **Handle Instructions**: For each instruction:
41 + - Collect mutable operands based on instruction type:
42 + - If lvalue has extended mutable range OR instruction may allocate, include lvalue
43 + - For StoreLocal/StoreContext: Include lvalue if it has extended mutable range, include value if mutable
44 + - For Destructure: Include each pattern operand with extended range, include source if mutable
45 + - For MethodCall: Include all mutable operands plus the computed property (to keep method resolution in same scope)
46 + - For other instructions: Include all mutable operands
47 + - Exclude global variables (mutableRange.start === 0) since they cannot be recreated
48 + - Union all collected operands together
49 +
50 +### Phase 2: Assign Scopes
51 +
52 +1. Iterate over all identifiers in the disjoint set using `forEach(item, groupIdentifier)`
53 +2. For each unique group, create a new ReactiveScope:
54 + - Generate a unique ScopeId from the environment
55 + - Initialize range from the first member's mutableRange
56 + - Set up empty dependencies, declarations, reassignments sets
57 +3. For subsequent members of the same group:
58 + - Expand the scope's range to encompass the member's mutableRange
59 + - Merge source locations
60 +4. Assign the scope to each identifier: `identifier.scope = scope`
61 +5. Update each identifier's mutableRange to match the scope's range
62 +
63 +**Validation**: After scope assignment, validate that all scopes have valid ranges within [1, maxInstruction+1].
64 +
65 +## Key Data Structures
66 +
67 +### DisjointSet<Identifier>
68 +A Union-Find data structure optimized for grouping items into disjoint sets:
69 +
70 +```typescript
71 +class DisjointSet<T> {
72 + #entries: Map<T, T>; // Maps each item to its parent (root points to self)
73 +
74 + union(items: Array<T>): void; // Merge items into one set
75 + find(item: T): T | null; // Find the root of item's set (with path compression)
76 + forEach(fn: (item, group) => void): void; // Iterate all items with their group root
77 +}
78 +```
79 +
80 +Path compression is used during `find()` to flatten the tree structure, improving subsequent lookup performance.
81 +
82 +### ReactiveScope
83 +```typescript
84 +type ReactiveScope = {
85 + id: ScopeId;
86 + range: MutableRange; // [start, end) instruction range
87 + dependencies: Set<ReactiveScopeDependency>; // Inputs (populated later)
88 + declarations: Map<IdentifierId, ReactiveScopeDeclaration>; // Outputs (populated later)
89 + reassignments: Set<Identifier>; // Reassigned variables (populated later)
90 + earlyReturnValue: {...} | null; // For scopes with early returns
91 + merged: Set<ScopeId>; // IDs of scopes merged into this one
92 + loc: SourceLocation;
93 +};
94 +```
95 +
96 +## Edge Cases
97 +
98 +### Global Variables
99 +Excluded from scopes (mutableRange.start === 0) since they cannot be recreated during memoization.
100 +
101 +### Phi Nodes After Mutation
102 +When a phi's result is mutated after the join point, all phi operands must be in the same scope to ensure the mutation can be recomputed correctly.
103 +
104 +### MethodCall Property Resolution
105 +The computed property load for a method call is explicitly added to the same scope as the call itself.
106 +
107 +### Allocating Instructions
108 +Instructions that allocate (Array, Object, JSX, etc.) add their lvalue to the scope even if the lvalue has a single-instruction range.
109 +
110 +### Single-Instruction Ranges
111 +Values with range `[n, n+1)` (used exactly once) are only included if they allocate, otherwise they're just read.
112 +
113 +### enableForest Config
114 +When enabled, phi operands are unconditionally unioned with the phi result (even without mutation after the phi).
115 +
116 +## TODOs
117 +1. `// TODO: improve handling of module-scoped variables and globals` - The current approach excludes globals entirely, but a more nuanced handling could be beneficial.
118 +
119 +2. Known issue with aliasing and mutable lifetimes (from header comments):
120 +```javascript
121 +let x = {};
122 +let y = [];
123 +x.y = y; // RHS is not considered mutable here bc not further mutation
124 +mutate(x); // bc y is aliased here, it should still be considered mutable above
125 +```
126 +This suggests the pass may miss some co-mutation relationships when aliasing is involved.
127 +
128 +## Example
129 +
130 +### Fixture: `reactive-scope-grouping.js`
131 +
132 +**Input:**
133 +```javascript
134 +function foo() {
135 + let x = {};
136 + let y = [];
137 + let z = {};
138 + y.push(z); // y and z co-mutate (z captured into y)
139 + x.y = y; // x and y co-mutate (y captured into x)
140 + return x;
141 +}
142 +```
143 +
144 +**After InferReactiveScopeVariables:**
145 +```
146 +[1] mutate? $19_@0[1:14] = Object { } // x's initial object, scope @0
147 +[2] store $21_@0[1:14] = StoreLocal x // x in scope @0
148 +[3] mutate? $22_@1[3:11] = Array [] // y's array, scope @1
149 +[4] store $24_@1[3:11] = StoreLocal y // y in scope @1
150 +[5] mutate? $25_@2 = Object { } // z's object, scope @2
151 +[10] MethodCall y.push(z) // Mutates y, captures z
152 +[13] PropertyStore x.y = y // Mutates x, captures y
153 +```
154 +
155 +The `y.push(z)` joins y and z into scope @1, and `x.y = y` joins x and y into scope @0. Because y is now in @0, and z was captured into y, ultimately x, y, and z all end up in the same scope @0.
156 +
157 +**Compiled Output:**
158 +```javascript
159 +function foo() {
160 + const $ = _c(1);
161 + let x;
162 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
163 + x = {};
164 + const y = [];
165 + const z = {};
166 + y.push(z);
167 + x.y = y;
168 + $[0] = x;
169 + } else {
170 + x = $[0];
171 + }
172 + return x;
173 +}
174 +```
175 +
176 +All three objects (x, y, z) are created within the same memoization block because they co-mutate and could potentially alias each other.
compiler/packages/babel-plugin-react-compiler/docs/passes/12-rewriteInstructionKindsBasedOnReassignment.md new
+151
@@ -0,0 +1,151 @@
1 +# rewriteInstructionKindsBasedOnReassignment
2 +
3 +## File
4 +`src/SSA/RewriteInstructionKindsBasedOnReassignment.ts`
5 +
6 +## Purpose
7 +Rewrites the `InstructionKind` of variable declaration and assignment instructions to correctly reflect whether variables should be declared as `const` or `let` in the final output. It determines this based on whether a variable is subsequently reassigned after its initial declaration.
8 +
9 +The key insight is that this pass runs **after dead code elimination (DCE)**, so a variable that was originally declared with `let` in the source (because it was reassigned) may be converted to `const` if the reassignment was removed by DCE. However, variables originally declared as `const` cannot become `let`.
10 +
11 +## Input Invariants
12 +- SSA form: Each identifier has a unique `IdentifierId` and `DeclarationId`
13 +- Dead code elimination has run: Unused assignments have been removed
14 +- Mutation/aliasing inference complete: Runs after `InferMutationAliasingRanges` and `InferReactivePlaces` in the main pipeline
15 +- All instruction kinds are initially set (typically `Let` for variables that may be reassigned)
16 +
17 +## Output Guarantees
18 +- **First declaration gets `Const` or `Let`**: The first `StoreLocal` for a named variable is marked as:
19 + - `InstructionKind.Const` if the variable is never reassigned after
20 + - `InstructionKind.Let` if the variable has subsequent reassignments
21 +- **Reassignments marked as `Reassign`**: Any subsequent `StoreLocal` to the same `DeclarationId` is marked as `InstructionKind.Reassign`
22 +- **Destructure consistency**: All places in a destructuring pattern must have consistent kinds (all Const or all Reassign)
23 +- **Update operations trigger Let**: `PrefixUpdate` and `PostfixUpdate` operations (like `++x` or `x--`) mark the original declaration as `Let`
24 +
25 +## Algorithm
26 +
27 +1. **Initialize declarations map**: Create a `Map<DeclarationId, LValue | LValuePattern>` to track declared variables.
28 +
29 +2. **Seed with parameters and context**: Add all named function parameters and captured context variables to the map with kind `Let` (since they're already "declared" outside the function body).
30 +
31 +3. **Process blocks in order**: Iterate through all blocks and instructions:
32 +
33 + - **DeclareLocal**: Record the declaration in the map (invariant: must not already exist)
34 +
35 + - **StoreLocal**:
36 + - If not in map: This is the first store, add to map with `kind = Const`
37 + - If already in map: This is a reassignment. Update original declaration to `Let`, set current instruction to `Reassign`
38 +
39 + - **Destructure**:
40 + - For each operand in the pattern, check if it's already declared
41 + - All operands must be consistent (all new declarations OR all reassignments)
42 + - Set pattern kind to `Const` for new declarations, `Reassign` for existing ones
43 +
44 + - **PrefixUpdate / PostfixUpdate**: Look up the declaration and mark it as `Let` (these always imply reassignment)
45 +
46 +## Key Data Structures
47 +
48 +```typescript
49 +// Main tracking structure
50 +const declarations = new Map<DeclarationId, LValue | LValuePattern>();
51 +
52 +// InstructionKind enum (from HIR.ts)
53 +enum InstructionKind {
54 + Const = 'Const', // const declaration
55 + Let = 'Let', // let declaration
56 + Reassign = 'Reassign', // reassignment to existing binding
57 + Catch = 'Catch', // catch clause binding
58 + HoistedLet = 'HoistedLet', // hoisted let
59 + HoistedConst = 'HoistedConst', // hoisted const
60 + HoistedFunction = 'HoistedFunction', // hoisted function
61 + Function = 'Function', // function declaration
62 +}
63 +```
64 +
65 +## Edge Cases
66 +
67 +### DCE Removes Reassignment
68 +A `let x = 0; x = 1;` where `x = 1` is unused becomes `const x = 0;` after DCE.
69 +
70 +### Destructuring with Mixed Operands
71 +The invariant checks ensure all operands in a destructure pattern are either all new declarations or all reassignments. Mixed cases cause a compiler error.
72 +
73 +### Value Blocks with DCE
74 +There's a TODO for handling reassignment in value blocks where the original declaration was removed by DCE.
75 +
76 +### Parameters and Context Variables
77 +These are pre-seeded as `Let` in the declarations map since they're conceptually "declared" at function entry.
78 +
79 +### Update Expressions
80 +`++x` and `x--` always mark the variable as `Let`, even if used inline.
81 +
82 +## TODOs
83 +```typescript
84 +CompilerError.invariant(block.kind !== 'value', {
85 + reason: `TODO: Handle reassignment in a value block where the original
86 + declaration was removed by dead code elimination (DCE)`,
87 + ...
88 +});
89 +```
90 +
91 +This indicates an edge case where a destructuring reassignment occurs in a value block but the original declaration was eliminated by DCE. This is currently an invariant violation rather than handled gracefully.
92 +
93 +## Example
94 +
95 +### Fixture: `reassignment.js`
96 +
97 +**Input Source:**
98 +```javascript
99 +function Component(props) {
100 + let x = [];
101 + x.push(props.p0);
102 + let y = x;
103 +
104 + x = [];
105 + let _ = <Component x={x} />;
106 +
107 + y.push(props.p1);
108 +
109 + return <Component x={x} y={y} />;
110 +}
111 +```
112 +
113 +**Before Pass (InferReactivePlaces output):**
114 +```
115 +[2] StoreLocal Let x$32 = $31 // x is initially marked Let
116 +[9] StoreLocal Let y$40 = $39 // y is initially marked Let
117 +[11] StoreLocal Reassign x$43 = $42 // reassignment already marked
118 +```
119 +
120 +**After Pass:**
121 +```
122 +[2] StoreLocal Let x$32 = $31 // x stays Let (has reassignment at line 11)
123 +[9] StoreLocal Const y$40 = $39 // y becomes Const (never reassigned)
124 +[11] StoreLocal Reassign x$43 = $42 // stays Reassign
125 +```
126 +
127 +**Final Generated Code:**
128 +```javascript
129 +function Component(props) {
130 + const $ = _c(4);
131 + let t0;
132 + if ($[0] !== props.p0 || $[1] !== props.p1) {
133 + let x = []; // let because reassigned
134 + x.push(props.p0);
135 + const y = x; // const because never reassigned
136 + // ... x = t1; (reassignment)
137 + y.push(props.p1);
138 + t0 = <Component x={x} y={y} />;
139 + // ...
140 + }
141 + return t0;
142 +}
143 +```
144 +
145 +The pass correctly identified that `x` needs `let` (since it's reassigned on line 6 of the source) while `y` can use `const` (it's never reassigned after initialization).
146 +
147 +## Where This Pass is Called
148 +
149 +1. **Main Pipeline** (`src/Entrypoint/Pipeline.ts:322`): Called after `InferReactivePlaces` and before `InferReactiveScopeVariables`.
150 +
151 +2. **AnalyseFunctions** (`src/Inference/AnalyseFunctions.ts:58`): Called when lowering inner function expressions as part of the function analysis phase.
compiler/packages/babel-plugin-react-compiler/docs/passes/13-alignMethodCallScopes.md new
+131
@@ -0,0 +1,131 @@
1 +# alignMethodCallScopes
2 +
3 +## File
4 +`src/ReactiveScopes/AlignMethodCallScopes.ts`
5 +
6 +## Purpose
7 +Ensures that `MethodCall` instructions and their associated `PropertyLoad` instructions (which load the method being called) have consistent scope assignments. The pass enforces one of two invariants:
8 +1. Both the MethodCall lvalue and the property have the **same** reactive scope
9 +2. **Neither** has a reactive scope
10 +
11 +This alignment is critical because the PropertyLoad and MethodCall are semantically a single operation (`receiver.method(args)`) and must be memoized together as a unit. If they had different scopes, the generated code would incorrectly try to memoize the property load separately from the method call, which could break correctness.
12 +
13 +## Input Invariants
14 +- The function has been converted to HIR form
15 +- `inferReactiveScopeVariables` has already run, assigning initial reactive scopes to identifiers based on mutation analysis
16 +- Each instruction's lvalue has an `identifier.scope` that is either a `ReactiveScope` or `null`
17 +- For `MethodCall` instructions, the `value.property` field contains a `Place` referencing the loaded method
18 +
19 +## Output Guarantees
20 +After this pass runs:
21 +- For every `MethodCall` instruction in the function:
22 + - If the lvalue has a scope AND the property has a scope, they point to the **same merged scope**
23 + - If only the lvalue has a scope, the property's scope is set to match the lvalue's scope
24 + - If only the property has a scope, the property's scope is set to `null` (so neither has a scope)
25 +- Merged scopes have their `range` extended to cover the union of the original scopes' ranges
26 +- Nested functions (FunctionExpression, ObjectMethod) are recursively processed
27 +
28 +## Algorithm
29 +
30 +### Phase 1: Collect Scope Relationships
31 +```
32 +For each instruction in all blocks:
33 + If instruction is a MethodCall:
34 + lvalueScope = instruction.lvalue.identifier.scope
35 + propertyScope = instruction.value.property.identifier.scope
36 +
37 + If both have scopes:
38 + Record that these scopes should be merged (using DisjointSet.union)
39 + Else if only lvalue has scope:
40 + Record that property should be assigned to lvalueScope
41 + Else if only property has scope:
42 + Record that property should be assigned to null (no scope)
43 +
44 + If instruction is FunctionExpression or ObjectMethod:
45 + Recursively process the nested function
46 +```
47 +
48 +### Phase 2: Merge Scopes
49 +```
50 +For each merged scope group:
51 + Pick a "root" scope
52 + Extend root's range to cover all merged scopes:
53 + root.range.start = min(all scope start points)
54 + root.range.end = max(all scope end points)
55 +```
56 +
57 +### Phase 3: Apply Changes
58 +```
59 +For each instruction:
60 + If lvalue was recorded for remapping:
61 + Set identifier.scope to the mapped value
62 + Else if identifier has a scope that was merged:
63 + Set identifier.scope to the merged root scope
64 +```
65 +
66 +## Key Data Structures
67 +
68 +1. **`scopeMapping: Map<IdentifierId, ReactiveScope | null>`**
69 + - Maps property identifier IDs to their new scope assignment
70 + - Value of `null` means the scope should be removed
71 +
72 +2. **`mergedScopes: DisjointSet<ReactiveScope>`**
73 + - Union-find data structure tracking scopes that need to be merged
74 + - Used when both MethodCall and property have different scopes
75 +
76 +3. **`ReactiveScope`** (from HIR)
77 + - Contains `range: { start: InstructionId, end: InstructionId }`
78 + - The range defines which instructions are part of the scope
79 +
80 +## Edge Cases
81 +
82 +### Both Have the Same Scope Already
83 +No action needed (implicit in the logic).
84 +
85 +### Nested Functions
86 +The pass recursively processes `FunctionExpression` and `ObjectMethod` instructions to handle closures.
87 +
88 +### Multiple MethodCalls Sharing Scopes
89 +The DisjointSet handles transitive merging - if A merges with B, and B merges with C, all three end up in the same scope.
90 +
91 +### Property Without Scope, MethodCall Without Scope
92 +No action needed (both already aligned at `null`).
93 +
94 +## TODOs
95 +There are no explicit TODO comments in the source code.
96 +
97 +## Example
98 +
99 +### Fixture: `alias-capture-in-method-receiver.js`
100 +
101 +**Source code:**
102 +```javascript
103 +function Component() {
104 + let a = someObj();
105 + let x = [];
106 + x.push(a);
107 + return [x, a];
108 +}
109 +```
110 +
111 +**Before AlignMethodCallScopes:**
112 +```
113 +[7] store $24_@1[4:10]:TFunction = PropertyLoad capture $23_@1.push
114 +[9] mutate? $26:TPrimitive = MethodCall store $23_@1.read $24_@1(capture $25)
115 +```
116 +- PropertyLoad result `$24_@1` has scope `@1`
117 +- MethodCall result `$26` has no scope (`null`)
118 +
119 +**After AlignMethodCallScopes:**
120 +```
121 +[7] store $24[4:10]:TFunction = PropertyLoad capture $23_@1.push
122 +[9] mutate? $26:TPrimitive = MethodCall store $23_@1.read $24(capture $25)
123 +```
124 +- PropertyLoad result `$24` now has **no scope** (the `_@1` suffix removed)
125 +- MethodCall result `$26` still has no scope
126 +
127 +**Why this matters:**
128 +Without this alignment, later passes might try to memoize the `.push` property load separately from the actual `push()` call. This would be incorrect because:
129 +1. Reading a method from an object and calling it are semantically one operation
130 +2. The property load's value (the bound method) is only valid immediately when called on the same receiver
131 +3. Separate memoization could lead to stale method references or incorrect this-binding
compiler/packages/babel-plugin-react-compiler/docs/passes/14-alignObjectMethodScopes.md new
+128
@@ -0,0 +1,128 @@
1 +# alignObjectMethodScopes
2 +
3 +## File
4 +`src/ReactiveScopes/AlignObjectMethodScopes.ts`
5 +
6 +## Purpose
7 +Ensures that object method values and their enclosing object expressions share the same reactive scope. This is critical for code generation because JavaScript requires object method definitions to be inlined within their containing object literals. If the object method and object expression were in different reactive scopes (which map to different memoization blocks), the generated code would be invalid since you cannot reference an object method defined in one block from an object literal in a different block.
8 +
9 +From the file's documentation:
10 +> "To produce a well-formed JS program in Codegen, object methods and object expressions must be in the same ReactiveBlock as object method definitions must be inlined."
11 +
12 +## Input Invariants
13 +- Reactive scopes have been inferred: This pass runs after `InferReactiveScopeVariables`
14 +- ObjectMethod and ObjectExpression have non-null scopes: The pass asserts this with an invariant check
15 +- Scopes are disjoint across functions: The pass assumes that scopes do not overlap between parent and nested functions
16 +
17 +## Output Guarantees
18 +- ObjectMethod and ObjectExpression share the same scope: Any ObjectMethod used as a property in an ObjectExpression will have its scope merged with the ObjectExpression's scope
19 +- Merged scope covers both ranges: The resulting merged scope's range is expanded to cover the minimum start and maximum end of all merged scopes
20 +- All identifiers are repointed: All identifiers whose scopes were merged are updated to point to the canonical root scope
21 +- Inner functions are also processed: The pass recursively handles nested ObjectMethod and FunctionExpression values
22 +
23 +## Algorithm
24 +
25 +### Phase 1: Find Scopes to Merge (`findScopesToMerge`)
26 +1. Iterate through all blocks and instructions in the function
27 +2. Track all ObjectMethod declarations in a set by their lvalue identifier
28 +3. When encountering an ObjectExpression, check each operand:
29 + - If an operand's identifier was previously recorded as an ObjectMethod declaration
30 + - Get the scope of both the ObjectMethod operand and the ObjectExpression lvalue
31 + - Assert both scopes are non-null
32 + - Union these two scopes together in a DisjointSet data structure
33 +
34 +### Phase 2: Merge and Repoint Scopes (`alignObjectMethodScopes`)
35 +1. Recursively process inner functions first (ObjectMethod and FunctionExpression values)
36 +2. Canonicalize the DisjointSet to get a mapping from each scope to its root
37 +3. **Step 1 - Merge ranges**: For each scope that maps to a different root:
38 + - Expand the root's range to encompass both the original range and the merged scope's range
39 + - `root.range.start = min(scope.range.start, root.range.start)`
40 + - `root.range.end = max(scope.range.end, root.range.end)`
41 +4. **Step 2 - Repoint identifiers**: For each instruction's lvalue:
42 + - If the identifier has a scope that was merged
43 + - Update the identifier's scope reference to point to the canonical root
44 +
45 +## Key Data Structures
46 +
47 +1. **DisjointSet<ReactiveScope>** - A union-find data structure that tracks which scopes should be merged together. Uses path compression for efficient `find()` operations.
48 +
49 +2. **Set<Identifier>** - Tracks which identifiers are ObjectMethod declarations, used to identify when an ObjectExpression operand is an object method.
50 +
51 +3. **ReactiveScope** - Contains:
52 + - `id: ScopeId` - Unique identifier
53 + - `range: MutableRange` - Start and end instruction IDs
54 + - `dependencies` - Inputs to the scope
55 + - `declarations` - Values produced by the scope
56 +
57 +4. **MutableRange** - Has `start` and `end` InstructionId fields that define the scope's extent.
58 +
59 +## Edge Cases
60 +
61 +### Nested Object Methods
62 +When an object method itself contains another object with methods, the pass recursively processes inner functions first before handling the outer function's scopes.
63 +
64 +### Multiple Object Methods in Same Object
65 +If an object has multiple method properties, all their scopes will be merged with the object's scope through the DisjointSet.
66 +
67 +### Object Methods in Conditional Expressions
68 +Object methods inside ternary expressions still need scope alignment to ensure the method and its containing object are in the same reactive block.
69 +
70 +### Method Call After Object Creation
71 +The pass works in conjunction with `AlignMethodCallScopes` (which runs immediately before) to ensure that method calls on objects with object methods are also properly scoped.
72 +
73 +## TODOs
74 +None explicitly marked in the source file.
75 +
76 +## Example
77 +
78 +### Fixture: `object-method-shorthand.js`
79 +
80 +**Input:**
81 +```javascript
82 +function Component() {
83 + let obj = {
84 + method() {
85 + return 1;
86 + },
87 + };
88 + return obj.method();
89 +}
90 +```
91 +
92 +**Before AlignObjectMethodScopes:**
93 +```
94 +InferReactiveScopeVariables:
95 + [1] mutate? $12_@0:TObjectMethod = ObjectMethod ... // scope @0
96 + [2] mutate? $14_@1[2:7]:TObject = Object { method: ... } // scope @1 (range 2:7)
97 +```
98 +The ObjectMethod `$12` is in scope `@0` while the ObjectExpression `$14` is in scope `@1` with range `[2:7]`.
99 +
100 +**After AlignObjectMethodScopes:**
101 +```
102 +AlignObjectMethodScopes:
103 + [1] mutate? $12_@0[1:7]:TObjectMethod = ObjectMethod ... // scope @0, range now 1:7
104 + [2] mutate? $14_@0[1:7]:TObject = Object { method: ... } // also scope @0, range 1:7
105 +```
106 +Both identifiers are in the same scope `@0`, and the scope's range has been expanded to `[1:7]` to cover both instructions.
107 +
108 +**Final Generated Code:**
109 +```javascript
110 +function Component() {
111 + const $ = _c(1);
112 + let t0;
113 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
114 + const obj = {
115 + method() {
116 + return 1;
117 + },
118 + };
119 + t0 = obj.method();
120 + $[0] = t0;
121 + } else {
122 + t0 = $[0];
123 + }
124 + return t0;
125 +}
126 +```
127 +
128 +The object literal with its method and the subsequent method call are all inside the same memoization block, producing valid JavaScript where the method definition is inlined within the object literal.
compiler/packages/babel-plugin-react-compiler/docs/passes/15-alignReactiveScopesToBlockScopesHIR.md new
+177
@@ -0,0 +1,177 @@
1 +# alignReactiveScopesToBlockScopesHIR
2 +
3 +## File
4 +`src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts`
5 +
6 +## Purpose
7 +This is the **2nd of 4 passes** that determine how to break a function into discrete reactive scopes (independently memoizable units of code). The pass aligns reactive scope boundaries to control flow (block scope) boundaries.
8 +
9 +The problem it solves: Prior inference passes assign reactive scopes to operands based on mutation ranges at arbitrary instruction points in the control-flow graph. However, to generate memoization blocks around instructions, scopes must be aligned to block-scope boundaries -- you cannot memoize half of a loop or half of an if-block.
10 +
11 +**Example from the source code comments:**
12 +```javascript
13 +function foo(cond, a) {
14 + // original scope end
15 + // expanded scope end
16 + const x = []; | |
17 + if (cond) { | |
18 + ... | |
19 + x.push(a); <--- original scope ended here
20 + ... |
21 + } <--- scope must extend to here
22 +}
23 +```
24 +
25 +## Input Invariants
26 +- `InferReactiveScopeVariables` has run: Each identifier has been assigned a `ReactiveScope` with a `range` (start/end instruction IDs) based on mutation analysis
27 +- The HIR is in SSA form: Blocks have unique IDs, instructions have unique IDs, and control flow is represented with basic blocks
28 +- Each block has a terminal with possible successors and fallthroughs
29 +- Each scope has a mutable range `{start: InstructionId, end: InstructionId}` indicating when the scope is active
30 +
31 +## Output Guarantees
32 +- **Scopes end at valid block boundaries**: A reactive scope may only end at the same block scope level as it began. The scope's `range.end` is updated to the first instruction of the fallthrough block after any control flow structure that the scope overlaps
33 +- **Scopes start at valid block boundaries**: For labeled breaks (gotos to a label), scopes that extend beyond the goto have their `range.start` extended back to include the label
34 +- **Value blocks (ternary, logical, optional) are handled specially**: Scopes inside value blocks are extended to align with the outer block scope's instruction range
35 +
36 +## Algorithm
37 +
38 +The pass performs a single forward traversal over all blocks:
39 +
40 +### 1. Tracking Active Scopes
41 +- Maintains `activeScopes: Set<ReactiveScope>` - scopes whose range overlaps the current block
42 +- Maintains `activeBlockFallthroughRanges: Array<{range, fallthrough}>` - stack of pending block-fallthrough ranges
43 +
44 +### 2. Per-Block Processing
45 +For each block:
46 +- Prune `activeScopes` to only those that extend past the current block's start
47 +- If this block is a fallthrough target, pop the range from the stack and extend all active scopes' start to the range start
48 +
49 +### 3. Recording Places
50 +For each instruction lvalue and operand:
51 +- If the place has a scope, add it to `activeScopes`
52 +- If inside a value block, extend the scope's range to match the value block's outer range
53 +
54 +### 4. Handling Block Fallthroughs
55 +When a terminal has a fallthrough (not a simple branch):
56 +- Extend all active scopes whose `range.end > terminal.id` to at least the first instruction of the fallthrough block
57 +- Push the fallthrough range onto the stack for future scopes
58 +
59 +### 5. Handling Labeled Breaks (Goto)
60 +When encountering a goto to a label (not the natural fallthrough):
61 +- Find the corresponding fallthrough range on the stack
62 +- Extend all active scopes to span from the label start to its fallthrough end
63 +
64 +### 6. Value Block Handling
65 +For ternary, logical, and optional terminals:
66 +- Create `ValueBlockNode` to track the outer block's instruction range
67 +- Scopes inside value blocks inherit this range, ensuring they align to the outer block scope
68 +
69 +## Key Data Structures
70 +
71 +```typescript
72 +type ValueBlockNode = {
73 + kind: 'node';
74 + id: InstructionId;
75 + valueRange: MutableRange; // Range of outer block scope
76 + children: Array<ValueBlockNode | ReactiveScopeNode>;
77 +};
78 +
79 +type ReactiveScopeNode = {
80 + kind: 'scope';
81 + id: InstructionId;
82 + scope: ReactiveScope;
83 +};
84 +
85 +// Tracked during traversal:
86 +activeBlockFallthroughRanges: Array<{
87 + range: InstructionRange;
88 + fallthrough: BlockId;
89 +}>;
90 +activeScopes: Set<ReactiveScope>;
91 +valueBlockNodes: Map<BlockId, ValueBlockNode>;
92 +```
93 +
94 +## Edge Cases
95 +
96 +### Labeled Breaks
97 +When a `goto` jumps to a label (not the natural fallthrough), scopes must be extended to include the entire labeled block range, preventing the break from jumping out of the scope.
98 +
99 +### Value Blocks (Ternary/Logical/Optional)
100 +These create nested "value" contexts. Scopes inside must be aligned to the outer block scope's boundaries, not the value block's boundaries.
101 +
102 +### Nested Control Flow
103 +Deeply nested if-statements require the scope to be extended through all levels back to the outermost block where the scope started.
104 +
105 +### do-while and try/catch
106 +The terminal's successor might be a block (not value block), which is handled specially.
107 +
108 +## TODOs
109 +1. `// TODO: consider pruning activeScopes per instruction` - Currently, `activeScopes` is only pruned at block start points. Some scopes may no longer be active by the time a goto is encountered.
110 +
111 +2. `// TODO: add a variant of eachTerminalSuccessor() that visits _all_ successors, not just those that are direct successors for normal control-flow ordering.` - The current implementation uses `mapTerminalSuccessors` which may not visit all successors in all cases.
112 +
113 +## Example
114 +
115 +### Fixture: `extend-scopes-if.js`
116 +
117 +**Input:**
118 +```javascript
119 +function foo(a, b, c) {
120 + let x = [];
121 + if (a) {
122 + if (b) {
123 + if (c) {
124 + x.push(0); // Mutation of x ends here (instruction 12-13)
125 + }
126 + }
127 + }
128 + if (x.length) { // instruction 16
129 + return x;
130 + }
131 + return null;
132 +}
133 +```
134 +
135 +**Before AlignReactiveScopesToBlockScopesHIR:**
136 +```
137 +x$23_@0[1:13] // Scope range 1-13
138 +```
139 +The scope for `x` ends at instruction 13 (inside the innermost if block).
140 +
141 +**After AlignReactiveScopesToBlockScopesHIR:**
142 +```
143 +x$23_@0[1:16] // Scope range extended to 1-16
144 +```
145 +The scope is extended to instruction 16 (the first instruction after all the nested if-blocks), aligning to the block scope boundary.
146 +
147 +**Generated Code:**
148 +```javascript
149 +function foo(a, b, c) {
150 + const $ = _c(4);
151 + let x;
152 + if ($[0] !== a || $[1] !== b || $[2] !== c) {
153 + x = [];
154 + if (a) {
155 + if (b) {
156 + if (c) {
157 + x.push(0);
158 + }
159 + }
160 + }
161 + // Scope ends here, after ALL the if-blocks
162 + $[0] = a;
163 + $[1] = b;
164 + $[2] = c;
165 + $[3] = x;
166 + } else {
167 + x = $[3];
168 + }
169 + // Code outside the scope
170 + if (x.length) {
171 + return x;
172 + }
173 + return null;
174 +}
175 +```
176 +
177 +The memoization block correctly wraps the entire nested if-structure, not just part of it.
compiler/packages/babel-plugin-react-compiler/docs/passes/16-mergeOverlappingReactiveScopesHIR.md new
+134
@@ -0,0 +1,134 @@
1 +# mergeOverlappingReactiveScopesHIR
2 +
3 +## File
4 +`src/HIR/MergeOverlappingReactiveScopesHIR.ts`
5 +
6 +## Purpose
7 +This pass ensures that reactive scope ranges form valid, non-overlapping blocks in the output JavaScript program. It merges reactive scopes that would otherwise be inconsistent with each other due to:
8 +
9 +1. **Overlapping ranges**: Scopes whose instruction ranges partially overlap (not disjoint and not nested) must be merged because the compiler cannot produce valid `if-else` memo blocks for overlapping scopes.
10 +
11 +2. **Cross-scope mutations**: When an instruction within one scope mutates a value belonging to a different (outer) scope, those scopes must be merged to maintain correctness.
12 +
13 +The pass guarantees that after execution, any two reactive scopes are either:
14 +- Entirely disjoint (no common instructions)
15 +- Properly nested (one scope is completely contained within the other)
16 +
17 +## Input Invariants
18 +- Reactive scope variables have been inferred (`InferReactiveScopeVariables` pass has run)
19 +- Scopes have been aligned to block scopes (`AlignReactiveScopesToBlockScopesHIR` pass has run)
20 +- Each `Place` may have an associated `ReactiveScope` with a `range` (start/end instruction IDs)
21 +- Scopes may still have overlapping ranges or contain instructions that mutate outer scopes
22 +
23 +## Output Guarantees
24 +- **No overlapping scopes**: All reactive scopes either are disjoint or properly nested
25 +- **Consistent mutation boundaries**: Instructions only mutate their "active" scope (the innermost containing scope)
26 +- **Merged scope ranges**: Merged scopes have their ranges extended to cover the union of all constituent scopes
27 +- **Updated references**: All `Place` references have their `identifier.scope` updated to point to the merged scope
28 +
29 +## Algorithm
30 +
31 +### Phase 1: Collect Scope Information (`collectScopeInfo`)
32 +- Iterates through all instructions and terminals in the function
33 +- Records for each `Place`:
34 + - The scope it belongs to (`placeScopes` map)
35 + - When scopes start and end (`scopeStarts` and `scopeEnds` arrays, sorted in descending order by ID)
36 +- Only records scopes with non-empty ranges (`range.start !== range.end`)
37 +
38 +### Phase 2: Detect Overlapping Scopes (`getOverlappingReactiveScopes`)
39 +Uses a stack-based traversal to track "active" scopes at each instruction:
40 +
41 +1. **For each instruction/terminal**:
42 + - **Handle scope endings**: Pop completed scopes from the active stack. If a scope ends while other scopes that started later are still active (detected by finding the scope is not at the top of the stack), those scopes overlap and must be merged via `DisjointSet.union()`.
43 +
44 + - **Handle scope starts**: Push new scopes onto the active stack (sorted by end time descending so earlier-ending scopes are at the top). Merge any scopes that have identical start/end ranges.
45 +
46 + - **Handle mutations**: For each operand/lvalue, if it:
47 + - Has an associated scope
48 + - Is mutable at the current instruction
49 + - The scope is active but not at the top of the stack (i.e., an outer scope)
50 +
51 + Then merge all scopes from the mutated outer scope to the top of the stack.
52 +
53 +2. **Special case**: Primitive operands in `FunctionExpression` and `ObjectMethod` are skipped.
54 +
55 +### Phase 3: Merge Scopes and Rewrite References
56 +1. For each scope in the disjoint set, compute the merged range as the union (min start, max end)
57 +2. Update all `Place.identifier.scope` references to point to the merged "group" scope
58 +
59 +## Key Data Structures
60 +
61 +### ScopeInfo
62 +```typescript
63 +type ScopeInfo = {
64 + scopeStarts: Array<{id: InstructionId; scopes: Set<ReactiveScope>}>;
65 + scopeEnds: Array<{id: InstructionId; scopes: Set<ReactiveScope>}>;
66 + placeScopes: Map<Place, ReactiveScope>;
67 +};
68 +```
69 +
70 +### TraversalState
71 +```typescript
72 +type TraversalState = {
73 + joined: DisjointSet<ReactiveScope>; // Union-find for merged scopes
74 + activeScopes: Array<ReactiveScope>; // Stack of currently active scopes
75 +};
76 +```
77 +
78 +### DisjointSet<ReactiveScope>
79 +A union-find data structure that tracks which scopes should be merged into the same group.
80 +
81 +## Edge Cases
82 +
83 +### Identical Scope Ranges
84 +When multiple scopes have the exact same start and end, they are automatically merged since they would produce the same reactive block.
85 +
86 +### Empty Scopes
87 +Scopes where `range.start === range.end` are skipped entirely.
88 +
89 +### Primitive Captures in Functions
90 +When a `FunctionExpression` or `ObjectMethod` captures a primitive operand, it's excluded from scope merging analysis.
91 +
92 +### JSX Single-Instruction Scopes
93 +The comment in the code notes this isn't perfect - mutating scopes may get merged with JSX single-instruction scopes.
94 +
95 +### Non-Mutating Captures
96 +The pass records both mutating and non-mutating scopes to handle cases where still-mutating values are aliased by inner scopes.
97 +
98 +## TODOs
99 +From the comments in the source file, the design constraints arise from the current compiler output design:
100 +- **Instruction ordering is preserved**: If reordering were allowed, disjoint ranges could be produced by reordering mutating instructions
101 +- **One if-else block per scope**: The current design doesn't allow composing a reactive scope from disconnected instruction ranges
102 +
103 +## Example
104 +
105 +### Fixture: `overlapping-scopes-interleaved.js`
106 +
107 +**Input Code:**
108 +```javascript
109 +function foo(a, b) {
110 + let x = [];
111 + let y = [];
112 + x.push(a);
113 + y.push(b);
114 +}
115 +```
116 +
117 +**Before MergeOverlappingReactiveScopesHIR:**
118 +```
119 +[1] $20_@0[1:9] = Array [] // x belongs to scope @0, range [1:9]
120 +[2] x$21_@0[1:9] = StoreLocal...
121 +[3] $23_@1[3:13] = Array [] // y belongs to scope @1, range [3:13]
122 +[4] y$24_@1[3:13] = StoreLocal...
123 +```
124 +Scopes @0 [1:9] and @1 [3:13] overlap: @0 starts at 1, @1 starts at 3, @0 ends at 9, @1 ends at 13. This is invalid.
125 +
126 +**After MergeOverlappingReactiveScopesHIR:**
127 +```
128 +[1] $20_@0[1:13] = Array [] // Merged scope @0, range [1:13]
129 +[2] x$21_@0[1:13] = StoreLocal...
130 +[3] $23_@0[1:13] = Array [] // Now also scope @0
131 +[4] y$24_@0[1:13] = StoreLocal...
132 +```
133 +
134 +Both `x` and `y` now belong to the same merged scope @0 with range [1:13], producing a single `if-else` memo block in the output.
compiler/packages/babel-plugin-react-compiler/docs/passes/17-buildReactiveScopeTerminalsHIR.md new
+161
@@ -0,0 +1,161 @@
1 +# buildReactiveScopeTerminalsHIR
2 +
3 +## File
4 +`src/HIR/BuildReactiveScopeTerminalsHIR.ts`
5 +
6 +## Purpose
7 +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:
8 +
9 +1. Inserting a `scope` terminal at the **start** of each reactive scope
10 +2. Inserting a `goto` terminal at the **end** of each reactive scope
11 +3. Creating fallthrough blocks to properly connect the scopes to the rest of the CFG
12 +
13 +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).
14 +
15 +## Input Invariants
16 +- **Properly nested scopes and blocks**: The pass assumes `assertValidBlockNesting` has passed, meaning all program blocks and reactive scopes form a proper tree hierarchy
17 +- **Aligned scope ranges**: Reactive scope ranges have been correctly aligned and merged by previous passes
18 +- **Valid instruction IDs**: All instructions have sequential IDs that define the scope boundaries
19 +- **Scopes attached to identifiers**: Reactive scopes are found by traversing all `Place` operands and collecting unique non-empty scopes
20 +
21 +## Output Guarantees
22 +- **Explicit scope terminals**: Each reactive scope is represented in the CFG as a `ReactiveScopeTerminal` with:
23 + - `block` - The BlockId containing the scope's instructions
24 + - `fallthrough` - The BlockId that executes after the scope
25 +- **Proper block structure**: Original blocks are split at scope boundaries
26 +- **Restored HIR invariants**: The pass restores RPO ordering, predecessor sets, instruction IDs, and scope/identifier ranges
27 +- **Updated phi nodes**: Phi operands are repointed when their source blocks are split
28 +
29 +## Algorithm
30 +
31 +### Step 1: Collect Scope Rewrites
32 +```
33 +for each reactive scope (in range pre-order):
34 + push StartScope rewrite at scope.range.start
35 + push EndScope rewrite at scope.range.end
36 +```
37 +The `recursivelyTraverseItems` helper traverses scopes in pre-order (outer scopes before inner scopes).
38 +
39 +### Step 2: Apply Rewrites by Splitting Blocks
40 +```
41 +reverse queuedRewrites (to pop in ascending instruction order)
42 +for each block:
43 + for each instruction (or terminal):
44 + while there are rewrites <= current instruction ID:
45 + split block at current index
46 + insert scope terminal (for start) or goto terminal (for end)
47 + emit final block segment with original terminal
48 +```
49 +
50 +### Step 3: Repoint Phi Nodes
51 +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.
52 +
53 +### Step 4: Restore HIR Invariants
54 +- Recompute RPO (reverse post-order) block traversal
55 +- Recalculate predecessor sets
56 +- Renumber instruction IDs
57 +- Fix scope and identifier ranges to match new instruction IDs
58 +
59 +## Key Data Structures
60 +
61 +### TerminalRewriteInfo
62 +```typescript
63 +type TerminalRewriteInfo =
64 + | {
65 + kind: 'StartScope';
66 + blockId: BlockId; // New block for scope content
67 + fallthroughId: BlockId; // Block after scope ends
68 + instrId: InstructionId; // Where to insert
69 + scope: ReactiveScope; // The scope being created
70 + }
71 + | {
72 + kind: 'EndScope';
73 + instrId: InstructionId; // Where to insert
74 + fallthroughId: BlockId; // Same as corresponding StartScope
75 + };
76 +```
77 +
78 +### RewriteContext
79 +```typescript
80 +type RewriteContext = {
81 + source: BasicBlock; // Original block being split
82 + instrSliceIdx: number; // Current slice start index
83 + nextPreds: Set<BlockId>; // Predecessors for next emitted block
84 + nextBlockId: BlockId; // BlockId for next emitted block
85 + rewrites: Array<BasicBlock>; // Accumulated split blocks
86 +};
87 +```
88 +
89 +### ScopeTraversalContext
90 +```typescript
91 +type ScopeTraversalContext = {
92 + fallthroughs: Map<ScopeId, BlockId>; // Cache: scope -> its fallthrough block
93 + rewrites: Array<TerminalRewriteInfo>;
94 + env: Environment;
95 +};
96 +```
97 +
98 +## Edge Cases
99 +
100 +### Multiple Rewrites at Same Instruction ID
101 +The while loop in Step 2 handles multiple scope start/ends at the same instruction ID.
102 +
103 +### Nested Scopes
104 +The pre-order traversal ensures outer scopes are processed before inner scopes, creating proper nesting in the CFG.
105 +
106 +### Empty Blocks After Split
107 +When a scope boundary falls at the start of a block, the split may create a block with no instructions (only a terminal).
108 +
109 +### Control Flow Within Scopes
110 +The pass preserves existing control flow (if/else, loops) within scopes; it only adds scope entry/exit points.
111 +
112 +### Early Returns
113 +When a return occurs within a scope, the scope terminal still has a fallthrough block, but that block may contain `Unreachable` terminal.
114 +
115 +## TODOs
116 +Line 283-284:
117 +```typescript
118 +// TODO make consistent instruction IDs instead of reusing
119 +```
120 +
121 +## Example
122 +
123 +### Fixture: `reactive-scopes-if.js`
124 +
125 +**Before BuildReactiveScopeTerminalsHIR:**
126 +```
127 +bb0 (block):
128 + [1] $29_@0[1:22] = Array [] // x with scope @0 range [1:22]
129 + [2] StoreLocal x$30_@0 = $29_@0
130 + [3] $32 = LoadLocal a$26
131 + [4] If ($32) then:bb2 else:bb3 fallthrough=bb1
132 +bb2:
133 + [5] $33_@1[5:11] = Array [] // y with scope @1 range [5:11]
134 + ...
135 +```
136 +
137 +**After BuildReactiveScopeTerminalsHIR:**
138 +```
139 +bb0 (block):
140 + [1] Scope @0 [1:28] block=bb9 fallthrough=bb10 // <-- scope terminal inserted
141 +bb9:
142 + [2] $29_@0 = Array []
143 + [3] StoreLocal x$30_@0 = $29_@0
144 + [4] $32 = LoadLocal a$26
145 + [5] If ($32) then:bb2 else:bb3 fallthrough=bb1
146 +bb2:
147 + [6] Scope @1 [6:14] block=bb11 fallthrough=bb12 // <-- nested scope terminal
148 +bb11:
149 + [7] $33_@1 = Array []
150 + ...
151 + [13] Goto bb12 // <-- scope end goto
152 +bb12:
153 + ...
154 +bb1:
155 + [27] Goto bb10 // <-- scope @0 end goto
156 +bb10:
157 + [28] $50 = LoadLocal x$30_@0
158 + [29] Return $50
159 +```
160 +
161 +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.
compiler/packages/babel-plugin-react-compiler/docs/passes/18-flattenReactiveLoopsHIR.md new
+158
@@ -0,0 +1,158 @@
1 +# flattenReactiveLoopsHIR
2 +
3 +## File
4 +`src/ReactiveScopes/FlattenReactiveLoopsHIR.ts`
5 +
6 +## Purpose
7 +This pass **prunes reactive scopes that are nested inside loops** (for, for-in, for-of, while, do-while). The compiler does not yet support memoization within loops because:
8 +
9 +1. Loop iterations would require reconciliation across runs (similar to how `key` is used in JSX for lists)
10 +2. There is no way to identify values across iterations
11 +3. The current approach is to memoize *around* the loop rather than *within* it
12 +
13 +When a reactive scope is found inside a loop body, the pass converts its terminal from `scope` to `pruned-scope`. A `pruned-scope` terminal is later treated specially during codegen - its instructions are emitted inline without any memoization guards.
14 +
15 +## Input Invariants
16 +- The HIR has been through `buildReactiveScopeTerminalsHIR`, which creates `scope` terminal nodes for reactive scopes
17 +- The HIR is in valid block form with proper terminal kinds
18 +- The block ordering respects control flow (blocks are iterated in order, with loop fallthroughs appearing after loop bodies)
19 +
20 +## Output Guarantees
21 +- All `scope` terminals that appear inside any loop body are converted to `pruned-scope` terminals
22 +- Scopes outside of loops remain unchanged as `scope` terminals
23 +- The structure of blocks is preserved; only the terminal kind is mutated
24 +- The `pruned-scope` terminal retains all the same fields as `scope` (block, fallthrough, scope, id, loc)
25 +
26 +## Algorithm
27 +
28 +The algorithm uses a **linear scan with a stack-based loop tracking** approach:
29 +
30 +```
31 +1. Initialize an empty array `activeLoops` to track which loop(s) we are currently inside
32 +2. For each block in the function body (in order):
33 + a. Remove the current block ID from activeLoops (if present)
34 + - This happens when we reach a loop's fallthrough block, exiting the loop
35 + b. Examine the block's terminal:
36 + - If it's a loop terminal (do-while, for, for-in, for-of, while):
37 + Push the loop's fallthrough block ID onto activeLoops
38 + - If it's a scope terminal AND activeLoops is non-empty:
39 + Convert the terminal to pruned-scope (keeping all other fields)
40 + - All other terminal kinds are ignored
41 +```
42 +
43 +Key insight: The algorithm tracks when we "enter" a loop by pushing the fallthrough ID when encountering a loop terminal, and "exits" the loop when that fallthrough block is visited.
44 +
45 +## Key Data Structures
46 +
47 +### activeLoops: Array<BlockId>
48 +A stack of block IDs representing loop fallthroughs. When non-empty, we are inside one or more nested loops.
49 +
50 +### PrunedScopeTerminal
51 +```typescript
52 +export type PrunedScopeTerminal = {
53 + kind: 'pruned-scope';
54 + fallthrough: BlockId;
55 + block: BlockId;
56 + scope: ReactiveScope;
57 + id: InstructionId;
58 + loc: SourceLocation;
59 +};
60 +```
61 +
62 +### retainWhere
63 +Utility from utils.ts - an in-place array filter that removes elements not matching the predicate.
64 +
65 +## Edge Cases
66 +
67 +### Nested Loops
68 +The algorithm handles nested loops correctly because `activeLoops` is an array that can contain multiple fallthrough IDs. A scope deep inside multiple nested loops will still be pruned.
69 +
70 +### Scope Spanning the Loop
71 +If a scope terminal appears before the loop terminal but its body contains the loop, it is NOT pruned because the scope terminal itself is not inside the loop.
72 +
73 +### Multiple Loops in Sequence
74 +When exiting one loop (reaching its fallthrough) and entering another, `activeLoops` correctly clears the first loop before potentially adding the second.
75 +
76 +### Control Flow That Exits Loops (break/return)
77 +The algorithm relies on block ordering and fallthrough IDs. Early exits via break/return don't affect the tracking since we track by fallthrough block ID.
78 +
79 +## TODOs
80 +No explicit TODOs in this file. However, the docstring mentions future improvements:
81 +> "Eventually we may integrate more deeply into the runtime so that we can do a single level of reconciliation"
82 +
83 +This suggests a potential future feature to support memoization within loops via runtime integration.
84 +
85 +## Example
86 +
87 +### Fixture: `repro-memoize-for-of-collection-when-loop-body-returns.js`
88 +
89 +**Input:**
90 +```javascript
91 +function useHook(nodeID, condition) {
92 + const graph = useContext(GraphContext);
93 + const node = nodeID != null ? graph[nodeID] : null;
94 +
95 + for (const key of Object.keys(node?.fields ?? {})) {
96 + if (condition) {
97 + return new Class(node.fields?.[field]); // <-- Scope @4 is here
98 + }
99 + }
100 + return new Class(); // <-- Scope @5 is here (outside loop)
101 +}
102 +```
103 +
104 +**Before FlattenReactiveLoopsHIR:**
105 +```
106 +[45] Scope scope @3 [45:72] ... block=bb35 fallthrough=bb36
107 +bb35:
108 + [46] ForOf init=bb6 test=bb7 loop=bb8 fallthrough=bb5
109 + ...
110 + [66] Scope scope @4 [66:69] ... block=bb37 fallthrough=bb38 <-- Inside loop
111 + ...
112 + [73] Scope scope @5 [73:76] ... block=bb39 fallthrough=bb40 <-- Outside loop
113 +```
114 +
115 +**After FlattenReactiveLoopsHIR:**
116 +```
117 +[45] Scope scope @3 [45:72] ... block=bb35 fallthrough=bb36 <-- Unchanged
118 +...
119 +[66] <pruned> Scope scope @4 [66:69] ... block=bb37 fallthrough=bb38 <-- PRUNED!
120 +...
121 +[73] Scope scope @5 [73:76] ... block=bb39 fallthrough=bb40 <-- Unchanged
122 +```
123 +
124 +**Final Codegen Result:**
125 +```javascript
126 +function useHook(nodeID, condition) {
127 + const $ = _c(7);
128 + // ... memoized Object.keys call (scope @2)
129 +
130 + let t1;
131 + if ($[2] !== condition || $[3] !== node || $[4] !== t0) {
132 + // Scope @3 wraps the loop
133 + t1 = Symbol.for("react.early_return_sentinel");
134 + bb0: for (const key of t0) {
135 + if (condition) {
136 + t1 = new Class(node.fields?.[field]); // Scope @4 was PRUNED - no memoization
137 + break bb0;
138 + }
139 + }
140 + $[2] = condition;
141 + $[3] = node;
142 + $[4] = t0;
143 + $[5] = t1;
144 + } else {
145 + t1 = $[5];
146 + }
147 + // ...
148 +
149 + // Scope @5 - memoized (sentinel check)
150 + if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
151 + t2 = new Class();
152 + $[6] = t2;
153 + }
154 + return t2;
155 +}
156 +```
157 +
158 +The `new Class(...)` inside the loop has no memoization guards because scope @4 was pruned. The `new Class()` outside the loop retains its memoization via scope @5.
compiler/packages/babel-plugin-react-compiler/docs/passes/19-flattenScopesWithHooksOrUseHIR.md new
+143
@@ -0,0 +1,143 @@
1 +# flattenScopesWithHooksOrUseHIR
2 +
3 +## File
4 +`src/ReactiveScopes/FlattenScopesWithHooksOrUseHIR.ts`
5 +
6 +## Purpose
7 +This pass removes (flattens) reactive scopes that transitively contain hook calls or `use()` operator calls. The key insight is that:
8 +
9 +1. **Hooks cannot be called conditionally** - wrapping them in a memoized scope would make them conditionally called based on whether the cache is valid
10 +2. **The `use()` operator** - while it can be called conditionally in source code, React requires it to be called consistently if the component needs the returned value. Memoizing a scope containing `use()` would also make it conditionally called.
11 +
12 +By running reactive scope inference first (agnostic of hooks), the compiler knows which values "construct together" in the same scope. The pass then removes ALL memoization for scopes containing hook/use calls to ensure they are always executed unconditionally.
13 +
14 +## Input Invariants
15 +- HIR must have reactive scope terminals already built (pass runs after `BuildReactiveScopeTerminalsHIR`)
16 +- Blocks are visited in order (the pass iterates through `fn.body.blocks`)
17 +- Scope terminals have a `block` (body of the scope) and `fallthrough` (block after the scope)
18 +- Type inference has run so that `getHookKind()` and `isUseOperator()` can identify hooks and use() calls
19 +
20 +## Output Guarantees
21 +- All scopes that transitively contained a hook or `use()` call are either:
22 + - Converted to `LabelTerminal` - if the scope body is trivial (just the hook call and a goto)
23 + - Converted to `PrunedScopeTerminal` - if the scope body contains other instructions besides the hook call
24 +- The `PrunedScopeTerminal` still tracks the original scope information for downstream passes but will not generate memoization code
25 +- The control flow structure is preserved (same blocks, same fallthroughs)
26 +
27 +## Algorithm
28 +
29 +### Phase 1: Identify Scopes Containing Hook/Use Calls
30 +1. Maintain a stack `activeScopes` of currently "open" reactive scopes
31 +2. Iterate through all blocks in order
32 +3. When entering a block:
33 + - Remove any scopes from `activeScopes` whose fallthrough equals the current block (those scopes have ended)
34 +4. For each instruction in the block:
35 + - If it's a `CallExpression` or `MethodCall` and the callee is a hook or use operator:
36 + - Add all currently active scopes to the `prune` list
37 + - Clear `activeScopes` (these scopes are now marked for pruning)
38 +5. If the block's terminal is a `scope`:
39 + - Push it onto `activeScopes`
40 +
41 +### Phase 2: Prune Identified Scopes
42 +For each block ID in `prune`:
43 +1. Get the scope terminal
44 +2. Check if the scope body is trivial (single instruction + goto to fallthrough):
45 + - If trivial: Convert to `LabelTerminal` (will be removed by `PruneUnusedLabels`)
46 + - If non-trivial: Convert to `PrunedScopeTerminal` (preserves scope info but skips memoization)
47 +
48 +## Key Data Structures
49 +
50 +```typescript
51 +// Stack tracking currently open scopes
52 +activeScopes: Array<{block: BlockId; fallthrough: BlockId}>
53 +
54 +// List of block IDs whose scope terminals should be pruned
55 +prune: Array<BlockId>
56 +
57 +// Terminal types used
58 +LabelTerminal: {kind: 'label', block, fallthrough, id, loc}
59 +PrunedScopeTerminal: {kind: 'pruned-scope', block, fallthrough, scope, id, loc}
60 +ReactiveScopeTerminal: {kind: 'scope', block, fallthrough, scope, id, loc}
61 +```
62 +
63 +## Edge Cases
64 +
65 +### Nested Scopes
66 +When a hook is found in an inner scope, ALL enclosing scopes are also pruned (the hook call would become conditional if any outer scope were memoized).
67 +
68 +### Method Call Hooks
69 +Handles both `CallExpression` (e.g., `useHook(...)`) and `MethodCall` (e.g., `obj.useHook(...)`).
70 +
71 +### Trivial Hook-Only Scopes
72 +If a scope exists just for a hook call (single instruction + goto), it's converted to a `LabelTerminal` which is a simpler structure that gets cleaned up by later passes.
73 +
74 +### Multiple Hooks in Sequence
75 +Once the first hook is encountered, all active scopes are pruned and cleared, so subsequent hooks in outer scopes still work correctly.
76 +
77 +## TODOs
78 +None explicitly marked in the source file.
79 +
80 +## Example
81 +
82 +### Fixture: `nested-scopes-hook-call.js`
83 +
84 +**Input:**
85 +```javascript
86 +function component(props) {
87 + let x = [];
88 + let y = [];
89 + y.push(useHook(props.foo));
90 + x.push(y);
91 + return x;
92 +}
93 +```
94 +
95 +**Before FlattenScopesWithHooksOrUseHIR:**
96 +```
97 +bb0:
98 + [1] Scope @0 [1:22] block=bb6 fallthrough=bb7 // Outer scope for x
99 +bb6:
100 + [2] $22 = Array [] // x = []
101 + [3] StoreLocal x = $22
102 + [4] Scope @1 [4:17] block=bb8 fallthrough=bb9 // Inner scope for y
103 +bb8:
104 + [5] $25 = Array [] // y = []
105 + [6] StoreLocal y = $25
106 + ...
107 + [10] $33 = Call useHook(...) // <-- Hook call here!
108 + [11] MethodCall y.push($33)
109 +```
110 +
111 +**After FlattenScopesWithHooksOrUseHIR:**
112 +```
113 +bb0:
114 + [1] <pruned> Scope @0 [1:22] block=bb6 fallthrough=bb7 // PRUNED
115 +bb6:
116 + [2] $22 = Array []
117 + [3] StoreLocal x = $22
118 + [4] <pruned> Scope @1 [4:17] block=bb8 fallthrough=bb9 // PRUNED
119 +bb8:
120 + [5] $25 = Array []
121 + [6] StoreLocal y = $25
122 + ...
123 + [12] Label block=bb10 fallthrough=bb11 // Hook call converted to label
124 +bb10:
125 + [13] $33 = Call useHook(...)
126 + [14] Goto bb11
127 +...
128 +```
129 +
130 +**Final Output (no memoization):**
131 +```javascript
132 +function component(props) {
133 + const x = [];
134 + const y = [];
135 + y.push(useHook(props.foo));
136 + x.push(y);
137 + return x;
138 +}
139 +```
140 +
141 +Notice that:
142 +1. Both scope @0 and scope @1 are marked as `<pruned>` because the hook call is inside scope @1, which is inside scope @0
143 +2. The final output has no memoization wrappers - just the raw code
compiler/packages/babel-plugin-react-compiler/docs/passes/20-propagateScopeDependenciesHIR.md new
+158
@@ -0,0 +1,158 @@
1 +# propagateScopeDependenciesHIR
2 +
3 +## File
4 +`src/HIR/PropagateScopeDependenciesHIR.ts`
5 +
6 +## Purpose
7 +The `propagateScopeDependenciesHIR` pass is responsible for computing and assigning the **dependencies** for each reactive scope in the compiled function. Dependencies are the external values that a scope reads, which determine when the scope needs to re-execute. This is a critical step for memoization correctness - the compiler must track exactly which values a scope depends on so it can generate proper cache invalidation checks.
8 +
9 +The pass also populates:
10 +- `scope.dependencies` - The set of `ReactiveScopeDependency` objects the scope reads
11 +- `scope.declarations` - Values declared within the scope that are used outside it
12 +
13 +## Input Invariants
14 +- Reactive scopes must be established (pass runs after `BuildReactiveScopeTerminalsHIR`)
15 +- The function must be in SSA form
16 +- `InferMutationAliasingRanges` must have run to establish when values are being mutated
17 +- `InferReactivePlaces` marks which identifiers are reactive
18 +- Scope ranges have been aligned and normalized by earlier passes
19 +
20 +## Output Guarantees
21 +After this pass completes:
22 +
23 +1. Each `ReactiveScope.dependencies` contains the minimal set of dependencies that:
24 + - Were declared before the scope started
25 + - Are read within the scope
26 + - Are not ref values (which are always mutable)
27 + - Are not object methods (which get codegen'd back into object literals)
28 +
29 +2. Each `ReactiveScope.declarations` contains identifiers that:
30 + - Are assigned within the scope
31 + - Are used outside the scope (need to be exposed as scope outputs)
32 +
33 +3. Property load chains are resolved to their root identifiers with paths (e.g., `props.user.name` becomes `{identifier: props, path: ["user", "name"]}`)
34 +
35 +4. Optional chains are handled correctly, distinguishing between `a?.b` and `a.b` access types
36 +
37 +## Algorithm
38 +
39 +### Phase 1: Build Sidemaps
40 +
41 +1. **findTemporariesUsedOutsideDeclaringScope**: Identifies temporaries that are used outside the scope where they were declared (cannot be hoisted/reordered safely)
42 +
43 +2. **collectTemporariesSidemap**: Creates a mapping from temporary IdentifierIds to their source `ReactiveScopeDependency`. For example:
44 + ```
45 + $0 = LoadLocal 'a'
46 + $1 = PropertyLoad $0.'b'
47 + ```
48 + Maps `$1.id` to `{identifier: a, path: [{property: 'b', optional: false}]}`
49 +
50 +3. **collectOptionalChainSidemap**: Traverses optional chain blocks to map temporaries within optional chains to their full optional dependency path
51 +
52 +4. **collectHoistablePropertyLoads**: Uses CFG analysis to determine which property loads can be safely hoisted
53 +
54 +### Phase 2: Collect Dependencies
55 +
56 +The `collectDependencies` function traverses the HIR, maintaining a stack of active scopes:
57 +
58 +1. **Scope Entry/Exit**: When entering a scope terminal, push a new dependency array. When exiting, propagate collected dependencies to parent scopes if valid.
59 +
60 +2. **Instruction Processing**: For each instruction:
61 + - Declare the lvalue with its instruction id and current scope
62 + - Visit operands to record them as potential dependencies
63 + - Handle special cases like `StoreLocal` (tracks reassignments), `Destructure`, `PropertyLoad`, etc.
64 +
65 +3. **Dependency Validation** (`#checkValidDependency`):
66 + - Skip ref values (`isRefValueType`)
67 + - Skip object methods (`isObjectMethodType`)
68 + - Only include if declared before scope start
69 +
70 +### Phase 3: Derive Minimal Dependencies
71 +
72 +For each scope, use `ReactiveScopeDependencyTreeHIR` to:
73 +1. Build a tree from hoistable property loads
74 +2. Add all collected dependencies to the tree
75 +3. Truncate dependencies at their maximal safe-to-evaluate subpath
76 +4. Derive the minimal set (removing redundant nested dependencies)
77 +
78 +## Key Data Structures
79 +
80 +### ReactiveScopeDependency
81 +```typescript
82 +type ReactiveScopeDependency = {
83 + identifier: Identifier; // Root identifier
84 + reactive: boolean; // Whether the value is reactive
85 + path: DependencyPathEntry[]; // Chain of property accesses
86 +}
87 +```
88 +
89 +### DependencyPathEntry
90 +```typescript
91 +type DependencyPathEntry = {
92 + property: PropertyLiteral; // Property name
93 + optional: boolean; // Is this `?.` access?
94 +}
95 +```
96 +
97 +### DependencyCollectionContext
98 +Maintains:
99 +- `#declarations`: Map of DeclarationId to {id, scope} recording where each value was declared
100 +- `#reassignments`: Map of Identifier to latest assignment info
101 +- `#scopes`: Stack of currently active ReactiveScopes
102 +- `#dependencies`: Stack of dependency arrays (one per active scope)
103 +- `#temporaries`: Sidemap for resolving property loads
104 +
105 +### ReactiveScopeDependencyTreeHIR
106 +A tree structure for efficient dependency deduplication that stores hoistable objects, tracks access types, and computes minimal dependencies.
107 +
108 +## Edge Cases
109 +
110 +### Values Used Outside Declaring Scope
111 +If a temporary is used outside its declaring scope, it cannot be tracked in the sidemap because reordering the read would be invalid.
112 +
113 +### Ref.current Access
114 +Accessing `ref.current` is treated specially - the dependency is truncated to just `ref`.
115 +
116 +### Optional Chains
117 +Optional chains like `a?.b?.c` produce different dependency paths than `a.b.c`. The pass distinguishes them and may merge optional loads into unconditional ones when control flow proves the object is non-null.
118 +
119 +### Inner Functions
120 +Dependencies from inner functions are collected recursively but with special handling for context variables.
121 +
122 +### Phi Nodes
123 +When a value comes from multiple control flow paths, optional chain dependencies from phi operands are also visited.
124 +
125 +## TODOs
126 +1. Line 374-375: `// TODO(mofeiZ): understand optional chaining` - More documentation needed for optional chain handling
127 +
128 +## Example
129 +
130 +### Fixture: `reactive-control-dependency-if.js`
131 +
132 +**Input:**
133 +```javascript
134 +function Component(props) {
135 + let x;
136 + if (props.cond) {
137 + x = 1;
138 + } else {
139 + x = 2;
140 + }
141 + return [x];
142 +}
143 +```
144 +
145 +**Before PropagateScopeDependenciesHIR:**
146 +```
147 +Scope scope @0 [12:15] dependencies=[] declarations=[] reassignments=[] block=bb9
148 +```
149 +
150 +**After PropagateScopeDependenciesHIR:**
151 +```
152 +Scope scope @0 [12:15] dependencies=[x$24:TPrimitive] declarations=[$26_@0] reassignments=[] block=bb9
153 +```
154 +
155 +The pass identified that:
156 +- The scope at `[x]` depends on `x$24` (the phi node result from the if/else branches)
157 +- Even though `x` is assigned to constants (1 or 2), its value depends on the reactive control flow condition `props.cond`
158 +- The scope declares `$26_@0` (the array output)
compiler/packages/babel-plugin-react-compiler/docs/passes/21-buildReactiveFunction.md new
+180
@@ -0,0 +1,180 @@
1 +# buildReactiveFunction
2 +
3 +## File
4 +`src/ReactiveScopes/BuildReactiveFunction.ts`
5 +
6 +## Purpose
7 +The `buildReactiveFunction` pass converts the compiler's HIR (High-level Intermediate Representation) from a **Control Flow Graph (CFG)** representation to a **tree-based ReactiveFunction** representation that is closer to an AST. This is a critical transformation in the React Compiler pipeline that:
8 +
9 +1. **Restores control flow constructs** - Reconstructs `if`, `while`, `for`, `switch`, and other control flow statements from the CFG's basic blocks and terminals
10 +2. **Eliminates phi nodes** - Replaces SSA phi nodes with compound value expressions (ternaries, logical expressions, sequence expressions)
11 +3. **Handles labeled break/continue** - Tracks control flow targets to emit explicit labeled `break` and `continue` statements when needed
12 +4. **Preserves reactive scope information** - Scope terminals are converted to `ReactiveScopeBlock` nodes in the tree
13 +
14 +## Input Invariants
15 +- HIR is in SSA form (variables have been renamed with unique identifiers)
16 +- Basic blocks are connected (valid predecessor/successor relationships)
17 +- Each block ends with a valid terminal
18 +- Phi nodes exist at merge points for values from different control flow paths
19 +- Reactive scopes have been constructed (`scope` terminals exist)
20 +- Scope dependencies are computed (`PropagateScopeDependenciesHIR` has run)
21 +
22 +## Output Guarantees
23 +- **Tree structure** - The output is a `ReactiveFunction` with a `body: ReactiveBlock` containing a tree of `ReactiveStatement` nodes
24 +- **No CFG structure** - Basic blocks are eliminated; control flow is represented through nested reactive terminals
25 +- **No phi nodes** - Value merges are represented as `ConditionalExpression`, `LogicalExpression`, or `SequenceExpression` values
26 +- **Labels emitted for all control flow** - Every terminal that can be a break/continue target has a label; unnecessary labels are removed by subsequent `PruneUnusedLabels` pass
27 +- **Each block emitted exactly once** - A block cannot be generated twice
28 +- **Scope blocks preserved** - `scope` terminals become `ReactiveScopeBlock` nodes
29 +
30 +## Algorithm
31 +
32 +### Core Classes
33 +
34 +1. **`Driver`** - Traverses blocks and emits ReactiveBlock arrays
35 +2. **`Context`** - Tracks state:
36 + - `emitted: Set<BlockId>` - Which blocks have been generated
37 + - `#scheduled: Set<BlockId>` - Blocks that will be emitted by parent constructs
38 + - `#controlFlowStack: Array<ControlFlowTarget>` - Stack of active break/continue targets
39 + - `scopeFallthroughs: Set<BlockId>` - Fallthroughs for scope blocks
40 +
41 +### Traversal Strategy
42 +
43 +1. Start at the entry block and call `traverseBlock(entryBlock)`
44 +2. For each block:
45 + - Emit all instructions as `ReactiveInstructionStatement`
46 + - Process the terminal based on its kind
47 +
48 +### Terminal Processing
49 +
50 +**Simple Terminals:**
51 +- `return`, `throw` - Emit directly as `ReactiveTerminal`
52 +- `unreachable` - No-op
53 +
54 +**Control Flow Terminals:**
55 +- `if` - Schedule fallthrough, recursively traverse consequent/alternate, emit `ReactiveIfTerminal`
56 +- `while`, `do-while`, `for`, `for-of`, `for-in` - Use `scheduleLoop()` which tracks continue targets
57 +- `switch` - Process cases in reverse order
58 +- `label` - Schedule fallthrough, traverse inner block
59 +
60 +**Value Terminals (expressions that produce values):**
61 +- `ternary`, `logical`, `optional`, `sequence` - Produce `ReactiveValue` compound expressions
62 +
63 +**Break/Continue:**
64 +- `goto` with `GotoVariant.Break` - Determine if break is implicit, unlabeled, or labeled
65 +- `goto` with `GotoVariant.Continue` - Determine continue type
66 +
67 +**Scope Terminals:**
68 +- `scope`, `pruned-scope` - Schedule fallthrough, traverse inner block, emit as `ReactiveScopeBlock`
69 +
70 +## Key Data Structures
71 +
72 +### ReactiveFunction
73 +```typescript
74 +type ReactiveFunction = {
75 + loc: SourceLocation;
76 + id: ValidIdentifierName | null;
77 + params: Array<Place | SpreadPattern>;
78 + generator: boolean;
79 + async: boolean;
80 + body: ReactiveBlock;
81 + env: Environment;
82 + directives: Array<string>;
83 +};
84 +```
85 +
86 +### ReactiveBlock
87 +```typescript
88 +type ReactiveBlock = Array<ReactiveStatement>;
89 +```
90 +
91 +### ReactiveStatement
92 +```typescript
93 +type ReactiveStatement =
94 + | ReactiveInstructionStatement // {kind: 'instruction', instruction}
95 + | ReactiveTerminalStatement // {kind: 'terminal', terminal, label}
96 + | ReactiveScopeBlock // {kind: 'scope', scope, instructions}
97 + | PrunedReactiveScopeBlock; // {kind: 'pruned-scope', ...}
98 +```
99 +
100 +### ReactiveValue (for compound expressions)
101 +```typescript
102 +type ReactiveValue =
103 + | InstructionValue // Regular instruction values
104 + | ReactiveLogicalValue // a && b, a || b, a ?? b
105 + | ReactiveSequenceValue // (a, b, c)
106 + | ReactiveTernaryValue // a ? b : c
107 + | ReactiveOptionalCallValue; // a?.b()
108 +```
109 +
110 +### ControlFlowTarget
111 +```typescript
112 +type ControlFlowTarget =
113 + | {type: 'if'; block: BlockId; id: number}
114 + | {type: 'switch'; block: BlockId; id: number}
115 + | {type: 'case'; block: BlockId; id: number}
116 + | {type: 'loop'; block: BlockId; continueBlock: BlockId; ...};
117 +```
118 +
119 +## Edge Cases
120 +
121 +### Nested Control Flow
122 +The scheduling mechanism handles arbitrarily nested control flow by pushing/popping from the control flow stack.
123 +
124 +### Value Blocks with Complex Expressions
125 +`SequenceExpression` handles cases where value blocks contain multiple instructions.
126 +
127 +### Scope Fallthroughs
128 +Breaks to scope fallthroughs are treated as implicit (no explicit break needed).
129 +
130 +### Catch Handlers
131 +Scheduled specially via `scheduleCatchHandler()` to prevent re-emission.
132 +
133 +### Unreachable Blocks
134 +The `reachable()` check prevents emitting unreachable blocks.
135 +
136 +## TODOs
137 +The code contains several `CompilerError.throwTodo()` calls for unsupported patterns:
138 +1. Optional chaining test blocks must end in `branch`
139 +2. Logical expression test blocks must end in `branch`
140 +3. Support for value blocks within try/catch statements
141 +4. Support for labeled statements combined with value blocks
142 +
143 +## Example
144 +
145 +### Fixture: `ternary-expression.js`
146 +
147 +**Input:**
148 +```javascript
149 +function ternary(props) {
150 + const a = props.a && props.b ? props.c || props.d : (props.e ?? props.f);
151 + const b = props.a ? (props.b && props.c ? props.d : props.e) : props.f;
152 + return a ? b : null;
153 +}
154 +```
155 +
156 +**HIR (CFG with many basic blocks):**
157 +The HIR contains 33 basic blocks with `Ternary`, `Logical`, `Branch`, and `Goto` terminals, plus phi nodes at merge points.
158 +
159 +**ReactiveFunction Output (Tree):**
160 +```
161 +function ternary(props$62{reactive}) {
162 + [1] $84 = Ternary
163 + Sequence
164 + [2] $66 = Logical
165 + Sequence [...]
166 + && Sequence [...]
167 + ?
168 + Sequence [...] // props.c || props.d
169 + :
170 + Sequence [...] // props.e ?? props.f
171 + [40] StoreLocal a$99 = $98
172 + ...
173 + [82] return $145
174 +}
175 +```
176 +
177 +The transformation eliminates:
178 +- 33 basic blocks reduced to a single tree
179 +- Phi nodes replaced with nested `Ternary` and `Logical` value expressions
180 +- CFG edges replaced with tree nesting
compiler/packages/babel-plugin-react-compiler/docs/passes/22-pruneUnusedLabels.md new
+145
@@ -0,0 +1,145 @@
1 +# pruneUnusedLabels
2 +
3 +## File
4 +`src/ReactiveScopes/PruneUnusedLabels.ts`
5 +
6 +## Purpose
7 +The `pruneUnusedLabels` pass optimizes control flow by:
8 +
9 +1. **Flattening labeled terminals** where the label is not reachable via a `break` or `continue` statement
10 +2. **Marking labels as implicit** for terminals where the label exists but is never targeted
11 +
12 +This pass removes unnecessary labeled blocks that were introduced during compilation but serve no control flow purpose in the final output. JavaScript labeled statements are only needed when there is a corresponding `break label` or `continue label` that targets them.
13 +
14 +## Input Invariants
15 +- The input is a `ReactiveFunction` (after conversion from HIR)
16 +- All `break` and `continue` terminals have:
17 + - A `target` (BlockId) indicating which label they jump to
18 + - A `targetKind` that is one of: `'implicit'`, `'labeled'`, or `'unlabeled'`
19 +- Each `ReactiveTerminalStatement` has an optional `label` field containing `id` and `implicit`
20 +- The pass runs after `assertWellFormedBreakTargets` which validates break/continue targets
21 +
22 +## Output Guarantees
23 +- Labeled terminals where the label is unreachable are flattened into their parent block
24 +- When flattening, trailing unlabeled `break` statements (that would just fall through) are removed
25 +- Labels that exist but are never targeted have their `implicit` flag set to `true`
26 +- Control flow semantics are preserved - only structurally unnecessary labels are removed
27 +
28 +## Algorithm
29 +
30 +The pass uses a two-phase approach with a single traversal:
31 +
32 +**Phase 1: Collect reachable labels**
33 +```typescript
34 +if ((terminal.kind === 'break' || terminal.kind === 'continue') &&
35 + terminal.targetKind === 'labeled') {
36 + state.add(terminal.target); // Mark this label as reachable
37 +}
38 +```
39 +
40 +**Phase 2: Transform terminals**
41 +```typescript
42 +const isReachableLabel = stmt.label !== null && state.has(stmt.label.id);
43 +
44 +if (stmt.terminal.kind === 'label' && !isReachableLabel) {
45 + // Flatten: extract block contents, removing trailing unlabeled break
46 + const block = [...stmt.terminal.block];
47 + const last = block.at(-1);
48 + if (last?.kind === 'terminal' && last.terminal.kind === 'break' &&
49 + last.terminal.target === null) {
50 + block.pop(); // Remove trailing break
51 + }
52 + return {kind: 'replace-many', value: block};
53 +} else {
54 + if (!isReachableLabel && stmt.label != null) {
55 + stmt.label.implicit = true; // Mark as implicit
56 + }
57 + return {kind: 'keep'};
58 +}
59 +```
60 +
61 +## Edge Cases
62 +
63 +### Trailing Break Removal
64 +When flattening a labeled block, if the last statement is an unlabeled break (`target === null`), it is removed since it would just fall through anyway.
65 +
66 +### Implicit vs Labeled Breaks
67 +Only breaks with `targetKind === 'labeled'` count toward label reachability. Implicit breaks (fallthrough) and unlabeled breaks don't make a label "used".
68 +
69 +### Continue Statements
70 +Both `break` and `continue` with labeled targets mark the label as reachable.
71 +
72 +### Non-Label Terminals with Labels
73 +Other terminal types (like `if`, `while`, `for`) can also have labels. If unreachable, these labels are marked implicit but the terminal is not flattened.
74 +
75 +## TODOs
76 +None in the source file.
77 +
78 +## Example
79 +
80 +### Fixture: `unconditional-break-label.js`
81 +
82 +**Input:**
83 +```javascript
84 +function foo(a) {
85 + let x = 0;
86 + bar: {
87 + x = 1;
88 + break bar;
89 + }
90 + return a + x;
91 +}
92 +```
93 +
94 +**Output (after full compilation):**
95 +```javascript
96 +function foo(a) {
97 + return a + 1;
98 +}
99 +```
100 +
101 +The labeled block `bar: { ... }` is removed because after the pass runs, constant propagation and dead code elimination further simplify the code.
102 +
103 +### Fixture: `conditional-break-labeled.js`
104 +
105 +**Input:**
106 +```javascript
107 +function Component(props) {
108 + const a = [];
109 + a.push(props.a);
110 + label: {
111 + if (props.b) {
112 + break label;
113 + }
114 + a.push(props.c);
115 + }
116 + a.push(props.d);
117 + return a;
118 +}
119 +```
120 +
121 +**Output:**
122 +```javascript
123 +function Component(props) {
124 + const $ = _c(5);
125 + let a;
126 + if ($[0] !== props.a || $[1] !== props.b ||
127 + $[2] !== props.c || $[3] !== props.d) {
128 + a = [];
129 + a.push(props.a);
130 + bb0: {
131 + if (props.b) {
132 + break bb0;
133 + }
134 + a.push(props.c);
135 + }
136 + a.push(props.d);
137 + // ... cache updates
138 + } else {
139 + a = $[4];
140 + }
141 + return a;
142 +}
143 +```
144 +
145 +The labeled block `bb0: { ... }` is preserved because the `break bb0` inside the conditional targets this label.
compiler/packages/babel-plugin-react-compiler/docs/passes/23-pruneNonEscapingScopes.md new
+130
@@ -0,0 +1,130 @@
1 +# pruneNonEscapingScopes
2 +
3 +## File
4 +`src/ReactiveScopes/PruneNonEscapingScopes.ts`
5 +
6 +## Purpose
7 +This pass prunes (removes) reactive scopes whose outputs do not "escape" the component and therefore do not need to be memoized. A value "escapes" in two ways:
8 +
9 +1. **Returned from the function** - The value is directly returned or transitively aliased by a return value
10 +2. **Passed to a hook** - Any value passed as an argument to a hook may be stored by React internally (e.g., the closure passed to `useEffect`)
11 +
12 +The key insight is that values which never escape the component boundary can be safely recreated on each render without affecting the behavior of consumers.
13 +
14 +## Input Invariants
15 +- The input is a `ReactiveFunction` after scope blocks have been identified
16 +- Reactive scopes have been assigned to instructions
17 +- The pass runs after `BuildReactiveFunction` and `PruneUnusedLabels`, before `PruneNonReactiveDependencies`
18 +
19 +## Output Guarantees
20 +- **Scopes with non-escaping outputs are removed** - Their instructions are inlined back into the parent scope/function body
21 +- **Scopes with escaping outputs are retained** - Values that escape via return or hook arguments remain memoized
22 +- **Transitive dependencies of escaping scopes are preserved** - If an escaping scope depends on a non-escaping value, that value's scope is also retained to prevent unnecessary invalidation
23 +- **`FinishMemoize` instructions are marked `pruned=true`** - When a scope is pruned, the associated memoization instructions are flagged
24 +
25 +## Algorithm
26 +
27 +### Phase 1: Build the Dependency Graph
28 +Using `CollectDependenciesVisitor`, build:
29 +- **Identifier nodes** - Each node tracks memoization level, dependencies, scopes, and whether ultimately memoized
30 +- **Scope nodes** - Each scope tracks its dependencies
31 +- **Escaping values** - Identifiers that escape via return or hook arguments
32 +
33 +### Phase 2: Classify Memoization Levels
34 +Each instruction value is classified:
35 +- `Memoized`: Arrays, objects, function calls, `new` expressions - always potentially aliasing
36 +- `Conditional`: Conditional/logical expressions, property loads - memoized only if dependencies are memoized
37 +- `Unmemoized`: JSX elements (when `memoizeJsxElements` is false), DeclareLocal
38 +- `Never`: Primitives, LoadGlobal, binary/unary expressions - can be cheaply compared
39 +
40 +### Phase 3: Compute Memoized Identifiers
41 +`computeMemoizedIdentifiers()` performs a graph traversal starting from escaping values:
42 +- For each escaping value, recursively visit its dependencies
43 +- Mark values and their scopes based on memoization level
44 +- When marking a scope, force-memoize all its dependencies
45 +
46 +### Phase 4: Prune Scopes
47 +`PruneScopesTransform` visits each scope block:
48 +- If any scope output is in the memoized set, keep the scope
49 +- If no outputs are memoized, replace the scope block with its inlined instructions
50 +
51 +## Edge Cases
52 +
53 +### Interleaved Mutations
54 +```javascript
55 +const a = [props.a]; // independently memoizable, non-escaping
56 +const b = [];
57 +const c = {};
58 +c.a = a; // c captures a, but c doesn't escape
59 +b.push(props.b); // b escapes via return
60 +return b;
61 +```
62 +Here `a` does not directly escape, but it is a dependency of the scope containing `b`. The algorithm correctly identifies that `a`'s scope must be preserved.
63 +
64 +### Hook Arguments Escape
65 +Values passed to hooks are treated as escaping because hooks may store references internally.
66 +
67 +### JSX Special Handling
68 +JSX elements are marked as `Unmemoized` by default because React.memo() can handle dynamic memoization.
69 +
70 +### noAlias Functions
71 +If a function signature indicates `noAlias === true`, its arguments are not treated as escaping.
72 +
73 +### Reassignments
74 +When a scope reassigns a variable, the scope is added as a dependency of that variable.
75 +
76 +## TODOs
77 +None explicitly in the source file.
78 +
79 +## Example
80 +
81 +### Fixture: `escape-analysis-non-escaping-interleaved-allocating-dependency.js`
82 +
83 +**Input:**
84 +```javascript
85 +function Component(props) {
86 + const a = [props.a];
87 +
88 + const b = [];
89 + const c = {};
90 + c.a = a;
91 + b.push(props.b);
92 +
93 + return b;
94 +}
95 +```
96 +
97 +**Output:**
98 +```javascript
99 +function Component(props) {
100 + const $ = _c(5);
101 + let t0;
102 + if ($[0] !== props.a) {
103 + t0 = [props.a];
104 + $[0] = props.a;
105 + $[1] = t0;
106 + } else {
107 + t0 = $[1];
108 + }
109 + const a = t0; // a is memoized even though it doesn't escape directly
110 +
111 + let b;
112 + if ($[2] !== a || $[3] !== props.b) {
113 + b = [];
114 + const c = {}; // c is NOT memoized - it doesn't escape
115 + c.a = a;
116 + b.push(props.b);
117 + $[2] = a;
118 + $[3] = props.b;
119 + $[4] = b;
120 + } else {
121 + b = $[4];
122 + }
123 + return b;
124 +}
125 +```
126 +
127 +Key observations:
128 +- `a` is memoized because it's a dependency of the scope containing `b`
129 +- `c` is not separately memoized because it doesn't escape
130 +- `b` is memoized because it's returned
compiler/packages/babel-plugin-react-compiler/docs/passes/24-pruneNonReactiveDependencies.md new
+138
@@ -0,0 +1,138 @@
1 +# pruneNonReactiveDependencies
2 +
3 +## File
4 +`src/ReactiveScopes/PruneNonReactiveDependencies.ts`
5 +
6 +## Purpose
7 +This pass removes dependencies from reactive scopes that are guaranteed to be **non-reactive** (i.e., their values cannot change between renders). This optimization reduces unnecessary memoization invalidations by ensuring scopes only depend on values that can actually change.
8 +
9 +The pass complements `PropagateScopeDependencies`, which infers dependencies without considering reactivity. This subsequent pruning step filters out dependencies that are semantically constant.
10 +
11 +## Input Invariants
12 +- The function has been converted to a ReactiveFunction structure
13 +- `InferReactivePlaces` has annotated places with `{reactive: true}` where values can change
14 +- Each `ReactiveScopeBlock` has a `scope.dependencies` set populated by `PropagateScopeDependenciesHIR`
15 +- Type inference has run, so identifiers have type information for `isStableType` checks
16 +
17 +## Output Guarantees
18 +- **Non-reactive dependencies removed**: All dependencies in `scope.dependencies` are reactive after this pass
19 +- **Scope outputs marked reactive if needed**: If a scope has any reactive dependencies remaining, all its outputs are marked reactive
20 +- **Stable types remain non-reactive through property loads**: When loading properties from stable types (like `useReducer` dispatch functions), the result is not added to the reactive set
21 +
22 +## Algorithm
23 +
24 +### Phase 1: Collect Reactive Identifiers
25 +The `collectReactiveIdentifiers` helper builds the initial set of reactive identifiers by:
26 +1. Visiting all places in the ReactiveFunction
27 +2. Adding any place marked `{reactive: true}` to the set
28 +3. For pruned scopes, adding declarations that are not primitives and not stable ref types
29 +
30 +### Phase 2: Propagate Reactivity and Prune Dependencies
31 +The main `Visitor` class traverses the ReactiveFunction and:
32 +
33 +1. **For Instructions** - Propagates reactivity through data flow:
34 + - `LoadLocal`: If source is reactive, mark the lvalue as reactive
35 + - `StoreLocal`: If source value is reactive, mark both the local variable and lvalue as reactive
36 + - `Destructure`: If source is reactive, mark all pattern operands as reactive (except stable types)
37 + - `PropertyLoad`: If object is reactive AND result is not a stable type, mark result as reactive
38 + - `ComputedLoad`: If object OR property is reactive, mark result as reactive
39 +
40 +2. **For Scopes** - Prunes non-reactive dependencies and propagates outputs:
41 + - Delete each dependency from `scope.dependencies` if its identifier is not in the reactive set
42 + - If any dependencies remain after pruning, mark all scope outputs as reactive
43 +
44 +### Key Insight: Stable Types
45 +The pass leverages `isStableType` to prevent reactivity from flowing through certain React-provided stable values:
46 +
47 +```typescript
48 +function isStableType(id: Identifier): boolean {
49 + return (
50 + isSetStateType(id) || // useState setter
51 + isSetActionStateType(id) || // useActionState setter
52 + isDispatcherType(id) || // useReducer dispatcher
53 + isUseRefType(id) || // useRef result
54 + isStartTransitionType(id) ||// useTransition startTransition
55 + isSetOptimisticType(id) // useOptimistic setter
56 + );
57 +}
58 +```
59 +
60 +## Edge Cases
61 +
62 +### Unmemoized Values Spanning Hook Calls
63 +A value created before a hook call and mutated after cannot be memoized. However, if it's non-reactive, it still should not appear as a dependency of downstream scopes.
64 +
65 +### Stable Types from Reactive Containers
66 +When `useReducer` returns `[state, dispatch]`, `state` is reactive but `dispatch` is stable. The pass correctly handles this.
67 +
68 +### Pruned Scopes with Reactive Content
69 +The `CollectReactiveIdentifiers` pass also examines pruned scopes and adds their non-primitive, non-stable-ref declarations to the reactive set.
70 +
71 +### Transitive Reactivity Through Scopes
72 +When a scope retains at least one reactive dependency, ALL its outputs become reactive.
73 +
74 +## TODOs
75 +None in the source file.
76 +
77 +## Example
78 +
79 +### Fixture: `unmemoized-nonreactive-dependency-is-pruned-as-dependency.js`
80 +
81 +**Input:**
82 +```javascript
83 +function Component(props) {
84 + const x = [];
85 + useNoAlias();
86 + mutate(x);
87 +
88 + return <div>{x}</div>;
89 +}
90 +```
91 +
92 +**Before PruneNonReactiveDependencies:**
93 +```
94 +scope @2 dependencies=[x$15_@0:TObject<BuiltInArray>] declarations=[$23_@2]
95 +```
96 +
97 +**After PruneNonReactiveDependencies:**
98 +```
99 +scope @2 dependencies=[] declarations=[$23_@2]
100 +```
101 +
102 +The dependency on `x` is removed because `x` is created locally and therefore non-reactive.
103 +
104 +### Fixture: `useReducer-returned-dispatcher-is-non-reactive.js`
105 +
106 +**Input:**
107 +```javascript
108 +function f() {
109 + const [state, dispatch] = useReducer();
110 +
111 + const onClick = () => {
112 + dispatch();
113 + };
114 +
115 + return <div onClick={onClick} />;
116 +}
117 +```
118 +
119 +**Generated Code:**
120 +```javascript
121 +function f() {
122 + const $ = _c(1);
123 + const [, dispatch] = useReducer();
124 + let t0;
125 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
126 + const onClick = () => {
127 + dispatch();
128 + };
129 + t0 = <div onClick={onClick} />;
130 + $[0] = t0;
131 + } else {
132 + t0 = $[0];
133 + }
134 + return t0;
135 +}
136 +```
137 +
138 +The `onClick` function only captures `dispatch`, which is a stable type. Therefore, `onClick` is non-reactive, and the JSX element can be memoized with zero dependencies.
compiler/packages/babel-plugin-react-compiler/docs/passes/25-pruneUnusedScopes.md new
+111
@@ -0,0 +1,111 @@
1 +# pruneUnusedScopes
2 +
3 +## File
4 +`src/ReactiveScopes/PruneUnusedScopes.ts`
5 +
6 +## Purpose
7 +This pass converts reactive scopes that have no meaningful outputs into "pruned scopes". A pruned scope is no longer memoized - its instructions are executed unconditionally on every render. This optimization removes unnecessary memoization overhead for scopes that don't produce values that need to be cached.
8 +
9 +## Input Invariants
10 +- The input is a `ReactiveFunction` that has already been transformed into reactive scope form
11 +- Scopes have been created and have `declarations`, `reassignments`, and potentially `earlyReturnValue` populated
12 +- The pass is called after:
13 + - `pruneUnusedLabels` - cleans up unnecessary labels
14 + - `pruneNonEscapingScopes` - removes scopes whose outputs don't escape
15 + - `pruneNonReactiveDependencies` - removes non-reactive dependencies from scopes
16 +- Scopes may already be marked as pruned by earlier passes
17 +
18 +## Output Guarantees
19 +Scopes that meet ALL of the following criteria are converted to `pruned-scope`:
20 +- No return statement within the scope
21 +- No reassignments (`scope.reassignments.size === 0`)
22 +- Either no declarations (`scope.declarations.size === 0`), OR all declarations "bubbled up" from inner scopes
23 +
24 +Pruned scopes:
25 +- Keep their original scope metadata (for debugging/tracking)
26 +- Keep their instructions intact
27 +- Will be executed unconditionally during codegen (no memoization check)
28 +
29 +## Algorithm
30 +
31 +The pass uses the visitor pattern with `ReactiveFunctionTransform`:
32 +
33 +1. **State Tracking**: A `State` object tracks whether a return statement was encountered:
34 + ```typescript
35 + type State = {
36 + hasReturnStatement: boolean;
37 + };
38 + ```
39 +
40 +2. **Terminal Visitor** (`visitTerminal`): Checks if any terminal is a `return` statement
41 +
42 +3. **Scope Transform** (`transformScope`): For each scope:
43 + - Creates a fresh state for this scope
44 + - Recursively visits the scope's contents
45 + - Checks pruning criteria:
46 + - `!scopeState.hasReturnStatement` - no early return
47 + - `scope.reassignments.size === 0` - no reassignments
48 + - `scope.declarations.size === 0` OR `!hasOwnDeclaration(scopeBlock)` - no outputs
49 +
50 +4. **hasOwnDeclaration Helper**: Determines if a scope has "own" declarations vs declarations propagated from nested scopes
51 +
52 +## Edge Cases
53 +
54 +### Return Statements
55 +Scopes containing return statements are preserved because early returns need memoization to avoid re-executing the return check on every render.
56 +
57 +### Bubbled-Up Declarations
58 +When nested scopes are flattened or merged, their declarations may be propagated to parent scopes. The `hasOwnDeclaration` check ensures that parent scopes with only inherited declarations can still be pruned.
59 +
60 +### Reassignments
61 +Scopes with reassignments are kept because the reassignment represents a side effect that needs to be tracked for memoization.
62 +
63 +### Already-Pruned Scopes
64 +The pass operates on `ReactiveScopeBlock` (kind: 'scope'), not `PrunedReactiveScopeBlock`. Scopes already pruned by earlier passes are not revisited.
65 +
66 +### Interaction with Subsequent Passes
67 +The `MergeReactiveScopesThatInvalidateTogether` pass explicitly handles pruned scopes - it does not merge across them.
68 +
69 +## TODOs
70 +None in the source file.
71 +
72 +## Example
73 +
74 +### Fixture: `prune-scopes-whose-deps-invalidate-array.js`
75 +
76 +**Input:**
77 +```javascript
78 +function Component(props) {
79 + const x = [];
80 + useHook();
81 + x.push(props.value);
82 + const y = [x];
83 + return [y];
84 +}
85 +```
86 +
87 +What happens:
88 +- The scope for `x` cannot be memoized because `useHook()` is called inside it
89 +- `FlattenScopesWithHooksOrUseHIR` marks scope @0 as `pruned-scope`
90 +- `PruneUnusedScopes` doesn't change it further since it's already pruned
91 +
92 +**Output (no memoization for x):**
93 +```javascript
94 +function Component(props) {
95 + const x = [];
96 + useHook();
97 + x.push(props.value);
98 + const y = [x];
99 + return [y];
100 +}
101 +```
102 +
103 +### Key Insight
104 +
105 +The `pruneUnusedScopes` pass is part of a multi-pass pruning strategy:
106 +1. `FlattenScopesWithHooksOrUseHIR` - Prunes scopes that contain hook/use calls
107 +2. `pruneNonEscapingScopes` - Prunes scopes whose outputs don't escape
108 +3. `pruneNonReactiveDependencies` - Removes non-reactive dependencies
109 +4. **`pruneUnusedScopes`** - Prunes scopes with no remaining outputs
110 +
111 +This pass acts as a cleanup for scopes that became "empty" after previous pruning passes removed their outputs.
compiler/packages/babel-plugin-react-compiler/docs/passes/26-mergeReactiveScopesThatInvalidateTogether.md new
+213
@@ -0,0 +1,213 @@
1 +# mergeReactiveScopesThatInvalidateTogether
2 +
3 +## File
4 +`src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts`
5 +
6 +## Purpose
7 +This pass is an optimization that reduces memoization overhead in the compiled output by merging reactive scopes that will always invalidate together. The pass operates on the ReactiveFunction representation and works in two main scenarios:
8 +
9 +1. **Consecutive Scopes**: When two scopes appear sequentially in the same reactive block with identical dependencies (or where the output of the first scope is the sole input to the second), they are merged into a single scope. This reduces the number of memo cache slots used and eliminates redundant dependency comparisons.
10 +
11 +2. **Nested Scopes**: When an inner scope has the same dependencies as its parent scope, the inner scope is flattened into the parent. Since PropagateScopeDependencies propagates dependencies upward, nested scopes can only have equal or fewer dependencies than their parents, never more. When they're equal, the inner scope always invalidates with the parent, making it safe and beneficial to flatten.
12 +
13 +## Input Invariants
14 +- The ReactiveFunction has already undergone scope dependency propagation (via `PropagateScopeDependencies`)
15 +- The function has been pruned of unused scopes (via `pruneNonReactiveDependencies` and `pruneUnusedScopes`)
16 +- Scopes have valid `dependencies`, `declarations`, `range`, and `reassignments` fields
17 +- The ReactiveFunction is in a valid structural state with properly formed blocks and instructions
18 +
19 +## Output Guarantees
20 +- **Fewer scopes**: Consecutive and nested scopes with identical dependencies are merged
21 +- **Valid scope ranges**: Merged scopes have their `range.end` updated to cover all merged instructions
22 +- **Updated declarations**: Scope declarations are updated to remove any that are no longer used after the merged scope
23 +- **Merged scope tracking**: The `scope.merged` set tracks which scope IDs were merged into each surviving scope
24 +- **Preserved semantics**: Only safe-to-memoize intermediate instructions are absorbed into merged scopes
25 +
26 +## Algorithm
27 +
28 +The pass operates in multiple phases:
29 +
30 +### Phase 1: Find Last Usage
31 +A visitor (`FindLastUsageVisitor`) collects the last usage instruction ID for each declaration:
32 +
33 +```typescript
34 +class FindLastUsageVisitor extends ReactiveFunctionVisitor<void> {
35 + lastUsage: Map<DeclarationId, InstructionId> = new Map();
36 +
37 + override visitPlace(id: InstructionId, place: Place, _state: void): void {
38 + const previousUsage = this.lastUsage.get(place.identifier.declarationId);
39 + const lastUsage =
40 + previousUsage !== undefined
41 + ? makeInstructionId(Math.max(previousUsage, id))
42 + : id;
43 + this.lastUsage.set(place.identifier.declarationId, lastUsage);
44 + }
45 +}
46 +```
47 +
48 +### Phase 2: Transform (Nested Scope Flattening)
49 +The `transformScope` method flattens nested scopes with identical dependencies:
50 +
51 +```typescript
52 +override transformScope(
53 + scopeBlock: ReactiveScopeBlock,
54 + state: ReactiveScopeDependencies | null,
55 +): Transformed<ReactiveStatement> {
56 + this.visitScope(scopeBlock, scopeBlock.scope.dependencies);
57 + if (
58 + state !== null &&
59 + areEqualDependencies(state, scopeBlock.scope.dependencies)
60 + ) {
61 + return {kind: 'replace-many', value: scopeBlock.instructions};
62 + } else {
63 + return {kind: 'keep'};
64 + }
65 +}
66 +```
67 +
68 +### Phase 3: Visit Block (Consecutive Scope Merging)
69 +Within `visitBlock`, the pass:
70 +1. First traverses nested blocks recursively
71 +2. Iterates through instructions, tracking merge candidates
72 +3. Determines if consecutive scopes can merge based on:
73 + - Identical dependencies, OR
74 + - Output of first scope is input to second scope (with always-invalidating types)
75 +4. Collects intermediate lvalues and ensures they're only used by the next scope
76 +5. Merges eligible scopes by combining instructions and updating range/declarations
77 +
78 +### Key Merging Conditions (`canMergeScopes`):
79 +```typescript
80 +function canMergeScopes(
81 + current: ReactiveScopeBlock,
82 + next: ReactiveScopeBlock,
83 + temporaries: Map<DeclarationId, DeclarationId>,
84 +): boolean {
85 + // Don't merge scopes with reassignments
86 + if (current.scope.reassignments.size !== 0 || next.scope.reassignments.size !== 0) {
87 + return false;
88 + }
89 + // Merge scopes whose dependencies are identical
90 + if (areEqualDependencies(current.scope.dependencies, next.scope.dependencies)) {
91 + return true;
92 + }
93 + // Merge scopes where outputs of previous are inputs of next
94 + // (with always-invalidating type check)
95 + // ...
96 +}
97 +```
98 +
99 +### Always-Invalidating Types:
100 +```typescript
101 +export function isAlwaysInvalidatingType(type: Type): boolean {
102 + switch (type.kind) {
103 + case 'Object': {
104 + switch (type.shapeId) {
105 + case BuiltInArrayId:
106 + case BuiltInObjectId:
107 + case BuiltInFunctionId:
108 + case BuiltInJsxId: {
109 + return true;
110 + }
111 + }
112 + break;
113 + }
114 + case 'Function': {
115 + return true;
116 + }
117 + }
118 + return false;
119 +}
120 +```
121 +
122 +## Edge Cases
123 +
124 +### Terminals
125 +The pass does not merge across terminals (control flow boundaries).
126 +
127 +### Pruned Scopes
128 +Merging stops at pruned scopes.
129 +
130 +### Reassignments
131 +Scopes containing reassignments cannot be merged (side-effect ordering concerns).
132 +
133 +### Intermediate Reassignments
134 +Non-const StoreLocal instructions between scopes prevent merging.
135 +
136 +### Safe Intermediate Instructions
137 +Only certain instruction types are allowed between merged scopes: `BinaryExpression`, `ComputedLoad`, `JSXText`, `LoadGlobal`, `LoadLocal`, `Primitive`, `PropertyLoad`, `TemplateLiteral`, `UnaryExpression`, and const `StoreLocal`.
138 +
139 +### Lvalue Usage
140 +Intermediate values must be last-used at or before the next scope to allow merging.
141 +
142 +### Non-Invalidating Outputs
143 +If a scope's output may not change when inputs change (e.g., `foo(x) { return x < 10 }` returns same boolean for different x values), that scope cannot be a merge candidate for subsequent scopes.
144 +
145 +## TODOs
146 +```typescript
147 +/*
148 + * TODO LeaveSSA: use IdentifierId for more precise tracking
149 + * Using DeclarationId is necessary for compatible output but produces suboptimal results
150 + * in cases where a scope defines a variable, but that version is never read and always
151 + * overwritten later.
152 + * see reassignment-separate-scopes.js for example
153 + */
154 +lastUsage: Map<DeclarationId, InstructionId> = new Map();
155 +```
156 +
157 +## Example
158 +
159 +### Fixture: `merge-consecutive-scopes-deps-subset-of-decls.js`
160 +
161 +**Input:**
162 +```javascript
163 +import {useState} from 'react';
164 +
165 +function Component() {
166 + const [count, setCount] = useState(0);
167 + return (
168 + <div>
169 + <button onClick={() => setCount(count - 1)}>Decrement</button>
170 + <button onClick={() => setCount(count + 1)}>Increment</button>
171 + </div>
172 + );
173 +}
174 +```
175 +
176 +**After MergeReactiveScopesThatInvalidateTogether** (from `yarn snap -p merge-consecutive-scopes-deps-subset-of-decls.js -d`):
177 +```
178 +scope @1 [7:24] dependencies=[count$32:TPrimitive] declarations=[$51_@5] reassignments=[] {
179 + [8] $35_@1 = Function @context[setCount$33, count$32] // decrement callback
180 + [10] $41 = JSXText "Decrement"
181 + [12] $42_@2 = JSX <button onClick={$35_@1}>{$41}</button>
182 + [15] $43_@3 = Function @context[setCount$33, count$32] // increment callback
183 + [17] $49 = JSXText "Increment"
184 + [19] $50_@4 = JSX <button onClick={$43_@3}>{$49}</button>
185 + [22] $51_@5 = JSX <div>{$42_@2}{$50_@4}</div>
186 +}
187 +```
188 +
189 +All scopes are merged because they share `count` as a dependency. Without merging, this would have separate scopes for each callback and button element.
190 +
191 +**Generated Code:**
192 +```javascript
193 +function Component() {
194 + const $ = _c(2);
195 + const [count, setCount] = useState(0);
196 + let t0;
197 + if ($[0] !== count) {
198 + t0 = (
199 + <div>
200 + <button onClick={() => setCount(count - 1)}>Decrement</button>
201 + <button onClick={() => setCount(count + 1)}>Increment</button>
202 + </div>
203 + );
204 + $[0] = count;
205 + $[1] = t0;
206 + } else {
207 + t0 = $[1];
208 + }
209 + return t0;
210 +}
211 +```
212 +
213 +The merged version uses only 2 cache slots instead of potentially 6-8.
compiler/packages/babel-plugin-react-compiler/docs/passes/27-pruneAlwaysInvalidatingScopes.md new
+143
@@ -0,0 +1,143 @@
1 +# pruneAlwaysInvalidatingScopes
2 +
3 +## File
4 +`src/ReactiveScopes/PruneAlwaysInvalidatingScopes.ts`
5 +
6 +## Purpose
7 +This pass identifies and prunes reactive scopes whose dependencies will *always* invalidate on every render, making memoization pointless. Specifically, it tracks values that are guaranteed to be new allocations (arrays, objects, JSX, new expressions) and checks if those values are used outside of any memoization scope. When a downstream scope depends on such an unmemoized always-invalidating value, the scope is pruned because it would re-execute on every render anyway.
8 +
9 +The optimization avoids wasted comparisons in the generated code. Without this pass, the compiler would emit dependency checks for scopes that will never cache-hit, adding runtime overhead with no benefit. By converting these scopes to `pruned-scope` nodes, the codegen emits the instructions inline without memoization guards.
10 +
11 +## Input Invariants
12 +- The pass expects a `ReactiveFunction` with scopes already formed
13 +- Scopes should have their `dependencies` populated with the identifiers they depend on
14 +- The pass runs after `MergeReactiveScopesThatInvalidateTogether`
15 +- Hook calls have already caused scope flattening via `FlattenScopesWithHooksOrUseHIR`
16 +
17 +## Output Guarantees
18 +- Scopes that depend on unmemoized always-invalidating values are converted to `pruned-scope` nodes
19 +- The `unmemoizedValues` set correctly propagates through `StoreLocal`/`LoadLocal` instructions
20 +- All declarations and reassignments within pruned scopes that are themselves always-invalidating are added to `unmemoizedValues`, enabling cascading pruning of downstream scopes
21 +
22 +## Algorithm
23 +
24 +The pass uses a `ReactiveFunctionTransform` visitor with two key methods:
25 +
26 +### 1. `transformInstruction` - Tracks always-invalidating values:
27 +
28 +```typescript
29 +switch (value.kind) {
30 + case 'ArrayExpression':
31 + case 'ObjectExpression':
32 + case 'JsxExpression':
33 + case 'JsxFragment':
34 + case 'NewExpression': {
35 + if (lvalue !== null) {
36 + this.alwaysInvalidatingValues.add(lvalue.identifier);
37 + if (!withinScope) {
38 + this.unmemoizedValues.add(lvalue.identifier); // Key: only if outside a scope
39 + }
40 + }
41 + break;
42 + }
43 + // Also propagates through StoreLocal and LoadLocal
44 +}
45 +```
46 +
47 +### 2. `transformScope` - Prunes scopes with unmemoized dependencies:
48 +
49 +```typescript
50 +for (const dep of scopeBlock.scope.dependencies) {
51 + if (this.unmemoizedValues.has(dep.identifier)) {
52 + // Propagate unmemoized status to scope outputs
53 + for (const [_, decl] of scopeBlock.scope.declarations) {
54 + if (this.alwaysInvalidatingValues.has(decl.identifier)) {
55 + this.unmemoizedValues.add(decl.identifier);
56 + }
57 + }
58 + return {
59 + kind: 'replace',
60 + value: {
61 + kind: 'pruned-scope',
62 + scope: scopeBlock.scope,
63 + instructions: scopeBlock.instructions,
64 + },
65 + };
66 + }
67 +}
68 +```
69 +
70 +## Edge Cases
71 +
72 +### Function Calls Not Considered Always-Invalidating
73 +The pass optimistically assumes function calls may return primitives, so `makeArray()` doesn't trigger pruning even though it might return a new array.
74 +
75 +### Conditional Allocations
76 +Code like `x = cond ? [] : 42` doesn't trigger pruning because the value might be a primitive.
77 +
78 +### Propagation Through Locals
79 +The pass correctly tracks values through `StoreLocal` and `LoadLocal` to handle variable reassignments and loads.
80 +
81 +### Cascading Pruning
82 +When a scope is pruned, its always-invalidating outputs become unmemoized, potentially causing downstream scopes to be pruned as well.
83 +
84 +## TODOs
85 +None in the source file.
86 +
87 +## Example
88 +
89 +### Fixture: `prune-scopes-whose-deps-invalidate-array.js`
90 +
91 +**Input:**
92 +```javascript
93 +function Component(props) {
94 + const x = [];
95 + useHook();
96 + x.push(props.value);
97 + const y = [x];
98 + return [y];
99 +}
100 +```
101 +
102 +**After PruneAlwaysInvalidatingScopes** (from `yarn snap -p prune-scopes-whose-deps-invalidate-array.js -d`):
103 +```
104 +<pruned> scope @0 [1:14] dependencies=[] declarations=[x$21_@0] reassignments=[] {
105 + [2] $20_@0 = Array []
106 + [3] StoreLocal Const x$21_@0 = $20_@0
107 + [4] $23 = LoadGlobal import { useHook }
108 + [6] $24_@1 = Call $23() // Hook flattens scope
109 + [7] break bb9 (implicit)
110 + [8] $25_@0 = LoadLocal x$21_@0
111 + [9] $26 = PropertyLoad $25_@0.push
112 + [10] $27 = LoadLocal props$19
113 + [11] $28 = PropertyLoad $27.value
114 + [12] $29 = MethodCall $25_@0.$26($28)
115 +}
116 +[14] $30 = LoadLocal x$21_@0
117 +<pruned> scope @2 [15:23] dependencies=[x$21_@0:TObject<BuiltInArray>] declarations=[$35_@3] {
118 + [16] $31_@2 = Array [$30]
119 + [18] StoreLocal Const y$32 = $31_@2
120 + [19] $34 = LoadLocal y$32
121 + [21] $35_@3 = Array [$34]
122 +}
123 +[23] return $35_@3
124 +```
125 +
126 +Key observations:
127 +- Scope @0 is pruned because the hook call (`useHook()`) flattens it (hook rules prevent memoization around hooks)
128 +- `x` is an `ArrayExpression` created in the pruned scope @0, making it unmemoized
129 +- Scope @2 depends on `x$21_@0` which is unmemoized and always-invalidating (it's an array)
130 +- Therefore, scope @2 is also pruned - cascading pruning
131 +
132 +**Generated Code:**
133 +```javascript
134 +function Component(props) {
135 + const x = [];
136 + useHook();
137 + x.push(props.value);
138 + const y = [x];
139 + return [y];
140 +}
141 +```
142 +
143 +The output matches the input because all memoization was pruned - the code runs unconditionally on every render.
compiler/packages/babel-plugin-react-compiler/docs/passes/28-propagateEarlyReturns.md new
+183
@@ -0,0 +1,183 @@
1 +# propagateEarlyReturns
2 +
3 +## File
4 +`src/ReactiveScopes/PropagateEarlyReturns.ts`
5 +
6 +## Purpose
7 +The `propagateEarlyReturns` pass ensures that reactive scopes (memoization blocks) correctly honor the control flow behavior of the original code, particularly when a function returns early from within a reactive scope. Without this transformation, if a component returned early on the previous render and the inputs have not changed, the cached memoization block would be skipped entirely, but the early return would not occur, causing incorrect behavior.
8 +
9 +The pass solves this by transforming `return` statements inside reactive scopes into assignments to a temporary variable followed by a labeled `break`. After the reactive scope completes, generated code checks whether the early return sentinel value was replaced with an actual return value; if so, the function returns that value.
10 +
11 +## Input Invariants
12 +1. **ReactiveFunction structure**: The input must be a `ReactiveFunction` with scopes already inferred (reactive scope blocks are already established)
13 +2. **Scope earlyReturnValue not set**: The pass expects `scopeBlock.scope.earlyReturnValue === null` for scopes it processes
14 +3. **Return statements within reactive scopes**: The pass specifically targets `return` terminal statements that appear within a `withinReactiveScope` context
15 +
16 +## Output Guarantees
17 +1. **Labeled scope blocks**: Top-level reactive scopes containing early returns are wrapped in a labeled block (e.g., `bb14: { ... }`)
18 +2. **Sentinel initialization**: At the start of each such scope, a temporary variable is initialized to `Symbol.for("react.early_return_sentinel")`
19 +3. **Return-to-break transformation**: All `return` statements inside the scope are replaced with:
20 + - An assignment of the return value to the early return temporary
21 + - A `break` to the scope's label
22 +4. **Early return declaration**: The temporary variable is registered as a declaration of the scope so it gets memoized
23 +5. **Post-scope check**: During codegen, an if-statement is added after the scope to check if the temporary differs from the sentinel and return it if so
24 +
25 +## Algorithm
26 +
27 +The pass uses a visitor pattern with a `ReactiveFunctionTransform` that tracks two pieces of state:
28 +
29 +```typescript
30 +type State = {
31 + withinReactiveScope: boolean; // Are we inside a reactive scope?
32 + earlyReturnValue: ReactiveScope['earlyReturnValue']; // Bubble up early return info
33 +};
34 +```
35 +
36 +### Key Steps:
37 +
38 +1. **visitScope** - When entering a reactive scope:
39 + - Create an inner state with `withinReactiveScope: true`
40 + - Traverse the scope's contents
41 + - If any early returns were found (`earlyReturnValue !== null`):
42 + - If this is the **outermost** scope (parent's `withinReactiveScope` is false):
43 + - Store the early return info on the scope
44 + - Add the temporary as a scope declaration
45 + - Prepend sentinel initialization instructions
46 + - Wrap the original instructions in a labeled block
47 + - Otherwise, propagate the early return info to the parent scope
48 +
49 +2. **transformTerminal** - When encountering a `return` inside a reactive scope:
50 + - Create or reuse an early return value identifier
51 + - Replace the return with:
52 + ```typescript
53 + [
54 + {kind: 'instruction', /* StoreLocal: reassign earlyReturnValue = returnValue */},
55 + {kind: 'terminal', /* break to earlyReturnValue.label */}
56 + ]
57 + ```
58 +
59 +### Sentinel Initialization Code (synthesized at scope start):
60 +```typescript
61 +// Load Symbol.for and call it with the sentinel string
62 +let t0 = Symbol.for("react.early_return_sentinel");
63 +```
64 +
65 +## Edge Cases
66 +
67 +### Nested Reactive Scopes
68 +When early returns occur in nested scopes, only the **outermost** scope gets the labeled block wrapper. Inner scopes bubble their early return information up via `parentState.earlyReturnValue`.
69 +
70 +### Multiple Early Returns in Same Scope
71 +All returns share the same temporary variable and label. The first return found creates the identifier, subsequent returns reuse it.
72 +
73 +### Partial Early Returns
74 +When only some control flow paths return early (e.g., one branch returns, the other falls through), the sentinel check after the scope allows normal execution to continue if no early return occurred.
75 +
76 +### Already Processed Scopes
77 +If `scopeBlock.scope.earlyReturnValue !== null` on entry, the pass exits early without modification.
78 +
79 +### Returns Outside Reactive Scopes
80 +The pass only transforms returns where `state.withinReactiveScope === true`. Returns outside scopes are left unchanged.
81 +
82 +## TODOs
83 +None in the source file.
84 +
85 +## Example
86 +
87 +### Fixture: `early-return-within-reactive-scope.js`
88 +
89 +**Input:**
90 +```javascript
91 +function Component(props) {
92 + let x = [];
93 + if (props.cond) {
94 + x.push(props.a);
95 + return x;
96 + } else {
97 + return makeArray(props.b);
98 + }
99 +}
100 +```
101 +
102 +**After PropagateEarlyReturns** (from `yarn snap -p early-return-within-reactive-scope.js -d`):
103 +```
104 +scope @0 [...] earlyReturn={id: #t34$34, label: 14} {
105 + [0] $36 = LoadGlobal(global) Symbol
106 + [0] $37 = PropertyLoad $36.for
107 + [0] $38 = "react.early_return_sentinel"
108 + [0] $35 = MethodCall $36.$37($38)
109 + [0] StoreLocal Let #t34$34{reactive} = $35 // Initialize sentinel
110 + bb14: {
111 + [2] $19_@0 = Array []
112 + [3] StoreLocal Const x$20_@0 = $19_@0
113 + [4] $22{reactive} = LoadLocal props$18
114 + [5] $23{reactive} = PropertyLoad $22.cond
115 + [6] if ($23) {
116 + [7] $24_@0 = LoadLocal x$20_@0
117 + [8] $25 = PropertyLoad $24_@0.push
118 + [9] $26 = LoadLocal props$18
119 + [10] $27 = PropertyLoad $26.a
120 + [11] $28 = MethodCall $24_@0.$25($27)
121 + [12] $29 = LoadLocal x$20_@0
122 + [0] StoreLocal Reassign #t34$34 = $29 // was: return x
123 + [0] break bb14 (labeled)
124 + } else {
125 + [14] $30 = LoadGlobal import { makeArray }
126 + [15] $31 = LoadLocal props$18
127 + [16] $32 = PropertyLoad $31.b
128 + scope @1 [...] {
129 + [18] $33_@1 = Call $30($32)
130 + }
131 + [0] StoreLocal Reassign #t34$34 = $33_@1 // was: return makeArray(props.b)
132 + [0] break bb14 (labeled)
133 + }
134 + }
135 +}
136 +```
137 +
138 +Key observations:
139 +- Scope @0 now has `earlyReturn={id: #t34$34, label: 14}`
140 +- Sentinel initialization code is prepended to the scope
141 +- The scope body is wrapped in `bb14: { ... }`
142 +- Both `return x` and `return makeArray(props.b)` are transformed to `StoreLocal Reassign + break bb14`
143 +
144 +**Generated Code:**
145 +```javascript
146 +function Component(props) {
147 + const $ = _c(6);
148 + let t0;
149 + if ($[0] !== props.a || $[1] !== props.b || $[2] !== props.cond) {
150 + t0 = Symbol.for("react.early_return_sentinel");
151 + bb0: {
152 + const x = [];
153 + if (props.cond) {
154 + x.push(props.a);
155 + t0 = x;
156 + break bb0;
157 + } else {
158 + let t1;
159 + if ($[4] !== props.b) {
160 + t1 = makeArray(props.b);
161 + $[4] = props.b;
162 + $[5] = t1;
163 + } else {
164 + t1 = $[5];
165 + }
166 + t0 = t1;
167 + break bb0;
168 + }
169 + }
170 + $[0] = props.a;
171 + $[1] = props.b;
172 + $[2] = props.cond;
173 + $[3] = t0;
174 + } else {
175 + t0 = $[3];
176 + }
177 + if (t0 !== Symbol.for("react.early_return_sentinel")) {
178 + return t0;
179 + }
180 +}
181 +```
182 +
183 +This transformation ensures that when inputs don't change, the cached return value is used and returned, preserving referential equality and correct early return behavior.
compiler/packages/babel-plugin-react-compiler/docs/passes/29-promoteUsedTemporaries.md new
+203
@@ -0,0 +1,203 @@
1 +# promoteUsedTemporaries
2 +
3 +## File
4 +`src/ReactiveScopes/PromoteUsedTemporaries.ts`
5 +
6 +## Purpose
7 +This pass promotes temporary variables (identifiers with no name) to named variables when they need to be referenced across scope boundaries or in code generation. Temporaries are intermediate values that the compiler creates during lowering; they are typically inlined at their use sites during codegen. However, some temporaries must be emitted as separate declarations - this pass identifies and names them.
8 +
9 +The pass ensures that:
10 +1. Scope dependencies and declarations have proper names for codegen
11 +2. Variables referenced across reactive scope boundaries are named
12 +3. JSX tag identifiers get special naming (`T0`, `T1`, etc.)
13 +4. Temporaries with interposing side-effects are promoted to preserve ordering
14 +
15 +## Input Invariants
16 +- The ReactiveFunction has undergone scope construction and dependency propagation
17 +- Identifiers may have `name === null` (temporaries) or be named
18 +- Scopes have `dependencies`, `declarations`, and `reassignments` populated
19 +- Pruned scopes are properly marked with `kind: 'pruned-scope'`
20 +
21 +## Output Guarantees
22 +- All scope dependencies have non-null names
23 +- All scope declarations have non-null names
24 +- JSX tag temporaries use uppercase naming (`T0`, `T1`, ...)
25 +- Regular temporaries use lowercase naming (`#t{id}`)
26 +- All instances of a promoted identifier share the same name (via DeclarationId tracking)
27 +- Temporaries with interposing mutating instructions are promoted to preserve source ordering
28 +
29 +## Algorithm
30 +
31 +The pass operates in four phases using visitor classes:
32 +
33 +### Phase 1: CollectPromotableTemporaries
34 +Collects information about which temporaries may need promotion:
35 +
36 +```typescript
37 +class CollectPromotableTemporaries {
38 + // Tracks pruned scope declarations and whether they're used outside their scope
39 + pruned: Map<DeclarationId, {activeScopes: Array<ScopeId>; usedOutsideScope: boolean}>
40 +
41 + // Tracks identifiers used as JSX tags (need uppercase names)
42 + tags: Set<DeclarationId>
43 +}
44 +```
45 +
46 +- When visiting a `JsxExpression`, adds the tag identifier to `tags`
47 +- When visiting a `PrunedScope`, records its declarations
48 +- Tracks when pruned declarations are used in different scopes
49 +
50 +### Phase 2: PromoteTemporaries
51 +Promotes temporaries that appear in positions requiring names:
52 +
53 +```typescript
54 +override visitScope(scopeBlock: ReactiveScopeBlock, state: State): void {
55 + // Promote all dependencies without names
56 + for (const dep of scopeBlock.scope.dependencies) {
57 + if (identifier.name == null) {
58 + promoteIdentifier(identifier, state);
59 + }
60 + }
61 + // Promote all declarations without names
62 + for (const [, declaration] of scopeBlock.scope.declarations) {
63 + if (declaration.identifier.name == null) {
64 + promoteIdentifier(declaration.identifier, state);
65 + }
66 + }
67 +}
68 +```
69 +
70 +Also promotes:
71 +- Function parameters without names
72 +- Pruned scope declarations used outside their scope
73 +
74 +### Phase 3: PromoteInterposedTemporaries
75 +Handles ordering-sensitive promotion:
76 +
77 +```typescript
78 +class PromoteInterposedTemporaries {
79 + // Instructions that emit as statements can interpose between temp defs and uses
80 + // If such an instruction occurs, mark pending temporaries as needing promotion
81 +
82 + override visitInstruction(instruction: ReactiveInstruction, state: InterState): void {
83 + // For instructions that become statements (calls, stores, etc.):
84 + if (willBeStatement && !constStore) {
85 + // Mark all pending temporaries as needing promotion
86 + for (const [key, [ident, _]] of state.entries()) {
87 + state.set(key, [ident, true]); // Mark as needing promotion
88 + }
89 + }
90 + }
91 +}
92 +```
93 +
94 +This preserves source ordering when side-effects occur between a temporary's definition and use.
95 +
96 +### Phase 4: PromoteAllInstancesOfPromotedTemporaries
97 +Ensures all instances of a promoted identifier share the same name:
98 +
99 +```typescript
100 +class PromoteAllInstancesOfPromotedTemporaries {
101 + override visitPlace(_id: InstructionId, place: Place, state: State): void {
102 + if (place.identifier.name === null &&
103 + state.promoted.has(place.identifier.declarationId)) {
104 + promoteIdentifier(place.identifier, state);
105 + }
106 + }
107 +}
108 +```
109 +
110 +### Naming Convention
111 +```typescript
112 +function promoteIdentifier(identifier: Identifier, state: State): void {
113 + if (state.tags.has(identifier.declarationId)) {
114 + promoteTemporaryJsxTag(identifier); // Uses #T{id} for JSX tags
115 + } else {
116 + promoteTemporary(identifier); // Uses #t{id} for regular temps
117 + }
118 + state.promoted.add(identifier.declarationId);
119 +}
120 +```
121 +
122 +## Edge Cases
123 +
124 +### JSX Tag Temporaries
125 +JSX tags require uppercase names to be valid JSX syntax. The pass tracks which temporaries are used as JSX tags and uses `T0`, `T1`, etc. instead of `t0`, `t1`.
126 +
127 +### Pruned Scope Declarations
128 +Declarations in pruned scopes are only promoted if they're actually used outside the pruned scope, avoiding unnecessary variable declarations.
129 +
130 +### Const vs Let Temporaries
131 +The pass tracks const identifiers specially - they don't need promotion for ordering purposes since they can't be mutated by interposing instructions.
132 +
133 +### Global Loads
134 +Values loaded from globals (and their property loads) are treated as const-like for promotion purposes.
135 +
136 +### Method Call Properties
137 +The property identifier in a method call is treated as const-like to avoid unnecessary promotion.
138 +
139 +## TODOs
140 +None in the source file.
141 +
142 +## Example
143 +
144 +### Fixture: `simple.js`
145 +
146 +**Input:**
147 +```javascript
148 +export default function foo(x, y) {
149 + if (x) {
150 + return foo(false, y);
151 + }
152 + return [y * 10];
153 +}
154 +```
155 +
156 +**Before PromoteUsedTemporaries:**
157 +```
158 +scope @0 [...] dependencies=[y$14] declarations=[$19_@0]
159 +scope @1 [...] dependencies=[$22] declarations=[$23_@1]
160 +```
161 +
162 +**After PromoteUsedTemporaries:**
163 +```
164 +scope @0 [...] dependencies=[y$14] declarations=[#t5$19_@0]
165 +scope @1 [...] dependencies=[#t9$22] declarations=[#t10$23_@1]
166 +```
167 +
168 +Key observations:
169 +- `$19_@0` is promoted to `#t5$19_@0` because it's a scope declaration
170 +- `$22` is promoted to `#t9$22` because it's a scope dependency
171 +- `$23_@1` is promoted to `#t10$23_@1` because it's a scope declaration
172 +- The `#t` prefix indicates this is a promoted temporary (later renamed by `renameVariables`)
173 +
174 +**Generated Code:**
175 +```javascript
176 +import { c as _c } from "react/compiler-runtime";
177 +export default function foo(x, y) {
178 + const $ = _c(4);
179 + if (x) {
180 + let t0;
181 + if ($[0] !== y) {
182 + t0 = foo(false, y);
183 + $[0] = y;
184 + $[1] = t0;
185 + } else {
186 + t0 = $[1];
187 + }
188 + return t0;
189 + }
190 + const t0 = y * 10;
191 + let t1;
192 + if ($[2] !== t0) {
193 + t1 = [t0];
194 + $[2] = t0;
195 + $[3] = t1;
196 + } else {
197 + t1 = $[3];
198 + }
199 + return t1;
200 +}
201 +```
202 +
203 +The promoted temporaries (`#t5`, `#t9`, `#t10`) become the named variables (`t0`, `t1`) in the output after `renameVariables` runs.
compiler/packages/babel-plugin-react-compiler/docs/passes/30-renameVariables.md new
+200
@@ -0,0 +1,200 @@
1 +# renameVariables
2 +
3 +## File
4 +`src/ReactiveScopes/RenameVariables.ts`
5 +
6 +## Purpose
7 +This pass ensures that every named variable in the function has a unique name that doesn't conflict with other variables in the same block scope or with global identifiers. After scope construction and temporary promotion, variables from different source scopes may end up in the same reactive block - this pass resolves any naming conflicts.
8 +
9 +The pass also converts the `#t{id}` promoted temporary names into clean output names like `t0`, `t1`, etc.
10 +
11 +## Input Invariants
12 +- The ReactiveFunction has been through `promoteUsedTemporaries`
13 +- Variables may have names that conflict with:
14 + - Other variables in the same or ancestor block scope
15 + - Global identifiers referenced by the function
16 + - Promoted temporaries with `#t{id}` or `#T{id}` naming
17 +- The function parameters have names (either from source or promoted)
18 +
19 +## Output Guarantees
20 +- Every named variable has a unique name within its scope
21 +- No variable shadows a global identifier referenced by the function
22 +- Promoted temporaries are renamed to `t0`, `t1`, ... (for regular temps)
23 +- Promoted JSX temporaries are renamed to `T0`, `T1`, ... (for JSX tags)
24 +- Conflicting source names get disambiguated with `$` suffix (e.g., `foo$0`, `foo$1`)
25 +- Returns a `Set<string>` of all unique variable names in the function
26 +
27 +## Algorithm
28 +
29 +### Phase 1: Collect Referenced Globals
30 +Uses `collectReferencedGlobals(fn)` to build a set of all global identifiers referenced by the function. Variable names must not conflict with these.
31 +
32 +### Phase 2: Rename with Scope Stack
33 +The `Scopes` class maintains:
34 +
35 +```typescript
36 +class Scopes {
37 + #seen: Map<DeclarationId, IdentifierName> = new Map(); // Canonical name for each declaration
38 + #stack: Array<Map<string, DeclarationId>> = [new Map()]; // Block scope stack
39 + #globals: Set<string>; // Global names to avoid
40 + names: Set<ValidIdentifierName> = new Set(); // All assigned names
41 +}
42 +```
43 +
44 +### Renaming Logic
45 +```typescript
46 +visit(identifier: Identifier): void {
47 + // Skip unnamed identifiers
48 + if (originalName === null) return;
49 +
50 + // If we've already named this declaration, reuse that name
51 + const mappedName = this.#seen.get(identifier.declarationId);
52 + if (mappedName !== undefined) {
53 + identifier.name = mappedName;
54 + return;
55 + }
56 +
57 + // Find a unique name
58 + let name = originalName.value;
59 + let id = 0;
60 +
61 + // Promoted temporaries start with t0/T0
62 + if (isPromotedTemporary(originalName.value)) {
63 + name = `t${id++}`;
64 + } else if (isPromotedJsxTemporary(originalName.value)) {
65 + name = `T${id++}`;
66 + }
67 +
68 + // Increment until we find a unique name
69 + while (this.#lookup(name) !== null || this.#globals.has(name)) {
70 + if (isPromotedTemporary(...)) {
71 + name = `t${id++}`;
72 + } else if (isPromotedJsxTemporary(...)) {
73 + name = `T${id++}`;
74 + } else {
75 + name = `${originalName.value}$${id++}`; // foo$0, foo$1, etc.
76 + }
77 + }
78 +
79 + identifier.name = makeIdentifierName(name);
80 + this.#seen.set(identifier.declarationId, identifier.name);
81 +}
82 +```
83 +
84 +### Scope Management
85 +```typescript
86 +enter(fn: () => void): void {
87 + this.#stack.push(new Map());
88 + fn();
89 + this.#stack.pop();
90 +}
91 +
92 +#lookup(name: string): DeclarationId | null {
93 + // Search from innermost to outermost scope
94 + for (let i = this.#stack.length - 1; i >= 0; i--) {
95 + const entry = this.#stack[i].get(name);
96 + if (entry !== undefined) return entry;
97 + }
98 + return null;
99 +}
100 +```
101 +
102 +### Visitor Pattern
103 +```typescript
104 +class Visitor extends ReactiveFunctionVisitor<Scopes> {
105 + override visitBlock(block: ReactiveBlock, state: Scopes): void {
106 + state.enter(() => {
107 + this.traverseBlock(block, state);
108 + });
109 + }
110 +
111 + override visitScope(scope: ReactiveScopeBlock, state: Scopes): void {
112 + // Visit scope declarations first
113 + for (const [_, declaration] of scope.scope.declarations) {
114 + state.visit(declaration.identifier);
115 + }
116 + this.traverseScope(scope, state);
117 + }
118 +
119 + override visitPlace(id: InstructionId, place: Place, state: Scopes): void {
120 + state.visit(place.identifier);
121 + }
122 +}
123 +```
124 +
125 +## Edge Cases
126 +
127 +### Shadowed Variables
128 +When the compiler merges scopes that had shadowing in the source:
129 +```javascript
130 +function foo() {
131 + const x = 1;
132 + {
133 + const x = 2; // Shadowed in source
134 + }
135 +}
136 +```
137 +If both `x` declarations end up in the same compiled scope, they become `x` and `x$0`.
138 +
139 +### Global Name Conflicts
140 +If a local variable would conflict with a referenced global:
141 +```javascript
142 +function foo() {
143 + const Math = 1; // Conflicts with global Math if used
144 +}
145 +```
146 +The local gets renamed to `Math$0` if `Math` global is referenced.
147 +
148 +### Nested Functions
149 +The pass recursively processes nested function expressions, entering a new scope for each function body.
150 +
151 +### Pruned Scopes
152 +Pruned scopes don't create a new block scope in the output - the pass traverses their instructions without entering a new scope level.
153 +
154 +### DeclarationId Consistency
155 +The pass uses `DeclarationId` to track which identifiers refer to the same variable, ensuring all references get the same renamed name.
156 +
157 +## TODOs
158 +None in the source file.
159 +
160 +## Example
161 +
162 +### Fixture: `simple.js`
163 +
164 +**Before RenameVariables:**
165 +```
166 +scope @0 [...] declarations=[#t5$19_@0]
167 +scope @1 [...] dependencies=[#t9$22] declarations=[#t10$23_@1]
168 +```
169 +
170 +**After RenameVariables:**
171 +```
172 +scope @0 [...] declarations=[t0$19_@0]
173 +scope @1 [...] dependencies=[t0$22] declarations=[t1$23_@1]
174 +```
175 +
176 +Key observations:
177 +- `#t5$19_@0` becomes `t0$19_@0` (first temporary in scope)
178 +- `#t9$22` becomes `t0$22` (first temporary in a different block scope)
179 +- `#t10$23_@1` becomes `t1$23_@1` (second temporary in that block)
180 +- The `#t` prefix is removed and sequential numbering is applied
181 +
182 +**Generated Code:**
183 +```javascript
184 +export default function foo(x, y) {
185 + const $ = _c(4);
186 + if (x) {
187 + let t0; // Was #t5
188 + if ($[0] !== y) {
189 + t0 = foo(false, y);
190 + // ...
191 + }
192 + return t0;
193 + }
194 + const t0 = y * 10; // Was #t9, reuses t0 since different block scope
195 + let t1; // Was #t10
196 + // ...
197 +}
198 +```
199 +
200 +The pass produces clean, readable output with minimal variable names while avoiding conflicts.
compiler/packages/babel-plugin-react-compiler/docs/passes/31-codegenReactiveFunction.md new
+289
@@ -0,0 +1,289 @@
1 +# codegenReactiveFunction
2 +
3 +## File
4 +`src/ReactiveScopes/CodegenReactiveFunction.ts`
5 +
6 +## Purpose
7 +This is the final pass that converts the ReactiveFunction representation back into a Babel AST. It generates the memoization code that makes React components and hooks efficient by:
8 +1. Creating the `useMemoCache` call to allocate cache slots
9 +2. Generating dependency comparisons to check if values have changed
10 +3. Emitting conditional blocks that skip computation when cached values are valid
11 +4. Storing computed values in the cache
12 +5. Loading cached values when dependencies haven't changed
13 +
14 +## Input Invariants
15 +- The ReactiveFunction has been through all prior passes
16 +- All identifiers that need names have been promoted and renamed
17 +- Reactive scopes have finalized `dependencies`, `declarations`, and `reassignments`
18 +- Early returns have been transformed with sentinel values (via `propagateEarlyReturns`)
19 +- Pruned scopes are marked with `kind: 'pruned-scope'`
20 +- Unique identifiers set is available to avoid naming conflicts
21 +
22 +## Output Guarantees
23 +- Returns a `CodegenFunction` with Babel AST `body`
24 +- All reactive scopes become if-else blocks checking dependencies
25 +- The `$` cache array is properly sized with `useMemoCache(n)`
26 +- Each dependency and output gets its own cache slot
27 +- Pruned scopes emit their instructions inline without memoization
28 +- Early returns use the sentinel pattern with post-scope checks
29 +- Statistics are collected: `memoSlotsUsed`, `memoBlocks`, `memoValues`, etc.
30 +
31 +## Algorithm
32 +
33 +### Entry Point: codegenFunction
34 +```typescript
35 +export function codegenFunction(fn: ReactiveFunction): Result<CodegenFunction, CompilerError> {
36 + const cx = new Context(...);
37 +
38 + // Optional: Fast Refresh source hash tracking
39 + if (enableResetCacheOnSourceFileChanges) {
40 + fastRefreshState = { cacheIndex: cx.nextCacheIndex, hash: sha256(source) };
41 + }
42 +
43 + const compiled = codegenReactiveFunction(cx, fn);
44 +
45 + // Prepend useMemoCache call if any cache slots used
46 + if (cacheCount !== 0) {
47 + body.unshift(
48 + t.variableDeclaration('const', [
49 + t.variableDeclarator(
50 + t.identifier('$'),
51 + t.callExpression(t.identifier('useMemoCache'), [t.numericLiteral(cacheCount)])
52 + )
53 + ])
54 + );
55 + }
56 +
57 + return compiled;
58 +}
59 +```
60 +
61 +### Context Class
62 +Tracks state during codegen:
63 +```typescript
64 +class Context {
65 + #nextCacheIndex: number = 0; // Allocates cache slots
66 + #declarations: Set<DeclarationId> = new Set(); // Tracks declared variables
67 + temp: Temporaries; // Maps identifiers to their expressions
68 + errors: CompilerError;
69 +
70 + get nextCacheIndex(): number {
71 + return this.#nextCacheIndex++; // Returns and increments
72 + }
73 +}
74 +```
75 +
76 +### codegenReactiveScope
77 +The core of memoization code generation:
78 +
79 +```typescript
80 +function codegenReactiveScope(cx: Context, statements: Array<t.Statement>,
81 + scope: ReactiveScope, block: ReactiveBlock): void {
82 + const changeExpressions: Array<t.Expression> = [];
83 + const cacheStoreStatements: Array<t.Statement> = [];
84 + const cacheLoadStatements: Array<t.Statement> = [];
85 +
86 + // 1. Generate dependency checks
87 + for (const dep of scope.dependencies) {
88 + const index = cx.nextCacheIndex;
89 + changeExpressions.push(
90 + t.binaryExpression('!==',
91 + t.memberExpression(t.identifier('$'), t.numericLiteral(index), true),
92 + codegenDependency(cx, dep)
93 + )
94 + );
95 + cacheStoreStatements.push(
96 + t.assignmentExpression('=', $[index], dep)
97 + );
98 + }
99 +
100 + // 2. Generate output cache slots
101 + for (const {identifier} of scope.declarations) {
102 + const index = cx.nextCacheIndex;
103 + // Declare variable if not already declared
104 + if (!cx.hasDeclared(identifier)) {
105 + statements.push(t.variableDeclaration('let', [t.variableDeclarator(name, null)]));
106 + }
107 + cacheLoads.push({name, index, value: name});
108 + }
109 +
110 + // 3. Build test condition
111 + let testCondition = changeExpressions.reduce((acc, expr) =>
112 + t.logicalExpression('||', acc, expr)
113 + );
114 +
115 + // 4. If no dependencies, use sentinel check
116 + if (testCondition === null) {
117 + testCondition = t.binaryExpression('===',
118 + $[firstOutputIndex],
119 + t.callExpression(Symbol.for, ['react.memo_cache_sentinel'])
120 + );
121 + }
122 +
123 + // 5. Generate the memoization if-else
124 + statements.push(
125 + t.ifStatement(
126 + testCondition,
127 + computationBlock, // Compute + store in cache
128 + cacheLoadBlock // Load from cache
129 + )
130 + );
131 +}
132 +```
133 +
134 +### Generated Structure
135 +For a scope with dependencies `[a, b]` and output `result`:
136 +
137 +```javascript
138 +let result;
139 +if ($[0] !== a || $[1] !== b) {
140 + // Computation block
141 + result = compute(a, b);
142 +
143 + // Store dependencies
144 + $[0] = a;
145 + $[1] = b;
146 +
147 + // Store output
148 + $[2] = result;
149 +} else {
150 + // Load from cache
151 + result = $[2];
152 +}
153 +```
154 +
155 +### Early Return Handling
156 +When a scope has an early return (from `propagateEarlyReturns`):
157 +
158 +```typescript
159 +// Before scope: initialize sentinel
160 +t0 = Symbol.for("react.early_return_sentinel");
161 +
162 +// Scope generates labeled block
163 +bb0: {
164 + // ... computation ...
165 + if (cond) {
166 + t0 = returnValue;
167 + break bb0;
168 + }
169 +}
170 +
171 +// After scope: check for early return
172 +if (t0 !== Symbol.for("react.early_return_sentinel")) {
173 + return t0;
174 +}
175 +```
176 +
177 +### Pruned Scopes
178 +Pruned scopes emit their instructions inline without memoization:
179 +```typescript
180 +case 'pruned-scope': {
181 + const scopeBlock = codegenBlockNoReset(cx, item.instructions);
182 + statements.push(...scopeBlock.body); // Inline, no memoization
183 + break;
184 +}
185 +```
186 +
187 +## Edge Cases
188 +
189 +### Zero Dependencies
190 +Scopes with no dependencies use a sentinel value check instead:
191 +```javascript
192 +if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
193 + // First render only
194 +}
195 +```
196 +
197 +### Fast Refresh / HMR
198 +When `enableResetCacheOnSourceFileChanges` is enabled, the generated code includes a source hash check that resets the cache when the source changes:
199 +```javascript
200 +if ($[0] !== "source_hash_abc123") {
201 + for (let $i = 0; $i < cacheCount; $i++) {
202 + $[$i] = Symbol.for("react.memo_cache_sentinel");
203 + }
204 + $[0] = "source_hash_abc123";
205 +}
206 +```
207 +
208 +### Change Detection for Debugging
209 +When `enableChangeDetectionForDebugging` is configured, additional code is generated to detect when cached values unexpectedly change.
210 +
211 +### Labeled Breaks
212 +Control flow with labeled breaks (for early returns or loop exits) uses `codegenLabel` to generate consistent label names:
213 +```typescript
214 +function codegenLabel(id: BlockId): string {
215 + return `bb${id}`; // e.g., "bb0", "bb1"
216 +}
217 +```
218 +
219 +### Nested Functions
220 +Function expressions and object methods are recursively processed with their own contexts.
221 +
222 +### FBT/Internationalization
223 +Special handling for FBT operands ensures they're memoized in the same scope for correct internationalization behavior.
224 +
225 +## Statistics Collected
226 +```typescript
227 +type CodegenFunction = {
228 + memoSlotsUsed: number; // Total cache slots allocated
229 + memoBlocks: number; // Number of reactive scopes
230 + memoValues: number; // Total memoized values
231 + prunedMemoBlocks: number; // Scopes that were pruned
232 + prunedMemoValues: number; // Values in pruned scopes
233 + hasInferredEffect: boolean;
234 + hasFireRewrite: boolean;
235 +};
236 +```
237 +
238 +## TODOs
239 +None in the source file.
240 +
241 +## Example
242 +
243 +### Fixture: `simple.js`
244 +
245 +**Input:**
246 +```javascript
247 +export default function foo(x, y) {
248 + if (x) {
249 + return foo(false, y);
250 + }
251 + return [y * 10];
252 +}
253 +```
254 +
255 +**Generated Code:**
256 +```javascript
257 +import { c as _c } from "react/compiler-runtime";
258 +export default function foo(x, y) {
259 + const $ = _c(4); // Allocate 4 cache slots
260 + if (x) {
261 + let t0;
262 + if ($[0] !== y) { // Check dependency
263 + t0 = foo(false, y); // Compute
264 + $[0] = y; // Store dependency
265 + $[1] = t0; // Store output
266 + } else {
267 + t0 = $[1]; // Load from cache
268 + }
269 + return t0;
270 + }
271 + const t0 = y * 10;
272 + let t1;
273 + if ($[2] !== t0) { // Check dependency
274 + t1 = [t0]; // Compute
275 + $[2] = t0; // Store dependency
276 + $[3] = t1; // Store output
277 + } else {
278 + t1 = $[3]; // Load from cache
279 + }
280 + return t1;
281 +}
282 +```
283 +
284 +Key observations:
285 +- `_c(4)` allocates 4 cache slots total
286 +- First scope uses slots 0-1: slot 0 for `y` dependency, slot 1 for `t0` output
287 +- Second scope uses slots 2-3: slot 2 for `t0` (the computed `y * 10`), slot 3 for `t1` (the array)
288 +- Each scope has an if-else structure: compute/store vs load
289 +- The memoization ensures referential equality of the returned array when `y` hasn't changed
compiler/packages/babel-plugin-react-compiler/docs/passes/32-transformFire.md new
+203
@@ -0,0 +1,203 @@
1 +# transformFire
2 +
3 +## File
4 +`src/Transform/TransformFire.ts`
5 +
6 +## Purpose
7 +This pass transforms `fire(fn())` calls inside `useEffect` lambdas into calls to a `useFire` hook that provides stable function references. The `fire()` function is a React API that allows effect callbacks to call functions with their current values while maintaining stable effect dependencies.
8 +
9 +Without this transform, if an effect depends on a function that changes every render, the effect would re-run on every render. The `useFire` hook provides a stable wrapper that always calls the latest version of the function.
10 +
11 +## Input Invariants
12 +- The `enableFire` feature flag must be enabled
13 +- `fire()` calls must only appear inside `useEffect` lambdas
14 +- Each `fire()` call must have exactly one argument (a function call expression)
15 +- The function being fired must be consistent across all `fire()` calls in the same effect
16 +
17 +## Output Guarantees
18 +- All `fire(fn(...args))` calls are replaced with direct calls `fired_fn(...args)`
19 +- A `useFire(fn)` hook call is inserted before the `useEffect`
20 +- The fired function is stored in a temporary and captured by the effect
21 +- The original function `fn` is removed from the effect's captured context
22 +
23 +## Algorithm
24 +
25 +### Phase 1: Find Fire Calls
26 +```typescript
27 +function replaceFireFunctions(fn: HIRFunction, context: Context): void {
28 + // For each useEffect call instruction:
29 + // 1. Find all fire() calls in the effect lambda
30 + // 2. Validate they have proper arguments
31 + // 3. Track which functions are being fired
32 +
33 + for (const [, block] of fn.body.blocks) {
34 + for (const instr of block.instructions) {
35 + if (isUseEffectCall(instr)) {
36 + const lambda = getEffectLambda(instr);
37 + findAndReplaceFireCalls(lambda, fireFunctions);
38 + }
39 + }
40 + }
41 +}
42 +```
43 +
44 +### Phase 2: Insert useFire Hooks
45 +For each function being fired, insert a `useFire` call:
46 +```typescript
47 +// Before:
48 +useEffect(() => {
49 + fire(foo(props));
50 +}, [foo, props]);
51 +
52 +// After:
53 +const t0 = useFire(foo);
54 +useEffect(() => {
55 + t0(props);
56 +}, [t0, props]);
57 +```
58 +
59 +### Phase 3: Replace Fire Calls
60 +Transform `fire(fn(...args))` to `firedFn(...args)`:
61 +```typescript
62 +// The fire() wrapper is removed
63 +// The inner function call uses the useFire'd version
64 +fire(foo(x, y)) → t0(x, y) // where t0 = useFire(foo)
65 +```
66 +
67 +### Phase 4: Validate No Remaining Fire Uses
68 +```typescript
69 +function ensureNoMoreFireUses(fn: HIRFunction, context: Context): void {
70 + // Ensure all fire() uses have been transformed
71 + // Report errors for any remaining fire() calls
72 +}
73 +```
74 +
75 +## Edge Cases
76 +
77 +### Fire Outside Effect
78 +`fire()` calls outside `useEffect` lambdas cause a validation error:
79 +```javascript
80 +// ERROR: fire() can only be used inside useEffect
81 +function Component() {
82 + fire(callback());
83 +}
84 +```
85 +
86 +### Mixed Fire and Non-Fire Calls
87 +All calls to the same function must either all use `fire()` or none:
88 +```javascript
89 +// ERROR: Cannot mix fire() and non-fire calls
90 +useEffect(() => {
91 + fire(foo(x));
92 + foo(y); // Error: foo is used with and without fire()
93 +});
94 +```
95 +
96 +### Multiple Arguments to Fire
97 +`fire()` accepts exactly one argument (the function call):
98 +```javascript
99 +// ERROR: fire() takes exactly one argument
100 +fire(foo, bar) // Invalid
101 +fire() // Invalid
102 +```
103 +
104 +### Nested Effects
105 +Fire calls in nested effects are validated separately:
106 +```javascript
107 +useEffect(() => {
108 + useEffect(() => { // Error: nested effects not allowed
109 + fire(foo());
110 + });
111 +});
112 +```
113 +
114 +### Deep Scope Handling
115 +The pass handles fire calls within deeply nested scopes inside effects:
116 +```javascript
117 +useEffect(() => {
118 + if (cond) {
119 + while (x) {
120 + fire(foo(x)); // Still transformed correctly
121 + }
122 + }
123 +});
124 +```
125 +
126 +## TODOs
127 +None in the source file.
128 +
129 +## Example
130 +
131 +### Fixture: `transform-fire/basic.js`
132 +
133 +**Input:**
134 +```javascript
135 +// @enableFire
136 +function Component(props) {
137 + const foo = (props_0) => {
138 + console.log(props_0);
139 + };
140 + useEffect(() => {
141 + fire(foo(props));
142 + });
143 + return null;
144 +}
145 +```
146 +
147 +**After TransformFire:**
148 +```
149 +bb0 (block):
150 + [1] $25 = Function @context[] ... // foo definition
151 + [2] StoreLocal Const foo$32 = $25
152 + [3] $45 = LoadGlobal import { useFire } from 'react/compiler-runtime'
153 + [4] $46 = LoadLocal foo$32
154 + [5] $47 = Call $45($46) // useFire(foo)
155 + [6] StoreLocal Const #t44$44 = $47
156 + [7] $34 = LoadGlobal(global) useEffect
157 + [8] $35 = Function @context[#t44$44, props$24] ...
158 + <<anonymous>>():
159 + [1] $37 = LoadLocal #t44$44 // Load the fired function
160 + [2] $38 = LoadLocal props$24
161 + [3] $39 = Call $37($38) // Call it directly (no fire wrapper)
162 + [4] Return Void
163 + [9] Call $34($35) // useEffect(lambda)
164 + [10] Return null
165 +```
166 +
167 +**Generated Code:**
168 +```javascript
169 +import { useFire as _useFire } from "react/compiler-runtime";
170 +function Component(props) {
171 + const $ = _c(4);
172 + let t0;
173 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
174 + t0 = (props_0) => {
175 + console.log(props_0);
176 + };
177 + $[0] = t0;
178 + } else {
179 + t0 = $[0];
180 + }
181 + const foo = t0;
182 + const t1 = _useFire(foo);
183 + let t2;
184 + if ($[1] !== props || $[2] !== t1) {
185 + t2 = () => {
186 + t1(props);
187 + };
188 + $[1] = props;
189 + $[2] = t1;
190 + $[3] = t2;
191 + } else {
192 + t2 = $[3];
193 + }
194 + useEffect(t2);
195 + return null;
196 +}
197 +```
198 +
199 +Key observations:
200 +- `useFire` is imported from `react/compiler-runtime`
201 +- `fire(foo(props))` becomes `t1(props)` where `t1 = _useFire(foo)`
202 +- The effect now depends on `t1` (stable) and `props` (reactive)
203 +- The original `foo` function is memoized and passed to `useFire`
compiler/packages/babel-plugin-react-compiler/docs/passes/33-lowerContextAccess.md new
+174
@@ -0,0 +1,174 @@
1 +# lowerContextAccess
2 +
3 +## File
4 +`src/Optimization/LowerContextAccess.ts`
5 +
6 +## Purpose
7 +This pass optimizes `useContext` calls by generating selector functions that extract only the needed properties from the context. Instead of subscribing to the entire context object, components can subscribe to specific slices, enabling more granular re-rendering.
8 +
9 +When a component destructures specific properties from a context, this pass transforms the `useContext` call to use a selector-based API that only triggers re-renders when the selected properties change.
10 +
11 +## Input Invariants
12 +- The `lowerContextAccess` configuration must be set with:
13 + - `source`: The module to import the lowered context hook from
14 + - `importSpecifierName`: The name of the hook function
15 +- The function must use `useContext` with destructuring patterns
16 +- Only object destructuring patterns with identifier values are supported
17 +
18 +## Output Guarantees
19 +- `useContext(Ctx)` calls with destructuring are replaced with selector calls
20 +- A selector function is generated that extracts the needed properties
21 +- The return type is changed from object to array for positional access
22 +- Unused original `useContext` calls are removed by dead code elimination
23 +
24 +## Algorithm
25 +
26 +### Phase 1: Collect Context Access Patterns
27 +```typescript
28 +function lowerContextAccess(fn: HIRFunction, config: ExternalFunction): void {
29 + const contextAccess: Map<IdentifierId, CallExpression> = new Map();
30 + const contextKeys: Map<IdentifierId, Array<string>> = new Map();
31 +
32 + for (const [, block] of fn.body.blocks) {
33 + for (const instr of block.instructions) {
34 + // Find useContext calls
35 + if (isUseContextCall(instr)) {
36 + contextAccess.set(instr.lvalue.identifier.id, instr.value);
37 + }
38 +
39 + // Find destructuring patterns that access context results
40 + if (isDestructure(instr) && contextAccess.has(instr.value.value.id)) {
41 + const keys = extractPropertyKeys(instr.value.pattern);
42 + contextKeys.set(instr.value.value.id, keys);
43 + }
44 + }
45 + }
46 +}
47 +```
48 +
49 +### Phase 2: Generate Selector Functions
50 +For each context access with known keys:
51 +```typescript
52 +// Original:
53 +const {foo, bar} = useContext(MyContext);
54 +
55 +// Selector function generated:
56 +(ctx) => [ctx.foo, ctx.bar]
57 +```
58 +
59 +### Phase 3: Transform Context Calls
60 +```typescript
61 +// Before:
62 +$0 = useContext(MyContext)
63 +{foo, bar} = $0
64 +
65 +// After:
66 +$0 = useContext_withSelector(MyContext, (ctx) => [ctx.foo, ctx.bar])
67 +[foo, bar] = $0
68 +```
69 +
70 +### Phase 4: Update Destructuring
71 +Change object destructuring to array destructuring to match selector return:
72 +```typescript
73 +// Before: { foo: foo$15, bar: bar$16 } = $14
74 +// After: [ foo$15, bar$16 ] = $14
75 +```
76 +
77 +## Edge Cases
78 +
79 +### Dynamic Property Access
80 +If context properties are accessed dynamically (not through destructuring), the optimization is skipped:
81 +```javascript
82 +const ctx = useContext(MyContext);
83 +const x = ctx[dynamicKey]; // Cannot optimize
84 +```
85 +
86 +### Spread in Destructuring
87 +Spread patterns prevent optimization:
88 +```javascript
89 +const {foo, ...rest} = useContext(MyContext); // Cannot optimize
90 +```
91 +
92 +### Non-Identifier Values
93 +Only simple identifier destructuring is supported:
94 +```javascript
95 +const {foo: bar} = useContext(MyContext); // Supported (rename)
96 +const {foo = defaultVal} = useContext(MyContext); // Not supported
97 +```
98 +
99 +### Multiple Context Accesses
100 +Each `useContext` call is transformed independently:
101 +```javascript
102 +const {a} = useContext(CtxA); // Transformed
103 +const {b} = useContext(CtxB); // Transformed separately
104 +```
105 +
106 +### Hook Guards
107 +When `enableEmitHookGuards` is enabled, the selector function includes proper hook guard annotations.
108 +
109 +## TODOs
110 +None in the source file.
111 +
112 +## Example
113 +
114 +### Fixture: `lower-context-selector-simple.js`
115 +
116 +**Input:**
117 +```javascript
118 +// @lowerContextAccess
119 +function App() {
120 + const {foo, bar} = useContext(MyContext);
121 + return <Bar foo={foo} bar={bar} />;
122 +}
123 +```
124 +
125 +**After OptimizePropsMethodCalls (where lowering happens):**
126 +```
127 +bb0 (block):
128 + [1] $12 = LoadGlobal(global) useContext // Original (now unused)
129 + [2] $13 = LoadGlobal(global) MyContext
130 + [3] $22 = LoadGlobal import { useContext_withSelector } from 'react-compiler-runtime'
131 + [4] $36 = Function @context[]
132 + <<anonymous>>(#t23$30):
133 + [1] $31 = LoadLocal #t23$30
134 + [2] $32 = PropertyLoad $31.foo
135 + [3] $33 = LoadLocal #t23$30
136 + [4] $34 = PropertyLoad $33.bar
137 + [5] $35 = Array [$32, $34] // Return [foo, bar]
138 + [6] Return $35
139 + [5] $14 = Call $22($13, $36) // useContext_withSelector(MyContext, selector)
140 + [6] $17 = Destructure Const { foo: foo$15, bar: bar$16 } = $14
141 + ...
142 +```
143 +
144 +**Generated Code:**
145 +```javascript
146 +import { c as _c } from "react/compiler-runtime";
147 +import { useContext_withSelector } from "react-compiler-runtime";
148 +function App() {
149 + const $ = _c(2);
150 + let t0;
151 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
152 + t0 = (ctx) => [ctx.foo, ctx.bar];
153 + $[0] = t0;
154 + } else {
155 + t0 = $[0];
156 + }
157 + const { foo, bar } = useContext_withSelector(MyContext, t0);
158 + let t1;
159 + if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
160 + t1 = <Bar foo={foo} bar={bar} />;
161 + $[1] = t1;
162 + } else {
163 + t1 = $[1];
164 + }
165 + return t1;
166 +}
167 +```
168 +
169 +Key observations:
170 +- `useContext` is replaced with `useContext_withSelector`
171 +- A selector function `(ctx) => [ctx.foo, ctx.bar]` is generated
172 +- The selector function is memoized (first cache slot)
173 +- Only `foo` and `bar` properties are extracted, enabling granular subscriptions
174 +- The selector return type changes from object to array
compiler/packages/babel-plugin-react-compiler/docs/passes/34-optimizePropsMethodCalls.md new
+132
@@ -0,0 +1,132 @@
1 +# optimizePropsMethodCalls
2 +
3 +## File
4 +`src/Optimization/OptimizePropsMethodCalls.ts`
5 +
6 +## Purpose
7 +This pass converts method calls on the props object to regular function calls. Method calls like `props.onClick()` are transformed to `const t0 = props.onClick; t0()`. This normalization enables better analysis and optimization by the compiler.
8 +
9 +The transformation is important because method calls have different semantics than regular calls - the receiver (`props`) would normally be passed as `this` to the method. For React props, methods are typically just callback functions where `this` binding doesn't matter, so converting them to regular calls is safe and enables better memoization.
10 +
11 +## Input Invariants
12 +- The function has been through type inference
13 +- Props parameters are typed as `TObject<BuiltInProps>`
14 +
15 +## Output Guarantees
16 +- All `MethodCall` instructions where the receiver has props type are converted to `CallExpression`
17 +- The method property becomes the callee of the call
18 +- Arguments are preserved exactly
19 +
20 +## Algorithm
21 +
22 +```typescript
23 +export function optimizePropsMethodCalls(fn: HIRFunction): void {
24 + for (const [, block] of fn.body.blocks) {
25 + for (let i = 0; i < block.instructions.length; i++) {
26 + const instr = block.instructions[i]!;
27 +
28 + if (
29 + instr.value.kind === 'MethodCall' &&
30 + isPropsType(instr.value.receiver.identifier)
31 + ) {
32 + // Transform: props.onClick(arg)
33 + // To: const t0 = props.onClick; t0(arg)
34 + instr.value = {
35 + kind: 'CallExpression',
36 + callee: instr.value.property, // The method becomes the callee
37 + args: instr.value.args,
38 + loc: instr.value.loc,
39 + };
40 + }
41 + }
42 + }
43 +}
44 +
45 +function isPropsType(identifier: Identifier): boolean {
46 + return (
47 + identifier.type.kind === 'Object' &&
48 + identifier.type.shapeId === BuiltInPropsId
49 + );
50 +}
51 +```
52 +
53 +## Edge Cases
54 +
55 +### Non-Props Method Calls
56 +Method calls on non-props objects are left unchanged:
57 +```javascript
58 +// Unchanged - array.map is not on props
59 +array.map(x => x * 2)
60 +
61 +// Unchanged - obj is not props
62 +obj.method()
63 +```
64 +
65 +### Props Type Detection
66 +The pass uses type information to identify props:
67 +```javascript
68 +function Component(props) {
69 + // props has type TObject<BuiltInProps>
70 + props.onClick(); // Transformed
71 +}
72 +
73 +function Regular(obj) {
74 + // obj has unknown type
75 + obj.onClick(); // Not transformed
76 +}
77 +```
78 +
79 +### Nested Props Access
80 +Only direct method calls on props are transformed:
81 +```javascript
82 +props.onClick(); // Transformed
83 +props.nested.onClick(); // Not transformed (receiver is props.nested, not props)
84 +```
85 +
86 +### Arrow Function Callbacks
87 +Works with any method on props:
88 +```javascript
89 +props.onChange(value); // Transformed
90 +props.onSubmit(data); // Transformed
91 +props.validate(input); // Transformed
92 +```
93 +
94 +## TODOs
95 +None in the source file.
96 +
97 +## Example
98 +
99 +### Fixture: Using props method
100 +
101 +**Input:**
102 +```javascript
103 +function Component(props) {
104 + return <button onClick={() => props.onClick()} />;
105 +}
106 +```
107 +
108 +**Before OptimizePropsMethodCalls:**
109 +```
110 +[1] $5 = Function @context[props$1] ...
111 + <<anonymous>>():
112 + [1] $2 = LoadLocal props$1
113 + [2] $3 = PropertyLoad $2.onClick
114 + [3] $4 = MethodCall $2.$3() // Method call on props
115 + [4] Return Void
116 +```
117 +
118 +**After OptimizePropsMethodCalls:**
119 +```
120 +[1] $5 = Function @context[props$1] ...
121 + <<anonymous>>():
122 + [1] $2 = LoadLocal props$1
123 + [2] $3 = PropertyLoad $2.onClick
124 + [3] $4 = Call $3() // Now a regular call
125 + [4] Return Void
126 +```
127 +
128 +Key observations:
129 +- `MethodCall $2.$3()` becomes `Call $3()`
130 +- The property load (`$3 = PropertyLoad $2.onClick`) is preserved
131 +- The receiver (`$2`) is no longer part of the call
132 +- This enables the compiler to analyze `onClick` as a regular function
compiler/packages/babel-plugin-react-compiler/docs/passes/35-optimizeForSSR.md new
+187
@@ -0,0 +1,187 @@
1 +# optimizeForSSR
2 +
3 +## File
4 +`src/Optimization/OptimizeForSSR.ts`
5 +
6 +## Purpose
7 +This pass applies Server-Side Rendering (SSR) specific optimizations. During SSR, React renders components to HTML strings without mounting them in the DOM. This means:
8 +
9 +1. **Effects don't run** - `useEffect` and `useLayoutEffect` are no-ops
10 +2. **Event handlers aren't needed** - There's no DOM to attach handlers to
11 +3. **State is never updated** - Components render once with initial state
12 +4. **Refs aren't attached** - There's no DOM to ref
13 +
14 +The pass leverages these SSR characteristics to inline and simplify code, removing unnecessary runtime overhead.
15 +
16 +## Input Invariants
17 +- The function has been through type inference
18 +- Hook types are properly identified (useState, useReducer, useEffect, etc.)
19 +- Function types for callbacks are properly inferred
20 +
21 +## Output Guarantees
22 +- `useState(initialValue)` is inlined to just `[initialValue, noop]`
23 +- `useReducer(reducer, initialArg, init?)` is inlined to `[init ? init(initialArg) : initialArg, noop]`
24 +- `useEffect` and `useLayoutEffect` calls are removed entirely
25 +- Event handler functions (functions that call setState) are replaced with empty functions
26 +- Ref-typed values are removed from JSX props
27 +
28 +## Algorithm
29 +
30 +### Phase 1: Identify Inlinable State
31 +```typescript
32 +const inlinedState = new Map<IdentifierId, InstructionValue>();
33 +
34 +for (const instr of block.instructions) {
35 + if (isUseStateCall(instr)) {
36 + // Store the initial value for inlining
37 + inlinedState.set(instr.lvalue.id, {
38 + kind: 'ArrayExpression',
39 + elements: [initialValue, noopFunction],
40 + });
41 + }
42 +
43 + if (isUseReducerCall(instr)) {
44 + // Compute initial state and store for inlining
45 + const initialState = init ? callInit(initialArg) : initialArg;
46 + inlinedState.set(instr.lvalue.id, {
47 + kind: 'ArrayExpression',
48 + elements: [initialState, noopFunction],
49 + });
50 + }
51 +}
52 +```
53 +
54 +### Phase 2: Inline State Hooks
55 +Replace useState/useReducer with their computed initial values:
56 +```typescript
57 +// Before:
58 +$0 = useState(0)
59 +[state, setState] = $0
60 +
61 +// After (inlined):
62 +$0 = [0, () => {}]
63 +[state, setState] = $0
64 +```
65 +
66 +### Phase 3: Remove Effects
67 +```typescript
68 +if (isUseEffectCall(instr) || isUseLayoutEffectCall(instr)) {
69 + // Remove the instruction entirely
70 + block.instructions.splice(i, 1);
71 +}
72 +```
73 +
74 +### Phase 4: Identify and Neuter Event Handlers
75 +```typescript
76 +// Functions that capture and call setState are event handlers
77 +if (capturesSetState(functionExpr)) {
78 + // Replace with empty function
79 + instr.value = {
80 + kind: 'FunctionExpression',
81 + params: originalParams,
82 + body: emptyBody,
83 + };
84 +}
85 +```
86 +
87 +### Phase 5: Remove Ref Props
88 +```typescript
89 +if (isJSX(instr) && hasRefProp(instr)) {
90 + // Remove ref={...} from JSX props
91 + removeRefProp(instr.value);
92 +}
93 +```
94 +
95 +## Edge Cases
96 +
97 +### useState with Function Initializer
98 +When `useState` receives a function initializer, it must be called:
99 +```javascript
100 +// useState(() => expensive())
101 +// SSR: Call the initializer to get the value
102 +const [state] = [expensiveComputation(), noop];
103 +```
104 +
105 +### useReducer with Init Function
106 +The optional `init` function is called with `initialArg`:
107 +```javascript
108 +// useReducer(reducer, arg, init)
109 +// SSR: [init(arg), noop]
110 +```
111 +
112 +### Nested State Setters
113 +Functions that transitively call setState are also event handlers:
114 +```javascript
115 +function outer() {
116 + function inner() {
117 + setState(x); // inner is event handler
118 + }
119 + inner(); // outer is also event handler
120 +}
121 +```
122 +
123 +### Conditional Event Handlers
124 +Event handler detection is conservative - if a function might call setState, it's treated as an event handler.
125 +
126 +### Refs in Nested Objects
127 +Only direct `ref` props on JSX are removed:
128 +```javascript
129 +<div ref={myRef} /> // ref removed
130 +<div config={{ref: myRef}} /> // ref NOT removed (nested)
131 +```
132 +
133 +## TODOs
134 +None in the source file.
135 +
136 +## Example
137 +
138 +### Fixture: `ssr/optimize-ssr.js`
139 +
140 +**Input:**
141 +```javascript
142 +function Component() {
143 + const [state, setState] = useState(0);
144 + const ref = useRef(null);
145 + const onChange = (e) => {
146 + setState(e.target.value);
147 + };
148 + useEffect(() => {
149 + log(ref.current.value);
150 + });
151 + return <input value={state} onChange={onChange} ref={ref} />;
152 +}
153 +```
154 +
155 +**After SSR Optimization:**
156 +```javascript
157 +function Component() {
158 + const $ = _c(1);
159 + // useState inlined to [initialValue, noop]
160 + const [state] = [0, () => {}];
161 +
162 + // useRef returns object with current: null
163 + const ref = { current: null };
164 +
165 + // Event handler replaced with noop (it calls setState)
166 + const onChange = () => {};
167 +
168 + // useEffect removed entirely (no-op on SSR)
169 +
170 + // ref prop removed from JSX
171 + let t0;
172 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
173 + t0 = <input value={state} onChange={onChange} />;
174 + $[0] = t0;
175 + } else {
176 + t0 = $[0];
177 + }
178 + return t0;
179 +}
180 +```
181 +
182 +Key observations:
183 +- `useState(0)` becomes `[0, () => {}]` - no hook call
184 +- `useEffect(...)` is removed entirely
185 +- `onChange` is replaced with empty function since it called `setState`
186 +- `ref={ref}` prop is removed from JSX
187 +- SSR output is simpler and has less runtime overhead
compiler/packages/babel-plugin-react-compiler/docs/passes/36-outlineJSX.md new
+224
@@ -0,0 +1,224 @@
1 +# outlineJSX
2 +
3 +## File
4 +`src/Optimization/OutlineJsx.ts`
5 +
6 +## Purpose
7 +This pass outlines nested JSX elements into separate component functions. When a callback function contains JSX, this pass can extract that JSX into a new component, which enables:
8 +
9 +1. **Better code splitting** - Outlined components can be lazily loaded
10 +2. **Memoization at component boundaries** - React's reconciliation can skip unchanged subtrees
11 +3. **Reduced closure captures** - Outlined components receive props explicitly
12 +
13 +The pass specifically targets JSX within callbacks (like `.map()` callbacks) rather than top-level component returns.
14 +
15 +## Input Invariants
16 +- The `enableJsxOutlining` feature flag must be enabled
17 +- The function must be a React component or hook
18 +- JSX must appear within a nested function expression (callback)
19 +
20 +## Output Guarantees
21 +- Nested functions containing only JSX returns are extracted as separate components
22 +- The original callback is replaced with a call to the outlined component
23 +- Captured variables become explicit props to the outlined component
24 +- The outlined component is registered with the environment for emission
25 +
26 +## Algorithm
27 +
28 +### Phase 1: Identify Outlinable JSX
29 +```typescript
30 +function outlineJsxImpl(fn: HIRFunction, outlinedFns: Array<HIRFunction>): void {
31 + for (const [, block] of fn.body.blocks) {
32 + for (const instr of block.instructions) {
33 + if (instr.value.kind === 'FunctionExpression') {
34 + const innerFn = instr.value.loweredFunc.func;
35 +
36 + // Check if function only returns JSX
37 + if (canOutline(innerFn)) {
38 + const outlined = createOutlinedComponent(innerFn);
39 + outlinedFns.push(outlined);
40 + replaceWithComponentCall(instr, outlined);
41 + }
42 + }
43 + }
44 + }
45 +}
46 +```
47 +
48 +### Phase 2: Check Outlinability
49 +```typescript
50 +function canOutline(fn: HIRFunction): boolean {
51 + // Must have exactly one block with only JSX-related instructions
52 + // Must end with returning JSX
53 + // Must not have complex control flow
54 +
55 + return (
56 + fn.body.blocks.size === 1 &&
57 + returnsJSX(fn) &&
58 + !hasComplexControlFlow(fn)
59 + );
60 +}
61 +```
62 +
63 +### Phase 3: Create Outlined Component
64 +```typescript
65 +function createOutlinedComponent(fn: HIRFunction): HIRFunction {
66 + // Convert captured context to props
67 + const props = fn.context.map(capture => ({
68 + name: capture.identifier.name,
69 + type: capture.identifier.type,
70 + }));
71 +
72 + // Create new component function
73 + return {
74 + ...fn,
75 + params: [{kind: 'Identifier', name: 'props', ...}],
76 + context: [], // No captures - all via props
77 + };
78 +}
79 +```
80 +
81 +### Phase 4: Replace Original Callback
82 +```typescript
83 +function replaceWithComponentCall(instr: Instruction, outlined: HIRFunction): void {
84 + // Original: items.map(item => <Stringify item={item} />)
85 + // Becomes: items.map(item => <OutlinedComponent item={item} />)
86 +
87 + instr.value = {
88 + kind: 'JSX',
89 + tag: {kind: 'LoadGlobal', name: outlined.id},
90 + props: capturedVariablesToProps(instr.value.context),
91 + };
92 +}
93 +```
94 +
95 +### Phase 5: Register Outlined Functions
96 +```typescript
97 +export function outlineJSX(fn: HIRFunction): void {
98 + const outlinedFns: Array<HIRFunction> = [];
99 + outlineJsxImpl(fn, outlinedFns);
100 +
101 + for (const outlinedFn of outlinedFns) {
102 + fn.env.outlineFunction(outlinedFn, 'Component');
103 + }
104 +}
105 +```
106 +
107 +## Edge Cases
108 +
109 +### Context Captures
110 +Variables captured by the callback become props:
111 +```javascript
112 +// Before:
113 +items.map(item => <Card key={item.id} user={currentUser} item={item} />)
114 +
115 +// After (outlined):
116 +function OutlinedCard(props) {
117 + return <Card key={props.item.id} user={props.currentUser} item={props.item} />;
118 +}
119 +items.map(item => <OutlinedCard currentUser={currentUser} item={item} />)
120 +```
121 +
122 +### Complex Control Flow
123 +Callbacks with conditionals or loops are not outlined:
124 +```javascript
125 +// Not outlined - has conditional
126 +items.map(item => item.show ? <Card item={item} /> : null)
127 +```
128 +
129 +### Multiple JSX Returns
130 +Only single-JSX-return callbacks are outlined:
131 +```javascript
132 +// Not outlined - multiple potential returns
133 +items.map(item => {
134 + if (item.type === 'a') return <TypeA item={item} />;
135 + return <TypeB item={item} />;
136 +})
137 +```
138 +
139 +### Top-Level JSX
140 +Only JSX in nested callbacks is outlined, not component return values:
141 +```javascript
142 +function Component() {
143 + return <div />; // Not outlined - this is the component's return
144 +}
145 +```
146 +
147 +### Recursive Outlining
148 +The pass recursively processes outlined components to outline their nested JSX.
149 +
150 +## TODOs
151 +None in the source file.
152 +
153 +## Example
154 +
155 +### Fixture: `outlined-helper.js`
156 +
157 +**Input:**
158 +```javascript
159 +// @enableFunctionOutlining
160 +function Component(props) {
161 + return (
162 + <div>
163 + {props.items.map(item => (
164 + <Stringify key={item.id} item={item.name} />
165 + ))}
166 + </div>
167 + );
168 +}
169 +```
170 +
171 +**After OutlineJSX:**
172 +```
173 +// Outlined component:
174 +function _outlined_Component$1(props) {
175 + return <Stringify key={props.item.id} item={props.item.name} />;
176 +}
177 +
178 +// Original component modified:
179 +function Component(props) {
180 + return (
181 + <div>
182 + {props.items.map(item => (
183 + <_outlined_Component$1 item={item} />
184 + ))}
185 + </div>
186 + );
187 +}
188 +```
189 +
190 +**Generated Code:**
191 +```javascript
192 +function _outlined_Component$1(props) {
193 + const $ = _c(2);
194 + const item = props.item;
195 + let t0;
196 + if ($[0] !== item.id || $[1] !== item.name) {
197 + t0 = <Stringify key={item.id} item={item.name} />;
198 + $[0] = item.id;
199 + $[1] = item.name;
200 + } else {
201 + t0 = $[1];
202 + }
203 + return t0;
204 +}
205 +
206 +function Component(props) {
207 + const $ = _c(2);
208 + let t0;
209 + if ($[0] !== props.items) {
210 + t0 = props.items.map((item) => <_outlined_Component$1 item={item} />);
211 + $[0] = props.items;
212 + $[1] = t0;
213 + } else {
214 + t0 = $[1];
215 + }
216 + return <div>{t0}</div>;
217 +}
218 +```
219 +
220 +Key observations:
221 +- The map callback JSX is extracted into `_outlined_Component$1`
222 +- The `item` variable becomes a prop instead of a closure capture
223 +- The outlined component gets its own memoization cache
224 +- This enables React to skip re-rendering unchanged list items
compiler/packages/babel-plugin-react-compiler/docs/passes/37-outlineFunctions.md new
+169
@@ -0,0 +1,169 @@
1 +# outlineFunctions
2 +
3 +## File
4 +`src/Optimization/OutlineFunctions.ts`
5 +
6 +## Purpose
7 +This pass outlines pure function expressions that have no captured context into top-level helper functions. By moving these functions outside the component, they become truly static and can be shared across renders without any memoization overhead.
8 +
9 +A function with no captured context is completely self-contained - it only uses its parameters and globals. Such functions don't need to be recreated on each render and can be hoisted to module scope.
10 +
11 +## Input Invariants
12 +- The `enableFunctionOutlining` feature flag must be enabled
13 +- Functions must have `context.length === 0` (no captured variables)
14 +- Functions must be anonymous (no `id` property)
15 +- Functions must not be FBT macro operands (tracked by `fbtOperands` parameter)
16 +
17 +## Output Guarantees
18 +- Pure function expressions are replaced with `LoadGlobal` of the outlined function
19 +- Outlined functions are registered with the environment for emission
20 +- The original instruction is transformed to load the global
21 +
22 +## Algorithm
23 +
24 +```typescript
25 +export function outlineFunctions(
26 + fn: HIRFunction,
27 + fbtOperands: Set<IdentifierId>,
28 +): void {
29 + for (const [, block] of fn.body.blocks) {
30 + for (let i = 0; i < block.instructions.length; i++) {
31 + const instr = block.instructions[i]!;
32 +
33 + if (
34 + instr.value.kind === 'FunctionExpression' &&
35 + instr.value.loweredFunc.func.context.length === 0 &&
36 + instr.value.loweredFunc.func.id === null &&
37 + !fbtOperands.has(instr.lvalue.identifier.id)
38 + ) {
39 + // Outline this function
40 + const outlinedId = fn.env.outlineFunction(
41 + instr.value.loweredFunc.func,
42 + 'helper',
43 + );
44 +
45 + // Replace with LoadGlobal
46 + instr.value = {
47 + kind: 'LoadGlobal',
48 + binding: {
49 + kind: 'ModuleLocal',
50 + name: outlinedId,
51 + },
52 + loc: instr.value.loc,
53 + };
54 + }
55 + }
56 + }
57 +}
58 +```
59 +
60 +## Edge Cases
61 +
62 +### Functions with Context
63 +Functions that capture variables are not outlined:
64 +```javascript
65 +function Component(props) {
66 + const x = props.value;
67 + const fn = () => x * 2; // Captures x, not outlined
68 +}
69 +```
70 +
71 +### Named Functions
72 +Functions with explicit names are not outlined:
73 +```javascript
74 +const foo = function namedFn() { ... }; // Has id, not outlined
75 +```
76 +
77 +### FBT Operands
78 +Functions used as FBT operands cannot be outlined due to translation requirements:
79 +```javascript
80 +<fbt>
81 + Hello <fbt:param name="user">{() => getName()}</fbt:param>
82 +</fbt>
83 +// The function cannot be outlined - FBT needs it inline
84 +```
85 +
86 +### Arrow Functions vs Function Expressions
87 +Both arrow functions and function expressions are candidates:
88 +```javascript
89 +const a = () => 1; // Outlined if no context
90 +const b = function() {}; // Outlined if no context
91 +```
92 +
93 +### Recursive Functions
94 +Self-referencing functions cannot be outlined (they would have themselves in context):
95 +```javascript
96 +const fib = (n) => n <= 1 ? n : fib(n-1) + fib(n-2); // References self
97 +```
98 +
99 +## TODOs
100 +None in the source file.
101 +
102 +## Example
103 +
104 +### Fixture: `outlined-helper.js`
105 +
106 +**Input:**
107 +```javascript
108 +// @enableFunctionOutlining
109 +function Component(props) {
110 + return (
111 + <div>
112 + {props.items.map(item => (
113 + <Stringify key={item.id} item={item.name} />
114 + ))}
115 + </div>
116 + );
117 +}
118 +```
119 +
120 +**Analysis:**
121 +The map callback `item => <Stringify .../>` has one captured variable: nothing from the component (only uses `item` parameter). However, it receives `item` as a parameter, not from context.
122 +
123 +If we have a truly pure helper:
124 +```javascript
125 +// @enableFunctionOutlining
126 +function Component(props) {
127 + const double = (x) => x * 2; // No context, pure
128 + return <div>{double(props.value)}</div>;
129 +}
130 +```
131 +
132 +**After OutlineFunctions:**
133 +```
134 +// Outlined to module scope:
135 +function _outlined_double$1(x) {
136 + return x * 2;
137 +}
138 +
139 +// In component:
140 +[1] $1 = LoadGlobal _outlined_double$1 // Instead of FunctionExpression
141 +[2] StoreLocal Const double = $1
142 +```
143 +
144 +**Generated Code:**
145 +```javascript
146 +function _outlined_double$1(x) {
147 + return x * 2;
148 +}
149 +
150 +function Component(props) {
151 + const $ = _c(2);
152 + const double = _outlined_double$1; // Just a reference, no recreation
153 + let t0;
154 + if ($[0] !== props.value) {
155 + t0 = <div>{double(props.value)}</div>;
156 + $[0] = props.value;
157 + $[1] = t0;
158 + } else {
159 + t0 = $[1];
160 + }
161 + return t0;
162 +}
163 +```
164 +
165 +Key observations:
166 +- The pure function is hoisted to module scope
167 +- The component just references the outlined function
168 +- No memoization needed for the function itself
169 +- Reduces runtime overhead by avoiding function recreation
compiler/packages/babel-plugin-react-compiler/docs/passes/38-memoizeFbtAndMacroOperandsInSameScope.md new
+231
@@ -0,0 +1,231 @@
1 +# memoizeFbtAndMacroOperandsInSameScope
2 +
3 +## File
4 +`src/ReactiveScopes/MemoizeFbtAndMacroOperandsInSameScope.ts`
5 +
6 +## Purpose
7 +This pass ensures that FBT (Facebook Translation) expressions and their operands are memoized within the same reactive scope. FBT is Facebook's internationalization system that requires special handling to ensure translations work correctly.
8 +
9 +The key insight is that FBT operands must be computed and frozen together with the FBT call itself. If operands were memoized in separate scopes, the translation system could receive stale operand values when only some inputs change.
10 +
11 +## Input Invariants
12 +- The function has been through type inference
13 +- FBT calls (`fbt`, `fbt.c`, `fbt:param`, etc.) are properly identified
14 +- Custom macros are configured in `fn.env.config.customMacros`
15 +- Reactive scope variables have been inferred
16 +
17 +## Output Guarantees
18 +- All operands of FBT calls are assigned to the same reactive scope as the FBT call
19 +- The `fbtOperands` set is returned for use by other passes (e.g., `outlineFunctions`)
20 +- Operand scope assignments use either transitive or shallow inlining based on macro definition
21 +
22 +## Algorithm
23 +
24 +### Phase 1: Collect Macro Kinds
25 +```typescript
26 +const macroKinds = new Map<Macro, MacroDefinition>([
27 + ...Array.from(FBT_TAGS.entries()), // Built-in fbt tags
28 + ...(fn.env.config.customMacros ?? []).map(([name, def]) => [name, def]),
29 +]);
30 +```
31 +
32 +### Phase 2: Populate Macro Tags
33 +```typescript
34 +function populateMacroTags(
35 + fn: HIRFunction,
36 + macroKinds: Map<Macro, MacroDefinition>,
37 +): Map<IdentifierId, MacroDefinition> {
38 + const macroTags = new Map();
39 +
40 + for (const instr of allInstructions(fn)) {
41 + if (isLoadGlobal(instr) || isPropertyLoad(instr)) {
42 + const name = getName(instr);
43 + if (macroKinds.has(name)) {
44 + macroTags.set(instr.lvalue.id, macroKinds.get(name));
45 + }
46 + }
47 + }
48 +
49 + return macroTags;
50 +}
51 +```
52 +
53 +### Phase 3: Merge Macro Arguments
54 +```typescript
55 +function mergeMacroArguments(
56 + fn: HIRFunction,
57 + macroTags: Map<IdentifierId, MacroDefinition>,
58 + macroKinds: Map<Macro, MacroDefinition>,
59 +): Set<IdentifierId> {
60 + const macroValues = new Set<IdentifierId>();
61 +
62 + for (const instr of allInstructions(fn)) {
63 + if (isCall(instr) || isMethodCall(instr) || isJSX(instr)) {
64 + const callee = getCallee(instr);
65 + const macroDef = macroTags.get(callee.id);
66 +
67 + if (macroDef !== undefined) {
68 + // Mark all operands to be in same scope
69 + for (const operand of getOperands(instr)) {
70 + macroValues.add(operand.id);
71 +
72 + // Merge scope to match macro call scope
73 + if (macroDef.inlineLevel === InlineLevel.Transitive) {
74 + mergeScopesTransitively(operand, instr.lvalue);
75 + } else {
76 + mergeScopes(operand, instr.lvalue);
77 + }
78 + }
79 + }
80 + }
81 + }
82 +
83 + return macroValues;
84 +}
85 +```
86 +
87 +### InlineLevel Types
88 +```typescript
89 +enum InlineLevel {
90 + Shallow, // Only merge direct operands
91 + Transitive, // Merge operands and their dependencies
92 +}
93 +```
94 +
95 +## Edge Cases
96 +
97 +### Nested FBT Params
98 +FBT params can be nested, and all levels must be in the same scope:
99 +```javascript
100 +<fbt>
101 + Hello <fbt:param name="user">
102 + <fbt:param name="firstName">{user.firstName}</fbt:param>
103 + </fbt:param>
104 +</fbt>
105 +```
106 +
107 +### FBT with Complex Expressions
108 +Complex expressions as operands have their entire dependency chain merged:
109 +```javascript
110 +<fbt>
111 + Count: <fbt:param name="count">{items.length * multiplier}</fbt:param>
112 +</fbt>
113 +// Both items.length and multiplier expressions are merged into fbt scope
114 +```
115 +
116 +### Custom Macros
117 +User-defined macros can specify their inlining behavior:
118 +```typescript
119 +customMacros: [
120 + ['myMacro', { inlineLevel: InlineLevel.Transitive }],
121 +]
122 +```
123 +
124 +### Method Calls on FBT
125 +`fbt.param()`, `fbt.plural()`, etc. are handled as method calls:
126 +```javascript
127 +fbt(
128 + fbt.param('count', items.length), // MethodCall on fbt
129 + 'description'
130 +)
131 +```
132 +
133 +### JSX vs Call Syntax
134 +Both JSX and call syntax for FBT are handled:
135 +```javascript
136 +// JSX syntax
137 +<fbt desc="greeting">Hello</fbt>
138 +
139 +// Call syntax
140 +fbt('Hello', 'greeting')
141 +```
142 +
143 +## Built-in FBT Tags
144 +The pass recognizes these FBT constructs:
145 +- `fbt` / `fbt.c` - Main translation functions
146 +- `fbt:param` - Parameter substitution
147 +- `fbt:plural` - Plural handling
148 +- `fbt:enum` - Enumeration values
149 +- `fbt:name` - Name parameters
150 +- `fbt:pronoun` - Pronoun handling
151 +- `fbs` - Simple string translation
152 +
153 +## TODOs
154 +None in the source file.
155 +
156 +## Example
157 +
158 +### Fixture: `fbt/fbt-call.js`
159 +
160 +**Input:**
161 +```javascript
162 +function Component(props) {
163 + const text = fbt(
164 + `${fbt.param('count', props.count)} items`,
165 + 'Number of items'
166 + );
167 + return <div>{text}</div>;
168 +}
169 +```
170 +
171 +**Before MemoizeFbtAndMacroOperandsInSameScope:**
172 +```
173 +[1] $18 = LoadGlobal import fbt from 'fbt'
174 +[2] $19 = LoadGlobal import fbt from 'fbt'
175 +[3] $20_@0[3:8] = PropertyLoad $19.param
176 +[4] $21 = "(key) count"
177 +[5] $22 = LoadLocal props$17
178 +[6] $23 = PropertyLoad $22.count
179 +[7] $24_@0[3:8] = MethodCall $19.$20_@0($21, $23) // fbt.param call
180 +[8] $25 = `${$24_@0} items`
181 +[9] $26 = "(description) Number of items"
182 +[10] $27_@1 = Call $18($25, $26) // fbt call
183 +```
184 +
185 +**After MemoizeFbtAndMacroOperandsInSameScope:**
186 +```
187 +[1] $18_@1[1:11] = LoadGlobal import fbt from 'fbt' // Merged to @1
188 +[2] $19 = LoadGlobal import fbt from 'fbt'
189 +[3] $20_@0[3:8] = PropertyLoad $19.param
190 +[4] $21 = "(key) count"
191 +[5] $22 = LoadLocal props$17
192 +[6] $23 = PropertyLoad $22.count
193 +[7] $24_@1[1:11] = MethodCall $19.$20_@0($21, $23) // Merged to @1
194 +[8] $25_@1[1:11] = `${$24_@1} items` // Merged to @1
195 +[9] $26_@1[1:11] = "(description) Number of items" // Merged to @1
196 +[10] $27_@1[1:11] = Call $18_@1($25_@1, $26_@1) // Main fbt scope @1
197 +```
198 +
199 +**Generated Code:**
200 +```javascript
201 +function Component(props) {
202 + const $ = _c(3);
203 + let t0;
204 + if ($[0] !== props.count) {
205 + // All fbt operands computed in same memoization block
206 + t0 = fbt(
207 + `${fbt.param("count", props.count)} items`,
208 + "Number of items"
209 + );
210 + $[0] = props.count;
211 + $[1] = t0;
212 + } else {
213 + t0 = $[1];
214 + }
215 + const text = t0;
216 + let t1;
217 + if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
218 + t1 = <div>{text}</div>;
219 + $[2] = t1;
220 + } else {
221 + t1 = $[2];
222 + }
223 + return t1;
224 +}
225 +```
226 +
227 +Key observations:
228 +- All FBT-related operations are in the same memoization scope `@1`
229 +- `fbt.param`, template literal, and `fbt` call are memoized together
230 +- This ensures the translation system receives consistent operand values
231 +- The entire translation is recomputed when any operand (`props.count`) changes
compiler/packages/babel-plugin-react-compiler/docs/passes/39-validateContextVariableLValues.md new
+192
@@ -0,0 +1,192 @@
1 +# validateContextVariableLValues
2 +
3 +## File
4 +`src/Validation/ValidateContextVariableLValues.ts`
5 +
6 +## Purpose
7 +This validation pass ensures that all load/store references to a given named identifier are consistent with the "kind" of that variable (normal local variable or context variable). Context variables are variables that are captured by closures and require special handling for correct closure semantics.
8 +
9 +The pass prevents mixing context variable operations (`DeclareContext`, `StoreContext`, `LoadContext`) with local variable operations (`DeclareLocal`, `StoreLocal`, `LoadLocal`, `Destructure`) on the same identifier.
10 +
11 +## Input Invariants
12 +- The function has been lowered to HIR
13 +- All instructions have been categorized by kind
14 +- Nested function expressions have been lowered
15 +
16 +## Validation Rules
17 +
18 +### Rule 1: Consistent Variable Kind
19 +All references to the same identifier must use consistent load/store operations:
20 +- Context variables must only use `DeclareContext`, `StoreContext`, `LoadContext`
21 +- Local variables must only use `DeclareLocal`, `StoreLocal`, `LoadLocal`
22 +
23 +**Error (Invariant violation):**
24 +```
25 +Expected all references to a variable to be consistently local or context references
26 +Identifier [place] is referenced as a [kind] variable, but was previously referenced as a [prev.kind] variable
27 +```
28 +
29 +### Rule 2: No Destructuring of Context Variables
30 +Context variables cannot be destructured using the `Destructure` instruction.
31 +
32 +**Error (Todo):**
33 +```
34 +Support destructuring of context variables
35 +```
36 +
37 +### Rule 3: Unhandled Instruction Variants
38 +If an instruction has lvalues that the pass does not handle, it throws a Todo error.
39 +
40 +**Error (Todo):**
41 +```
42 +ValidateContextVariableLValues: unhandled instruction variant
43 +Handle '[kind]' lvalues
44 +```
45 +
46 +## Algorithm
47 +
48 +### Phase 1: Initialize Tracking
49 +```typescript
50 +const identifierKinds: Map<IdentifierId, {place: Place, kind: 'local' | 'context' | 'destructure'}> = new Map();
51 +```
52 +
53 +### Phase 2: Visit All Instructions
54 +The pass iterates through all blocks and instructions, categorizing each based on its kind:
55 +
56 +```typescript
57 +for (const [, block] of fn.body.blocks) {
58 + for (const instr of block.instructions) {
59 + switch (value.kind) {
60 + case 'DeclareContext':
61 + case 'StoreContext':
62 + visit(identifierKinds, value.lvalue.place, 'context');
63 + break;
64 + case 'LoadContext':
65 + visit(identifierKinds, value.place, 'context');
66 + break;
67 + case 'StoreLocal':
68 + case 'DeclareLocal':
69 + visit(identifierKinds, value.lvalue.place, 'local');
70 + break;
71 + case 'LoadLocal':
72 + visit(identifierKinds, value.place, 'local');
73 + break;
74 + case 'PostfixUpdate':
75 + case 'PrefixUpdate':
76 + visit(identifierKinds, value.lvalue, 'local');
77 + break;
78 + case 'Destructure':
79 + for (const lvalue of eachPatternOperand(value.lvalue.pattern)) {
80 + visit(identifierKinds, lvalue, 'destructure');
81 + }
82 + break;
83 + case 'ObjectMethod':
84 + case 'FunctionExpression':
85 + // Recursively validate nested functions
86 + validateContextVariableLValuesImpl(value.loweredFunc.func, identifierKinds);
87 + break;
88 + }
89 + }
90 +}
91 +```
92 +
93 +### Phase 3: Check Consistency
94 +For each place visited, the `visit` function checks if the identifier was previously seen with a different kind:
95 +
96 +```typescript
97 +function visit(identifiers, place, kind) {
98 + const prev = identifiers.get(place.identifier.id);
99 + if (prev !== undefined) {
100 + const wasContext = prev.kind === 'context';
101 + const isContext = kind === 'context';
102 + if (wasContext !== isContext) {
103 + // Check for destructuring of context variable
104 + if (prev.kind === 'destructure' || kind === 'destructure') {
105 + CompilerError.throwTodo({
106 + reason: `Support destructuring of context variables`,
107 + ...
108 + });
109 + }
110 + // Invariant violation: inconsistent variable kinds
111 + CompilerError.invariant(false, {
112 + reason: 'Expected all references to be consistently local or context references',
113 + ...
114 + });
115 + }
116 + }
117 + identifiers.set(place.identifier.id, {place, kind});
118 +}
119 +```
120 +
121 +## Edge Cases
122 +
123 +### Nested Function Expressions
124 +The validation recursively processes nested function expressions and object methods, sharing the same `identifierKinds` map. This ensures that a variable captured by a nested function is consistently treated as a context variable throughout the entire function hierarchy.
125 +
126 +### Destructuring Patterns
127 +Each operand in a destructure pattern is visited individually, marked as 'destructure' kind. If the same identifier was previously used as a context variable, a Todo error is thrown since destructuring of context variables is not yet supported.
128 +
129 +### Update Expressions
130 +Both `PostfixUpdate` (e.g., `x++`) and `PrefixUpdate` (e.g., `++x`) are treated as local variable operations.
131 +
132 +## TODOs
133 +
134 +1. **Destructuring of context variables** - Currently not supported:
135 + ```typescript
136 + CompilerError.throwTodo({
137 + reason: `Support destructuring of context variables`,
138 + ...
139 + });
140 + ```
141 +
142 +2. **Unhandled instruction variants** - Some instruction types with lvalues may not be handled:
143 + ```typescript
144 + CompilerError.throwTodo({
145 + reason: 'ValidateContextVariableLValues: unhandled instruction variant',
146 + description: `Handle '${value.kind}' lvalues`,
147 + ...
148 + });
149 + ```
150 +
151 +## Example
152 +
153 +### Fixture: `error.todo-for-of-loop-with-context-variable-iterator.js`
154 +
155 +**Input:**
156 +```javascript
157 +import {useHook} from 'shared-runtime';
158 +
159 +function Component(props) {
160 + const data = useHook();
161 + const items = [];
162 + // NOTE: `item` is a context variable because it's reassigned and also referenced
163 + // within a closure, the `onClick` handler of each item
164 + for (let item of props.data) {
165 + item = item ?? {}; // reassignment to force a context variable
166 + items.push(
167 + <div key={item.id} onClick={() => data.set(item)}>
168 + {item.id}
169 + </div>
170 + );
171 + }
172 + return <div>{items}</div>;
173 +}
174 +```
175 +
176 +**Error:**
177 +```
178 +Todo: Support non-trivial for..of inits
179 +
180 +error.todo-for-of-loop-with-context-variable-iterator.ts:8:2
181 + 6 | // NOTE: `item` is a context variable because it's reassigned and also referenced
182 + 7 | // within a closure, the `onClick` handler of each item
183 +> 8 | for (let item of props.data) {
184 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
185 +> 9 | item = item ?? {}; // reassignment to force a context variable
186 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
187 +...
188 +> 15 | }
189 + | ^^^^ Support non-trivial for..of inits
190 +```
191 +
192 +Note: This particular error comes from an earlier pass (lowering), but demonstrates the kind of context variable scenarios that this validation is designed to catch.
compiler/packages/babel-plugin-react-compiler/docs/passes/40-validateUseMemo.md new
+299
@@ -0,0 +1,299 @@
1 +# validateUseMemo
2 +
3 +## File
4 +`src/Validation/ValidateUseMemo.ts`
5 +
6 +## Purpose
7 +This validation pass ensures that `useMemo()` callbacks follow React's requirements. The pass checks for several common mistakes that developers make when using `useMemo()`:
8 +
9 +1. Callbacks should not accept parameters (useMemo callbacks are called with no arguments)
10 +2. Callbacks should not be async or generator functions (must return a value synchronously)
11 +3. Callbacks should not reassign variables declared outside the callback (must be pure)
12 +4. Callbacks should return a value (useMemo is for computing values, not side effects)
13 +5. The result of useMemo should be used (not discarded)
14 +
15 +## Input Invariants
16 +- The function has been lowered to HIR
17 +- `useMemo` is either imported directly or accessed via `React.useMemo`
18 +- Function expressions have been lowered with their parameters and async/generator flags preserved
19 +
20 +## Validation Rules
21 +
22 +### Rule 1: No Parameters
23 +useMemo callbacks must not accept parameters.
24 +
25 +**Error:**
26 +```
27 +Error: useMemo() callbacks may not accept parameters
28 +
29 +useMemo() callbacks are called by React to cache calculations across re-renders. They should not take parameters. Instead, directly reference the props, state, or local variables needed for the computation.
30 +```
31 +
32 +### Rule 2: No Async or Generator Functions
33 +useMemo callbacks must synchronously return a value.
34 +
35 +**Error:**
36 +```
37 +Error: useMemo() callbacks may not be async or generator functions
38 +
39 +useMemo() callbacks are called once and must synchronously return a value.
40 +```
41 +
42 +### Rule 3: No Reassigning Outer Variables
43 +useMemo callbacks cannot reassign variables declared outside the callback.
44 +
45 +**Error:**
46 +```
47 +Error: useMemo() callbacks may not reassign variables declared outside of the callback
48 +
49 +useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function.
50 +```
51 +
52 +### Rule 4: Must Return a Value (when `validateNoVoidUseMemo` is enabled)
53 +useMemo callbacks should return a value.
54 +
55 +**Error:**
56 +```
57 +Error: useMemo() callbacks must return a value
58 +
59 +This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects.
60 +```
61 +
62 +### Rule 5: Result Must Be Used (when `validateNoVoidUseMemo` is enabled)
63 +The result of useMemo should be used somewhere.
64 +
65 +**Error:**
66 +```
67 +Error: useMemo() result is unused
68 +
69 +This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects.
70 +```
71 +
72 +## Algorithm
73 +
74 +### Phase 1: Track useMemo References
75 +```typescript
76 +const useMemos = new Set<IdentifierId>();
77 +const react = new Set<IdentifierId>();
78 +const functions = new Map<IdentifierId, FunctionExpression>();
79 +const unusedUseMemos = new Map<IdentifierId, SourceLocation>();
80 +```
81 +
82 +The pass tracks:
83 +- Direct `useMemo` imports via `LoadGlobal`
84 +- `React` imports to detect `React.useMemo` pattern
85 +- Function expressions that might be useMemo callbacks
86 +- Unused useMemo results
87 +
88 +### Phase 2: Identify useMemo Calls
89 +```typescript
90 +for (const instr of block.instructions) {
91 + switch (value.kind) {
92 + case 'LoadGlobal':
93 + if (value.binding.name === 'useMemo') {
94 + useMemos.add(lvalue.identifier.id);
95 + } else if (value.binding.name === 'React') {
96 + react.add(lvalue.identifier.id);
97 + }
98 + break;
99 + case 'PropertyLoad':
100 + if (react.has(value.object.identifier.id) && value.property === 'useMemo') {
101 + useMemos.add(lvalue.identifier.id);
102 + }
103 + break;
104 + case 'CallExpression':
105 + case 'MethodCall':
106 + // Check if callee is useMemo
107 + const callee = value.kind === 'CallExpression' ? value.callee : value.property;
108 + if (useMemos.has(callee.identifier.id) && value.args.length > 0) {
109 + // Validate the callback
110 + }
111 + break;
112 + }
113 +}
114 +```
115 +
116 +### Phase 3: Validate Callback
117 +For each useMemo call, the pass retrieves the callback function expression and validates:
118 +
119 +```typescript
120 +const body = functions.get(arg.identifier.id);
121 +
122 +// Check for parameters
123 +if (body.loweredFunc.func.params.length > 0) {
124 + errors.push("useMemo() callbacks may not accept parameters");
125 +}
126 +
127 +// Check for async/generator
128 +if (body.loweredFunc.func.async || body.loweredFunc.func.generator) {
129 + errors.push("useMemo() callbacks may not be async or generator functions");
130 +}
131 +
132 +// Check for context variable reassignment
133 +validateNoContextVariableAssignment(body.loweredFunc.func, errors);
134 +
135 +// Check for return value (if config enabled)
136 +if (fn.env.config.validateNoVoidUseMemo) {
137 + if (!hasNonVoidReturn(body.loweredFunc.func)) {
138 + errors.push("useMemo() callbacks must return a value");
139 + }
140 +}
141 +```
142 +
143 +### Phase 4: Validate No Context Variable Assignment
144 +```typescript
145 +function validateNoContextVariableAssignment(fn: HIRFunction, errors: CompilerError) {
146 + const context = new Set(fn.context.map(place => place.identifier.id));
147 + for (const block of fn.body.blocks.values()) {
148 + for (const instr of block.instructions) {
149 + if (value.kind === 'StoreContext') {
150 + if (context.has(value.lvalue.place.identifier.id)) {
151 + errors.push("Cannot reassign variable");
152 + }
153 + }
154 + }
155 + }
156 +}
157 +```
158 +
159 +### Phase 5: Check for Unused Results
160 +```typescript
161 +// Track which useMemo results are referenced
162 +for (const operand of eachInstructionValueOperand(value)) {
163 + unusedUseMemos.delete(operand.identifier.id);
164 +}
165 +
166 +// At the end, report any unused useMemos
167 +for (const loc of unusedUseMemos.values()) {
168 + errors.push("useMemo() result is unused");
169 +}
170 +```
171 +
172 +### Return Value Helper
173 +```typescript
174 +function hasNonVoidReturn(func: HIRFunction): boolean {
175 + for (const [, block] of func.body.blocks) {
176 + if (block.terminal.kind === 'return') {
177 + if (block.terminal.returnVariant === 'Explicit' ||
178 + block.terminal.returnVariant === 'Implicit') {
179 + return true;
180 + }
181 + }
182 + }
183 + return false;
184 +}
185 +```
186 +
187 +## Edge Cases
188 +
189 +### React.useMemo vs useMemo
190 +The pass handles both import styles:
191 +```javascript
192 +import {useMemo} from 'react';
193 +useMemo(() => x, [x]);
194 +
195 +import React from 'react';
196 +React.useMemo(() => x, [x]);
197 +```
198 +
199 +### Immediately Used Results
200 +Results that are used immediately don't trigger the "unused" warning:
201 +```javascript
202 +const x = useMemo(() => compute(), [dep]);
203 +return x; // x is used
204 +```
205 +
206 +### Void Return Detection
207 +The pass checks for explicit and implicit returns. A function with only `return;` statements (void returns) will trigger the "must return a value" error.
208 +
209 +### VoidUseMemo Errors as Logged Errors
210 +The void useMemo errors (no return value, unused result) are logged via `fn.env.logErrors()` rather than thrown immediately. This allows them to be treated differently (e.g., as warnings) based on configuration.
211 +
212 +## TODOs
213 +None in the source file.
214 +
215 +## Example
216 +
217 +### Fixture: `error.invalid-useMemo-callback-args.js`
218 +
219 +**Input:**
220 +```javascript
221 +function component(a, b) {
222 + let x = useMemo(c => a, []);
223 + return x;
224 +}
225 +```
226 +
227 +**Error:**
228 +```
229 +Error: useMemo() callbacks may not accept parameters
230 +
231 +useMemo() callbacks are called by React to cache calculations across re-renders. They should not take parameters. Instead, directly reference the props, state, or local variables needed for the computation.
232 +
233 +error.invalid-useMemo-callback-args.ts:2:18
234 + 1 | function component(a, b) {
235 +> 2 | let x = useMemo(c => a, []);
236 + | ^ Callbacks with parameters are not supported
237 + 3 | return x;
238 + 4 | }
239 +```
240 +
241 +### Fixture: `error.invalid-useMemo-async-callback.js`
242 +
243 +**Input:**
244 +```javascript
245 +function component(a, b) {
246 + let x = useMemo(async () => {
247 + await a;
248 + }, []);
249 + return x;
250 +}
251 +```
252 +
253 +**Error:**
254 +```
255 +Error: useMemo() callbacks may not be async or generator functions
256 +
257 +useMemo() callbacks are called once and must synchronously return a value.
258 +
259 +error.invalid-useMemo-async-callback.ts:2:18
260 + 1 | function component(a, b) {
261 +> 2 | let x = useMemo(async () => {
262 + | ^^^^^^^^^^^^^
263 +> 3 | await a;
264 + | ^^^^^^^^^^^^
265 +> 4 | }, []);
266 + | ^^^^ Async and generator functions are not supported
267 +```
268 +
269 +### Fixture: `error.invalid-reassign-variable-in-usememo.js`
270 +
271 +**Input:**
272 +```javascript
273 +function Component() {
274 + let x;
275 + const y = useMemo(() => {
276 + let z;
277 + x = [];
278 + z = true;
279 + return z;
280 + }, []);
281 + return [x, y];
282 +}
283 +```
284 +
285 +**Error:**
286 +```
287 +Error: useMemo() callbacks may not reassign variables declared outside of the callback
288 +
289 +useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function.
290 +
291 +error.invalid-reassign-variable-in-usememo.ts:5:4
292 + 3 | const y = useMemo(() => {
293 + 4 | let z;
294 +> 5 | x = [];
295 + | ^ Cannot reassign variable
296 + 6 | z = true;
297 + 7 | return z;
298 + 8 | }, []);
299 +```
compiler/packages/babel-plugin-react-compiler/docs/passes/41-validateHooksUsage.md new
+330
@@ -0,0 +1,330 @@
1 +# validateHooksUsage
2 +
3 +## File
4 +`src/Validation/ValidateHooksUsage.ts`
5 +
6 +## Purpose
7 +This validation pass ensures that the function honors the [Rules of Hooks](https://react.dev/warnings/invalid-hook-call-warning). Specifically, it validates that:
8 +
9 +1. Hooks may only be called unconditionally (not in if statements, loops, etc.)
10 +2. Hooks cannot be used as first-class values (passed around, stored in variables, etc.)
11 +3. Hooks must be the same function on every render (no dynamic hooks)
12 +4. Hooks must be called at the top level, not within nested function expressions
13 +
14 +## Input Invariants
15 +- The function has been lowered to HIR
16 +- Global bindings have been resolved and typed
17 +- Nested function expressions have been lowered
18 +
19 +## Value Kinds Lattice
20 +
21 +The pass uses abstract interpretation with a lattice of value kinds:
22 +
23 +```typescript
24 +enum Kind {
25 + Error, // Hook already used in an invalid way (stop reporting)
26 + KnownHook, // Definitely a hook (from LoadGlobal with hook type)
27 + PotentialHook, // Might be a hook (hook-like name but not from global)
28 + Global, // A global value that is not a hook
29 + Local, // A local variable
30 +}
31 +```
32 +
33 +The `joinKinds` function merges kinds, with earlier kinds taking precedence:
34 +- `Error` > `KnownHook` > `PotentialHook` > `Global` > `Local`
35 +
36 +## Validation Rules
37 +
38 +### Rule 1: No Conditional Hook Calls
39 +Hooks must always be called in a consistent order.
40 +
41 +**Error:**
42 +```
43 +Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
44 +```
45 +
46 +### Rule 2: No Hooks as First-Class Values
47 +Known hooks may not be referenced as normal values (only called).
48 +
49 +**Error:**
50 +```
51 +Error: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values
52 +```
53 +
54 +### Rule 3: No Dynamic Hooks
55 +Potential hooks (hook-like names from local scope) may change between renders.
56 +
57 +**Error:**
58 +```
59 +Error: Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks
60 +```
61 +
62 +### Rule 4: No Hooks in Nested Functions
63 +Hooks must be called at the top level of a component or custom hook.
64 +
65 +**Error:**
66 +```
67 +Error: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
68 +
69 +Cannot call [hookKind] within a function expression
70 +```
71 +
72 +## Algorithm
73 +
74 +### Phase 1: Compute Unconditional Blocks
75 +```typescript
76 +const unconditionalBlocks = computeUnconditionalBlocks(fn);
77 +```
78 +Determines which blocks are guaranteed to execute on every render (not inside conditionals).
79 +
80 +### Phase 2: Initialize Tracking
81 +```typescript
82 +const valueKinds = new Map<IdentifierId, Kind>();
83 +
84 +// Initialize parameters
85 +for (const param of fn.params) {
86 + const place = param.kind === 'Identifier' ? param : param.place;
87 + const kind = getKindForPlace(place); // PotentialHook if hook-like name
88 + setKind(place, kind);
89 +}
90 +```
91 +
92 +### Phase 3: Track Value Kinds Through Instructions
93 +
94 +For each instruction, the pass tracks how hook-ness flows through values:
95 +
96 +```typescript
97 +case 'LoadGlobal':
98 + // Globals are the source of KnownHook
99 + if (getHookKind(fn.env, instr.lvalue.identifier) != null) {
100 + setKind(instr.lvalue, Kind.KnownHook);
101 + } else {
102 + setKind(instr.lvalue, Kind.Global);
103 + }
104 + break;
105 +
106 +case 'PropertyLoad':
107 + // Hook-like property of Global -> KnownHook
108 + // Hook-like property of Local -> PotentialHook
109 + // Property of KnownHook -> KnownHook (if hook-like name)
110 + const objectKind = getKindForPlace(value.object);
111 + const isHookProperty = isHookName(value.property);
112 + // Determine kind based on object kind and property name
113 + break;
114 +
115 +case 'CallExpression':
116 + const calleeKind = getKindForPlace(value.callee);
117 + const isHookCallee = calleeKind === Kind.KnownHook || calleeKind === Kind.PotentialHook;
118 +
119 + if (isHookCallee && !unconditionalBlocks.has(block.id)) {
120 + recordConditionalHookError(value.callee);
121 + } else if (calleeKind === Kind.PotentialHook) {
122 + recordDynamicHookUsageError(value.callee);
123 + }
124 + break;
125 +```
126 +
127 +### Phase 4: Check for Invalid Hook References
128 +
129 +When a `KnownHook` is used as an operand (not as a callee), it's an error:
130 +
131 +```typescript
132 +function visitPlace(place: Place): void {
133 + const kind = valueKinds.get(place.identifier.id);
134 + if (kind === Kind.KnownHook) {
135 + recordInvalidHookUsageError(place);
136 + }
137 +}
138 +```
139 +
140 +### Phase 5: Validate Nested Function Expressions
141 +
142 +Recursively check that nested functions don't call hooks:
143 +
144 +```typescript
145 +function visitFunctionExpression(errors: CompilerError, fn: HIRFunction) {
146 + for (const instr of allInstructions(fn)) {
147 + if (isCall(instr)) {
148 + const callee = getCallee(instr);
149 + const hookKind = getHookKind(fn.env, callee.identifier);
150 + if (hookKind != null) {
151 + errors.push({
152 + reason: 'Hooks must be called at the top level...',
153 + description: `Cannot call ${hookKind} within a function expression`,
154 + });
155 + }
156 + }
157 + // Recursively check nested functions
158 + if (isFunctionExpression(instr)) {
159 + visitFunctionExpression(errors, instr.value.loweredFunc.func);
160 + }
161 + }
162 +}
163 +```
164 +
165 +### Phi Node Handling
166 +
167 +For phi nodes (control flow join points), the pass joins the kinds of all operands:
168 +
169 +```typescript
170 +for (const phi of block.phis) {
171 + let kind = isHookName(phi.place.identifier.name) ? Kind.PotentialHook : Kind.Local;
172 + for (const [, operand] of phi.operands) {
173 + const operandKind = valueKinds.get(operand.identifier.id);
174 + if (operandKind !== undefined) {
175 + kind = joinKinds(kind, operandKind);
176 + }
177 + }
178 + valueKinds.set(phi.place.identifier.id, kind);
179 +}
180 +```
181 +
182 +## Edge Cases
183 +
184 +### Optional Calls
185 +Optional calls like `useHook?.()` are treated as conditional:
186 +```javascript
187 +const result = useHook?.(); // Error: conditional hook call
188 +```
189 +
190 +### Property Access on Hooks
191 +Hook-like properties of known hooks are also known hooks:
192 +```javascript
193 +const useFoo = useHook.useFoo; // useFoo is KnownHook
194 +useFoo(); // Must be called unconditionally
195 +```
196 +
197 +### Destructuring from Global
198 +Destructuring hook-like names from a global creates known hooks:
199 +```javascript
200 +const {useState} = React; // useState is KnownHook
201 +```
202 +
203 +### Hook-Like Names from Local Variables
204 +Hook-like names from local variables are potential hooks:
205 +```javascript
206 +const obj = createObject();
207 +const useFoo = obj.useFoo; // PotentialHook
208 +useFoo(); // Error: dynamic hook
209 +```
210 +
211 +### Error Deduplication
212 +The pass deduplicates errors by source location, and once an error is recorded for a place, it's marked as `Kind.Error` to prevent further errors for the same place.
213 +
214 +## TODOs
215 +
216 +1. **Fixpoint iteration for loops** - The pass currently skips phi operands whose value is unknown (which can occur in loops). A follow-up could expand this to fixpoint iteration:
217 + ```typescript
218 + // NOTE: we currently skip operands whose value is unknown
219 + // (which can only occur for functions with loops), we may
220 + // cause us to miss invalid code in some cases. We should
221 + // expand this to a fixpoint iteration in a follow-up.
222 + ```
223 +
224 +## Example
225 +
226 +### Fixture: `rules-of-hooks/error.invalid-hook-if-consequent.js`
227 +
228 +**Input:**
229 +```javascript
230 +function Component(props) {
231 + let x = null;
232 + if (props.cond) {
233 + x = useHook();
234 + }
235 + return x;
236 +}
237 +```
238 +
239 +**Error:**
240 +```
241 +Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
242 +
243 +error.invalid-hook-if-consequent.ts:4:8
244 + 2 | let x = null;
245 + 3 | if (props.cond) {
246 +> 4 | x = useHook();
247 + | ^^^^^^^ Hooks must always be called in a consistent order...
248 + 5 | }
249 + 6 | return x;
250 +```
251 +
252 +### Fixture: `rules-of-hooks/error.invalid-hook-as-prop.js`
253 +
254 +**Input:**
255 +```javascript
256 +function Component({useFoo}) {
257 + useFoo();
258 +}
259 +```
260 +
261 +**Error:**
262 +```
263 +Error: Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks
264 +
265 +error.invalid-hook-as-prop.ts:2:2
266 + 1 | function Component({useFoo}) {
267 +> 2 | useFoo();
268 + | ^^^^^^ Hooks must be the same function on every render...
269 + 3 | }
270 +```
271 +
272 +### Fixture: `rules-of-hooks/error.invalid-hook-in-nested-function-expression-object-expression.js`
273 +
274 +**Input:**
275 +```javascript
276 +function Component() {
277 + 'use memo';
278 + const f = () => {
279 + const x = {
280 + outer() {
281 + const g = () => {
282 + const y = {
283 + inner() {
284 + return useFoo();
285 + },
286 + };
287 + return y;
288 + };
289 + },
290 + };
291 + return x;
292 + };
293 +}
294 +```
295 +
296 +**Error:**
297 +```
298 +Error: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
299 +
300 +Cannot call hook within a function expression.
301 +
302 +error.invalid-hook-in-nested-function-expression-object-expression.ts:10:21
303 + 8 | const y = {
304 + 9 | inner() {
305 +> 10 | return useFoo();
306 + | ^^^^^^ Hooks must be called at the top level...
307 + 11 | },
308 + 12 | };
309 +```
310 +
311 +### Fixture: `rules-of-hooks/error.invalid-hook-optionalcall.js`
312 +
313 +**Input:**
314 +```javascript
315 +function Component() {
316 + const {result} = useConditionalHook?.() ?? {};
317 + return result;
318 +}
319 +```
320 +
321 +**Error:**
322 +```
323 +Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
324 +
325 +error.invalid-hook-optionalcall.ts:2:19
326 + 1 | function Component() {
327 +> 2 | const {result} = useConditionalHook?.() ?? {};
328 + | ^^^^^^^^^^^^^^^^^^ Hooks must always be called in a consistent order...
329 + 3 | return result;
330 +```
compiler/packages/babel-plugin-react-compiler/docs/passes/42-validateNoCapitalizedCalls.md new
+233
@@ -0,0 +1,233 @@
1 +# validateNoCapitalizedCalls
2 +
3 +## File
4 +`src/Validation/ValidateNoCapitalizedCalls.ts`
5 +
6 +## Purpose
7 +This validation pass ensures that capitalized functions are not called directly in a component. In React, capitalized functions are conventionally reserved for components, which should be invoked via JSX syntax rather than direct function calls.
8 +
9 +Direct calls to capitalized functions can cause issues because:
10 +1. Components may contain hooks, and calling them directly violates the Rules of Hooks
11 +2. The React runtime expects components to be rendered via JSX for proper reconciliation
12 +3. Direct calls bypass React's rendering lifecycle and state management
13 +
14 +This validation is opt-in and controlled by the `validateNoCapitalizedCalls` configuration option.
15 +
16 +## Input Invariants
17 +- The function has been lowered to HIR
18 +- Global bindings have been resolved
19 +- The `validateNoCapitalizedCalls` configuration option is enabled (via pragma or config)
20 +
21 +## Validation Rules
22 +
23 +### Rule 1: No Direct Calls to Capitalized Globals
24 +Capitalized global functions (not in the allowlist) cannot be called directly.
25 +
26 +**Error:**
27 +```
28 +Error: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
29 +
30 +[FunctionName] may be a component.
31 +```
32 +
33 +### Rule 2: No Direct Method Calls to Capitalized Properties
34 +Capitalized methods on objects cannot be called directly.
35 +
36 +**Error:**
37 +```
38 +Error: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
39 +
40 +[MethodName] may be a component.
41 +```
42 +
43 +## Algorithm
44 +
45 +### Phase 1: Build Allowlist
46 +```typescript
47 +const ALLOW_LIST = new Set([
48 + ...DEFAULT_GLOBALS.keys(), // Built-in globals (Array, Object, etc.)
49 + ...(envConfig.validateNoCapitalizedCalls ?? []), // User-configured allowlist
50 +]);
51 +
52 +const hookPattern = envConfig.hookPattern != null
53 + ? new RegExp(envConfig.hookPattern)
54 + : null;
55 +
56 +const isAllowed = (name: string): boolean => {
57 + return ALLOW_LIST.has(name) ||
58 + (hookPattern != null && hookPattern.test(name));
59 +};
60 +```
61 +
62 +### Phase 2: Track Capitalized Globals and Properties
63 +```typescript
64 +const capitalLoadGlobals = new Map<IdentifierId, string>();
65 +const capitalizedProperties = new Map<IdentifierId, string>();
66 +```
67 +
68 +### Phase 3: Scan Instructions
69 +```typescript
70 +for (const instr of block.instructions) {
71 + switch (value.kind) {
72 + case 'LoadGlobal':
73 + // Track capitalized globals (excluding CONSTANTS)
74 + if (
75 + value.binding.name !== '' &&
76 + /^[A-Z]/.test(value.binding.name) &&
77 + !(value.binding.name.toUpperCase() === value.binding.name) &&
78 + !isAllowed(value.binding.name)
79 + ) {
80 + capitalLoadGlobals.set(lvalue.identifier.id, value.binding.name);
81 + }
82 + break;
83 +
84 + case 'CallExpression':
85 + // Check if calling a tracked capitalized global
86 + const calleeName = capitalLoadGlobals.get(value.callee.identifier.id);
87 + if (calleeName != null) {
88 + CompilerError.throwInvalidReact({
89 + reason: 'Capitalized functions are reserved for components...',
90 + description: `${calleeName} may be a component`,
91 + ...
92 + });
93 + }
94 + break;
95 +
96 + case 'PropertyLoad':
97 + // Track capitalized properties
98 + if (typeof value.property === 'string' && /^[A-Z]/.test(value.property)) {
99 + capitalizedProperties.set(lvalue.identifier.id, value.property);
100 + }
101 + break;
102 +
103 + case 'MethodCall':
104 + // Check if calling a tracked capitalized property
105 + const propertyName = capitalizedProperties.get(value.property.identifier.id);
106 + if (propertyName != null) {
107 + errors.push({
108 + reason: 'Capitalized functions are reserved for components...',
109 + description: `${propertyName} may be a component`,
110 + ...
111 + });
112 + }
113 + break;
114 + }
115 +}
116 +```
117 +
118 +## Edge Cases
119 +
120 +### ALL_CAPS Constants
121 +Functions with names that are entirely uppercase (like `CONSTANTS`) are not flagged:
122 +```javascript
123 +const x = MY_CONSTANT(); // Not an error - all caps indicates a constant, not a component
124 +const y = MyComponent(); // Error - PascalCase indicates a component
125 +```
126 +
127 +### Built-in Globals
128 +The default globals from `DEFAULT_GLOBALS` are automatically allowlisted:
129 +```javascript
130 +const arr = Array(5); // OK - Array is a built-in
131 +const obj = Object.create(null); // OK - Object is a built-in
132 +```
133 +
134 +### User-Configured Allowlist
135 +Users can allowlist specific functions via configuration:
136 +```typescript
137 +validateNoCapitalizedCalls: ['MyUtility', 'SomeFactory']
138 +```
139 +
140 +### Hook Patterns
141 +Functions matching the configured hook pattern are allowed even if capitalized:
142 +```typescript
143 +// With hookPattern: 'React\\$use.*'
144 +const x = React$useState(); // Allowed if it matches the hook pattern
145 +```
146 +
147 +### Method Calls vs Function Calls
148 +Both direct function calls and method calls on objects are checked:
149 +```javascript
150 +MyComponent(); // Error - direct call
151 +someObject.MyComponent(); // Error - method call
152 +```
153 +
154 +### Chained Property Access
155 +Only the immediate property being called is checked:
156 +```javascript
157 +a.b.MyComponent(); // Only checks if MyComponent is capitalized
158 +```
159 +
160 +## TODOs
161 +None in the source file.
162 +
163 +## Example
164 +
165 +### Fixture: `error.capitalized-function-call.js`
166 +
167 +**Input:**
168 +```javascript
169 +// @validateNoCapitalizedCalls
170 +function Component() {
171 + const x = SomeFunc();
172 +
173 + return x;
174 +}
175 +```
176 +
177 +**Error:**
178 +```
179 +Error: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
180 +
181 +SomeFunc may be a component.
182 +
183 +error.capitalized-function-call.ts:3:12
184 + 1 | // @validateNoCapitalizedCalls
185 + 2 | function Component() {
186 +> 3 | const x = SomeFunc();
187 + | ^^^^^^^^^^ Capitalized functions are reserved for components...
188 + 4 |
189 + 5 | return x;
190 + 6 | }
191 +```
192 +
193 +### Fixture: `error.capitalized-method-call.js`
194 +
195 +**Input:**
196 +```javascript
197 +// @validateNoCapitalizedCalls
198 +function Component() {
199 + const x = someGlobal.SomeFunc();
200 +
201 + return x;
202 +}
203 +```
204 +
205 +**Error:**
206 +```
207 +Error: Capitalized functions are reserved for components, which must be invoked with JSX. If this is a component, render it with JSX. Otherwise, ensure that it has no hook calls and rename it to begin with a lowercase letter. Alternatively, if you know for a fact that this function is not a component, you can allowlist it via the compiler config
208 +
209 +SomeFunc may be a component.
210 +
211 +error.capitalized-method-call.ts:3:12
212 + 1 | // @validateNoCapitalizedCalls
213 + 2 | function Component() {
214 +> 3 | const x = someGlobal.SomeFunc();
215 + | ^^^^^^^^^^^^^^^^^^^^^ Capitalized functions are reserved for components...
216 + 4 |
217 + 5 | return x;
218 + 6 | }
219 +```
220 +
221 +### Fixture: `capitalized-function-allowlist.js` (No Error)
222 +
223 +**Input:**
224 +```javascript
225 +// @validateNoCapitalizedCalls:["SomeFunc"]
226 +function Component() {
227 + const x = SomeFunc();
228 + return x;
229 +}
230 +```
231 +
232 +**Output:**
233 +Compiles successfully because `SomeFunc` is in the allowlist.
compiler/packages/babel-plugin-react-compiler/docs/passes/43-validateLocalsNotReassignedAfterRender.md new
+321
@@ -0,0 +1,321 @@
1 +# validateLocalsNotReassignedAfterRender
2 +
3 +## File
4 +`src/Validation/ValidateLocalsNotReassignedAfterRender.ts`
5 +
6 +## Purpose
7 +This validation pass prevents a category of bugs where a closure captures a binding from one render but does not update when the binding is reassigned in a later render.
8 +
9 +When the React Compiler memoizes a function, that function captures bindings at the time of creation. If the function is reused across renders (because its dependencies haven't changed), any reassignments to captured variables will affect the wrong binding version. This can cause inconsistent behavior that's difficult to debug.
10 +
11 +The pass detects when:
12 +1. A local variable is reassigned within a function expression
13 +2. That function expression escapes (e.g., passed to useEffect, used as event handler)
14 +3. The reassignment would occur after render completes (in effects or async callbacks)
15 +
16 +## Input Invariants
17 +- The function has been lowered to HIR
18 +- Effects have been inferred for all operands (`operand.effect !== Effect.Unknown`)
19 +- Function signatures have been analyzed for `noAlias` properties
20 +
21 +## Validation Rules
22 +
23 +### Rule 1: No Reassignment After Render
24 +Variables cannot be reassigned in functions that escape to be called after render.
25 +
26 +**Error:**
27 +```
28 +Error: Cannot reassign variable after render completes
29 +
30 +Reassigning `[variable]` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
31 +```
32 +
33 +### Rule 2: No Reassignment in Async Functions
34 +Variables cannot be reassigned within async functions (async functions always execute after render).
35 +
36 +**Error:**
37 +```
38 +Error: Cannot reassign variable in async function
39 +
40 +Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead.
41 +```
42 +
43 +## Algorithm
44 +
45 +### Phase 1: Track Context Variables
46 +Context variables are variables declared in the outer component/hook that are captured by inner functions:
47 +
48 +```typescript
49 +const contextVariables = new Set<IdentifierId>();
50 +
51 +// For DeclareContext in the main function, add to tracking
52 +case 'DeclareContext':
53 + if (!isFunctionExpression) {
54 + contextVariables.add(value.lvalue.place.identifier.id);
55 + }
56 + break;
57 +```
58 +
59 +### Phase 2: Detect Reassigning Functions
60 +The pass tracks which functions contain reassignments to context variables:
61 +
62 +```typescript
63 +const reassigningFunctions = new Map<IdentifierId, Place>();
64 +
65 +case 'FunctionExpression':
66 +case 'ObjectMethod':
67 + // Recursively check if the function reassigns context variables
68 + let reassignment = getContextReassignment(
69 + value.loweredFunc.func,
70 + contextVariables,
71 + true, // isFunctionExpression
72 + isAsync || value.loweredFunc.func.async
73 + );
74 +
75 + // Also check if any captured functions reassign
76 + if (reassignment === null) {
77 + for (const operand of eachInstructionValueOperand(value)) {
78 + const fromOperand = reassigningFunctions.get(operand.identifier.id);
79 + if (fromOperand !== undefined) {
80 + reassignment = fromOperand;
81 + break;
82 + }
83 + }
84 + }
85 +
86 + if (reassignment !== null) {
87 + // If async, error immediately
88 + if (isAsync || value.loweredFunc.func.async) {
89 + throw new CompilerError("Cannot reassign variable in async function");
90 + }
91 + // Otherwise, track this function as reassigning
92 + reassigningFunctions.set(lvalue.identifier.id, reassignment);
93 + }
94 + break;
95 +```
96 +
97 +### Phase 3: Detect Reassignment in Function Expression
98 +Within a function expression, a `StoreContext` to a context variable is a reassignment:
99 +
100 +```typescript
101 +case 'StoreContext':
102 + if (isFunctionExpression) {
103 + if (contextVariables.has(value.lvalue.place.identifier.id)) {
104 + return value.lvalue.place; // Found a reassignment
105 + }
106 + } else {
107 + // In main function, just track the context variable
108 + contextVariables.add(value.lvalue.place.identifier.id);
109 + }
110 + break;
111 +```
112 +
113 +### Phase 4: Propagate Reassignment Through Data Flow
114 +Reassigning functions flow through local/context stores:
115 +
116 +```typescript
117 +case 'StoreLocal':
118 +case 'StoreContext':
119 + const reassignment = reassigningFunctions.get(value.value.identifier.id);
120 + if (reassignment !== undefined) {
121 + reassigningFunctions.set(value.lvalue.place.identifier.id, reassignment);
122 + reassigningFunctions.set(lvalue.identifier.id, reassignment);
123 + }
124 + break;
125 +
126 +case 'LoadLocal':
127 + const reassignment = reassigningFunctions.get(value.place.identifier.id);
128 + if (reassignment !== undefined) {
129 + reassigningFunctions.set(lvalue.identifier.id, reassignment);
130 + }
131 + break;
132 +```
133 +
134 +### Phase 5: Check Escape Points
135 +When a reassigning function is used as an operand with `Effect.Freeze`, it means the function escapes (e.g., passed to a hook, used as a prop):
136 +
137 +```typescript
138 +for (const operand of operands) {
139 + const reassignment = reassigningFunctions.get(operand.identifier.id);
140 + if (reassignment !== undefined) {
141 + if (operand.effect === Effect.Freeze) {
142 + // Function escapes - this is an error
143 + return reassignment;
144 + } else {
145 + // Function doesn't escape yet, propagate to lvalues
146 + for (const lval of eachInstructionLValue(instr)) {
147 + reassigningFunctions.set(lval.identifier.id, reassignment);
148 + }
149 + }
150 + }
151 +}
152 +```
153 +
154 +### Phase 6: Check Terminal Operands
155 +Reassigning functions used in terminal operands (like return) also escape:
156 +
157 +```typescript
158 +for (const operand of eachTerminalOperand(block.terminal)) {
159 + const reassignment = reassigningFunctions.get(operand.identifier.id);
160 + if (reassignment !== undefined) {
161 + return reassignment;
162 + }
163 +}
164 +```
165 +
166 +### NoAlias Optimization
167 +For function calls with `noAlias` signatures, only the callee needs to be checked (not all arguments):
168 +
169 +```typescript
170 +if (value.kind === 'CallExpression') {
171 + const signature = getFunctionCallSignature(fn.env, value.callee.identifier.type);
172 + if (signature?.noAlias) {
173 + operands = [value.callee]; // Only check the callee
174 + }
175 +}
176 +```
177 +
178 +## Edge Cases
179 +
180 +### Nested Async Functions
181 +Async functions are always detected as problematic, regardless of nesting level:
182 +```javascript
183 +function Component() {
184 + let x = 0;
185 + const f = async () => {
186 + const g = () => {
187 + x = 1; // Error: in async context
188 + };
189 + };
190 +}
191 +```
192 +
193 +### Function Composition
194 +If a reassigning function is captured by another function, that outer function is also marked as reassigning:
195 +```javascript
196 +function Component() {
197 + let x = 0;
198 + const reassign = () => { x = 1; };
199 + const wrapper = () => { reassign(); };
200 + useEffect(wrapper); // Error: wrapper contains reassign
201 +}
202 +```
203 +
204 +### NoAlias Functions
205 +Functions with `noAlias` signatures don't let their arguments escape, so passing a reassigning function to them is safe:
206 +```javascript
207 +function Component() {
208 + let x = 0;
209 + const f = () => { x = 1; };
210 + console.log(f); // OK: console.log has noAlias, f doesn't escape
211 +}
212 +```
213 +
214 +### Direct Effect Usage
215 +The most common case is passing a reassigning function to useEffect:
216 +```javascript
217 +function Component() {
218 + let local;
219 + const reassign = () => { local = 'new value'; };
220 + useEffect(() => { reassign(); }, []); // Error
221 +}
222 +```
223 +
224 +## TODOs
225 +None in the source file.
226 +
227 +## Example
228 +
229 +### Fixture: `error.invalid-reassign-local-variable-in-effect.js`
230 +
231 +**Input:**
232 +```javascript
233 +import {useEffect} from 'react';
234 +
235 +function Component() {
236 + let local;
237 +
238 + const reassignLocal = newValue => {
239 + local = newValue;
240 + };
241 +
242 + const onMount = newValue => {
243 + reassignLocal('hello');
244 +
245 + if (local === newValue) {
246 + // Without React Compiler, `reassignLocal` is freshly created
247 + // on each render, capturing a binding to the latest `local`,
248 + // such that invoking reassignLocal will reassign the same
249 + // binding that we are observing in the if condition, and
250 + // we reach this branch
251 + console.log('`local` was updated!');
252 + } else {
253 + // With React Compiler enabled, `reassignLocal` is only created
254 + // once, capturing a binding to `local` in that render pass.
255 + // Therefore, calling `reassignLocal` will reassign the wrong
256 + // version of `local`, and not update the binding we are checking
257 + // in the if condition.
258 + throw new Error('`local` not updated!');
259 + }
260 + };
261 +
262 + useEffect(() => {
263 + onMount();
264 + }, [onMount]);
265 +
266 + return 'ok';
267 +}
268 +```
269 +
270 +**Error:**
271 +```
272 +Error: Cannot reassign variable after render completes
273 +
274 +Reassigning `local` after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.
275 +
276 +error.invalid-reassign-local-variable-in-effect.ts:7:4
277 + 5 |
278 + 6 | const reassignLocal = newValue => {
279 +> 7 | local = newValue;
280 + | ^^^^^ Cannot reassign `local` after render completes
281 + 8 | };
282 + 9 |
283 + 10 | const onMount = newValue => {
284 +```
285 +
286 +### Fixture: `error.invalid-reassign-local-variable-in-async-callback.js`
287 +
288 +**Input:**
289 +```javascript
290 +function Component() {
291 + let value = null;
292 + const reassign = async () => {
293 + await foo().then(result => {
294 + // Reassigning a local variable in an async function is *always* mutating
295 + // after render, so this should error regardless of where this ends up
296 + // getting called
297 + value = result;
298 + });
299 + };
300 +
301 + const onClick = async () => {
302 + await reassign();
303 + };
304 + return <div onClick={onClick}>Click</div>;
305 +}
306 +```
307 +
308 +**Error:**
309 +```
310 +Error: Cannot reassign variable in async function
311 +
312 +Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead.
313 +
314 +error.invalid-reassign-local-variable-in-async-callback.ts:8:6
315 + 6 | // after render, so this should error regardless of where this ends up
316 + 7 | // getting called
317 +> 8 | value = result;
318 + | ^^^^^ Cannot reassign `value`
319 + 9 | });
320 + 10 | };
321 +```
compiler/packages/babel-plugin-react-compiler/docs/passes/44-validateNoSetStateInRender.md new
+133
@@ -0,0 +1,133 @@
1 +# validateNoSetStateInRender
2 +
3 +## File
4 +`src/Validation/ValidateNoSetStateInRender.ts`
5 +
6 +## Purpose
7 +Validates that a component does not unconditionally call `setState` during render, which would cause an infinite update loop. This pass is conservative and may miss some cases (false negatives) but avoids false positives.
8 +
9 +## Input Invariants
10 +- Operates on HIRFunction (pre-reactive scope inference)
11 +- Must run before reactive scope inference
12 +- Uses `computeUnconditionalBlocks` to determine which blocks always execute
13 +
14 +## Validation Rules
15 +This pass detects two types of violations:
16 +
17 +1. **Unconditional setState in render**: Calling `setState` (or a function that transitively calls setState) in a block that always executes during render.
18 +
19 +2. **setState inside useMemo**: Calling `setState` inside a `useMemo` callback, which can cause infinite loops when the memo's dependencies change.
20 +
21 +### Error Messages
22 +
23 +**For unconditional setState in render:**
24 +```
25 +Error: Cannot call setState during render
26 +
27 +Calling setState during render may trigger an infinite loop.
28 +* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
29 +* To derive data from other state/props, compute the derived data during render without using state
30 +```
31 +
32 +**For setState in useMemo:**
33 +```
34 +Error: Calling setState from useMemo may trigger an infinite loop
35 +
36 +Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render.
37 +```
38 +
39 +## Algorithm
40 +1. Compute the set of unconditional blocks using post-dominator analysis
41 +2. Initialize a set `unconditionalSetStateFunctions` to track functions that unconditionally call setState
42 +3. Traverse all blocks and instructions:
43 + - **LoadLocal/StoreLocal**: Propagate setState tracking through variable assignments and loads
44 + - **FunctionExpression/ObjectMethod**: Recursively check if the function unconditionally calls setState. If so, add the function's lvalue to the tracking set
45 + - **StartMemoize/FinishMemoize**: Track when inside a manual memoization block (useMemo/useCallback)
46 + - **CallExpression**: Check if the callee is a setState function or tracked setter:
47 + - If inside a memoize block, emit a useMemo-specific error
48 + - If in an unconditional block, emit a render-time setState error
49 +
50 +### Key Helper: `computeUnconditionalBlocks`
51 +Uses post-dominator tree analysis to find blocks that always execute when the function runs. The analysis ignores throw statements since hooks only need consistent ordering for normal execution paths.
52 +
53 +## Edge Cases
54 +
55 +### Conditional setState is allowed
56 +```javascript
57 +// This is valid - setState is conditional
58 +if (someCondition) {
59 + setState(newValue);
60 +}
61 +```
62 +
63 +### Transitive detection through functions
64 +```javascript
65 +// Detected - setTrue unconditionally calls setState
66 +const setTrue = () => setState(true);
67 +setTrue(); // Error here
68 +```
69 +
70 +### False negative: setState in data structures
71 +```javascript
72 +// NOT detected - setState stored in array then extracted
73 +const [state, setState] = useState(false);
74 +const x = [setState];
75 +const y = x.pop();
76 +y(); // No error, but will cause infinite loop
77 +```
78 +
79 +### Feature flag: enableUseKeyedState
80 +When enabled, the error message suggests using `useKeyedState(initialState, key)` as an alternative pattern for resetting state when dependencies change.
81 +
82 +## TODOs
83 +None in source code.
84 +
85 +## Example
86 +
87 +### Fixture: `error.invalid-unconditional-set-state-in-render.js`
88 +
89 +**Input:**
90 +```javascript
91 +// @validateNoSetStateInRender
92 +function Component(props) {
93 + const [x, setX] = useState(0);
94 + const aliased = setX;
95 +
96 + setX(1);
97 + aliased(2);
98 +
99 + return x;
100 +}
101 +```
102 +
103 +**Error:**
104 +```
105 +Found 2 errors:
106 +
107 +Error: Cannot call setState during render
108 +
109 +Calling setState during render may trigger an infinite loop.
110 +* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
111 +* To derive data from other state/props, compute the derived data during render without using state.
112 +
113 +error.invalid-unconditional-set-state-in-render.ts:6:2
114 + 4 | const aliased = setX;
115 + 5 |
116 +> 6 | setX(1);
117 + | ^^^^ Found setState() in render
118 + 7 | aliased(2);
119 + 8 |
120 + 9 | return x;
121 +
122 +Error: Cannot call setState during render
123 +
124 +...
125 +
126 +error.invalid-unconditional-set-state-in-render.ts:7:2
127 + 5 |
128 + 6 | setX(1);
129 +> 7 | aliased(2);
130 + | ^^^^^^^ Found setState() in render
131 +```
132 +
133 +**Why it fails:** Both `setX(1)` and `aliased(2)` are unconditionally called during render. The pass tracks that `aliased` is assigned from `setX`, so calling `aliased()` is also detected as a setState call.
compiler/packages/babel-plugin-react-compiler/docs/passes/45-validateNoDerivedComputationsInEffects.md new
+141
@@ -0,0 +1,141 @@
1 +# validateNoDerivedComputationsInEffects
2 +
3 +## File
4 +`src/Validation/ValidateNoDerivedComputationsInEffects.ts`
5 +
6 +## Purpose
7 +Validates that `useEffect` is not used for derived computations that could and should be performed during render. This catches a common anti-pattern where developers use effects to synchronize derived state, which causes unnecessary re-renders and complexity.
8 +
9 +See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
10 +
11 +## Input Invariants
12 +- Operates on HIRFunction (pre-reactive scope inference)
13 +- Effect hooks must be identified (`isUseEffectHookType`)
14 +- setState functions must be identified (`isSetStateType`)
15 +
16 +## Validation Rules
17 +The pass detects when an effect:
18 +1. Has a dependency array (2nd argument)
19 +2. The effect function only captures the dependencies and setState functions
20 +3. The effect calls setState with a value derived solely from the dependencies
21 +4. The effect has no control flow (loops with back edges)
22 +
23 +When detected, it produces:
24 +```
25 +Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
26 +```
27 +
28 +## Algorithm
29 +1. **Collection Phase**: Traverse all instructions to collect:
30 + - `candidateDependencies`: Map of ArrayExpression identifiers (potential deps arrays)
31 + - `functions`: Map of FunctionExpression identifiers (potential effect callbacks)
32 + - `locals`: Map of LoadLocal sources for identifier resolution
33 +
34 +2. **Detection Phase**: When a `useEffect` call is found with 2 arguments:
35 + - Look up the effect function and dependencies array
36 + - Verify all dependency array elements are identifiers
37 + - Call `validateEffect()` on the effect function
38 +
39 +3. **Effect Validation** (`validateEffect`):
40 + - Check that the effect only captures dependencies or setState functions
41 + - Check that all dependencies are actually used in the effect
42 + - Skip if any block has a back edge (loop)
43 + - Track data flow through instructions:
44 + - `LoadLocal`: Propagate dependency tracking
45 + - `PropertyLoad`, `BinaryExpression`, `TemplateLiteral`, `CallExpression`, `MethodCall`: Aggregate dependencies from operands
46 + - When `setState` is called with a single argument that depends on ALL effect dependencies, record the location
47 + - If any dependency is used in a terminal operand (control flow), abort validation
48 + - Push errors for all recorded setState locations
49 +
50 +### Value Tracking
51 +The pass maintains a `values` map from `IdentifierId` to `Array<IdentifierId>` tracking which effect dependencies each value derives from. When setState is called, if the argument derives from all dependencies, it's flagged as a derived computation.
52 +
53 +## Edge Cases
54 +
55 +### Allowed: Effects with side effects
56 +```javascript
57 +// Valid - effect captures external values, not just deps
58 +useEffect(() => {
59 + logToServer(firstName);
60 + setFullName(firstName);
61 +}, [firstName]);
62 +```
63 +
64 +### Allowed: Effects with loops
65 +```javascript
66 +// Valid - has control flow, not a simple derivation
67 +useEffect(() => {
68 + let result = '';
69 + for (const item of items) {
70 + result += item;
71 + }
72 + setResult(result);
73 +}, [items]);
74 +```
75 +
76 +### Allowed: Effects with conditional setState
77 +```javascript
78 +// Valid - setState is conditional on control flow
79 +useEffect(() => {
80 + if (condition) {
81 + setFullName(firstName + lastName);
82 + }
83 +}, [firstName, lastName]);
84 +```
85 +
86 +### Not detected: Subset of dependencies
87 +```javascript
88 +// Not flagged - only uses firstName, not lastName
89 +useEffect(() => {
90 + setResult(firstName);
91 +}, [firstName, lastName]);
92 +```
93 +
94 +## TODOs
95 +None in source code.
96 +
97 +## Example
98 +
99 +### Fixture: `error.invalid-derived-computation-in-effect.js`
100 +
101 +**Input:**
102 +```javascript
103 +// @validateNoDerivedComputationsInEffects
104 +import {useEffect, useState} from 'react';
105 +
106 +function BadExample() {
107 + const [firstName, setFirstName] = useState('Taylor');
108 + const [lastName, setLastName] = useState('Swift');
109 +
110 + // Avoid: redundant state and unnecessary Effect
111 + const [fullName, setFullName] = useState('');
112 + useEffect(() => {
113 + setFullName(firstName + ' ' + lastName);
114 + }, [firstName, lastName]);
115 +
116 + return <div>{fullName}</div>;
117 +}
118 +```
119 +
120 +**Error:**
121 +```
122 +Found 1 error:
123 +
124 +Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
125 +
126 +error.invalid-derived-computation-in-effect.ts:11:4
127 + 9 | const [fullName, setFullName] = useState('');
128 + 10 | useEffect(() => {
129 +> 11 | setFullName(firstName + ' ' + lastName);
130 + | ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect.
131 + 12 | }, [firstName, lastName]);
132 + 13 |
133 + 14 | return <div>{fullName}</div>;
134 +```
135 +
136 +**Why it fails:** The effect computes `fullName` purely from `firstName` and `lastName` (the dependencies) and then sets state. This is a derived computation that should be calculated during render:
137 +
138 +```javascript
139 +// Correct approach
140 +const fullName = firstName + ' ' + lastName;
141 +```
compiler/packages/babel-plugin-react-compiler/docs/passes/46-validateNoSetStateInEffects.md new
+150
@@ -0,0 +1,150 @@
1 +# validateNoSetStateInEffects
2 +
3 +## File
4 +`src/Validation/ValidateNoSetStateInEffects.ts`
5 +
6 +## Purpose
7 +Validates against calling `setState` synchronously in the body of an effect (`useEffect`, `useLayoutEffect`, `useInsertionEffect`), while allowing `setState` in callbacks scheduled by the effect. Synchronous setState in effects triggers cascading re-renders which hurts performance.
8 +
9 +See: https://react.dev/learn/you-might-not-need-an-effect
10 +
11 +## Input Invariants
12 +- Operates on HIRFunction (pre-reactive scope inference)
13 +- Effect hooks must be identified (`isUseEffectHookType`, `isUseLayoutEffectHookType`, `isUseInsertionEffectHookType`)
14 +- setState functions must be identified (`isSetStateType`)
15 +- Only runs when `outputMode === 'lint'`
16 +
17 +## Validation Rules
18 +This pass detects synchronous setState calls within effect bodies:
19 +
20 +**Standard error message:**
21 +```
22 +Error: Calling setState synchronously within an effect can trigger cascading renders
23 +
24 +Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
25 +* Update external systems with the latest state from React.
26 +* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
27 +
28 +Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended.
29 +```
30 +
31 +**Verbose error message** (when `enableVerboseNoSetStateInEffect` is enabled):
32 +Provides more detailed guidance about specific anti-patterns like non-local derived data, derived event patterns, and force update patterns.
33 +
34 +## Algorithm
35 +1. **Main function traversal**: Build a map `setStateFunctions` tracking which identifiers are setState functions
36 +2. For each instruction:
37 + - **LoadLocal/StoreLocal**: Propagate setState tracking through variable assignments
38 + - **FunctionExpression**: Check if the function synchronously calls setState by recursively calling `getSetStateCall()`. If so, track the function as a setState-calling function
39 + - **useEffectEvent call**: If the argument is a function that calls setState, track the return value as a setState function
40 + - **useEffect/useLayoutEffect/useInsertionEffect call**: Check if the callback argument is tracked as calling setState. If so, emit an error
41 +
42 +3. **`getSetStateCall()` helper**: Recursively analyzes a function to find synchronous setState calls:
43 + - Tracks ref-derived values when `enableAllowSetStateFromRefsInEffects` is enabled
44 + - Propagates setState tracking through local variables
45 + - Returns the Place of the setState call if found, null otherwise
46 +
47 +### Ref-derived setState exception
48 +When `enableAllowSetStateFromRefsInEffects` is enabled, the pass allows setState calls where:
49 +- The value being set is derived from a ref (`useRef` or `ref.current`)
50 +- The block containing setState is controlled by a ref-dependent condition
51 +
52 +This allows patterns like storing initial layout measurements from refs in state.
53 +
54 +## Edge Cases
55 +
56 +### Allowed: setState in callbacks
57 +```javascript
58 +// Valid - setState in event callback, not synchronous
59 +useEffect(() => {
60 + const handler = () => {
61 + setState(newValue);
62 + };
63 + window.addEventListener('resize', handler);
64 + return () => window.removeEventListener('resize', handler);
65 +}, []);
66 +```
67 +
68 +### Transitive detection
69 +```javascript
70 +// Detected - transitive through function calls
71 +const f = () => setState(value);
72 +const g = () => f();
73 +useEffect(() => {
74 + g(); // Error: calls setState transitively
75 +});
76 +```
77 +
78 +### useEffectEvent tracking
79 +```javascript
80 +// Detected - useEffectEvent that calls setState is tracked
81 +const handler = useEffectEvent(() => {
82 + setState(value);
83 +});
84 +useEffect(() => {
85 + handler(); // Error: handler calls setState
86 +});
87 +```
88 +
89 +### Allowed: Ref-derived state (with flag)
90 +```javascript
91 +// Valid when enableAllowSetStateFromRefsInEffects is true
92 +const ref = useRef(null);
93 +useEffect(() => {
94 + const width = ref.current.offsetWidth;
95 + setWidth(width); // Allowed - derived from ref
96 +}, []);
97 +```
98 +
99 +## TODOs
100 +From the source code:
101 +```typescript
102 +/*
103 + * TODO: once we support multiple locations per error, we should link to the
104 + * original Place in the case that setStateFunction.has(callee)
105 + */
106 +```
107 +
108 +## Example
109 +
110 +### Fixture: `invalid-setState-in-useEffect-transitive.js`
111 +
112 +**Input:**
113 +```javascript
114 +// @loggerTestOnly @validateNoSetStateInEffects @outputMode:"lint"
115 +import {useEffect, useState} from 'react';
116 +
117 +function Component() {
118 + const [state, setState] = useState(0);
119 + const f = () => {
120 + setState(s => s + 1);
121 + };
122 + const g = () => {
123 + f();
124 + };
125 + useEffect(() => {
126 + g();
127 + });
128 + return state;
129 +}
130 +```
131 +
132 +**Error:**
133 +```
134 +Error: Calling setState synchronously within an effect can trigger cascading renders
135 +
136 +Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
137 +* Update external systems with the latest state from React.
138 +* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
139 +
140 +Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended.
141 +
142 +invalid-setState-in-useEffect-transitive.ts:13:4
143 + 11 | };
144 + 12 | useEffect(() => {
145 +> 13 | g();
146 + | ^ Avoid calling setState() directly within an effect
147 + 14 | });
148 +```
149 +
150 +**Why it fails:** Even though `setState` is not called directly in the effect, the pass traces through `g()` -> `f()` -> `setState()` and detects that the effect synchronously triggers a state update.
compiler/packages/babel-plugin-react-compiler/docs/passes/47-validateNoJSXInTryStatement.md new
+167
@@ -0,0 +1,167 @@
1 +# validateNoJSXInTryStatement
2 +
3 +## File
4 +`src/Validation/ValidateNoJSXInTryStatement.ts`
5 +
6 +## Purpose
7 +Validates that JSX is not created within a try block. Developers may incorrectly assume that wrapping JSX in try/catch will catch rendering errors, but React does not immediately render components when JSX is created - JSX is just a description of UI that will be rendered later. Error boundaries should be used instead.
8 +
9 +See: https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
10 +
11 +## Input Invariants
12 +- Operates on HIRFunction (pre-reactive scope inference)
13 +- Blocks are traversed in order
14 +- Only runs when `outputMode === 'lint'`
15 +
16 +## Validation Rules
17 +The pass errors when `JsxExpression` or `JsxFragment` instructions are found within a try block.
18 +
19 +**Error message:**
20 +```
21 +Error: Avoid constructing JSX within try/catch
22 +
23 +React does not immediately render components when JSX is rendered, so any errors from this component will not be caught by the try/catch. To catch errors in rendering a given component, wrap that component in an error boundary.
24 +```
25 +
26 +### Important distinction
27 +- JSX in a **try block**: Error
28 +- JSX in a **catch block** (not nested in outer try): Allowed
29 +- JSX in a **catch block** (nested in outer try): Error
30 +
31 +## Algorithm
32 +1. Maintain a stack `activeTryBlocks` of currently active try statement handler block IDs
33 +2. For each block:
34 + - Remove the current block from `activeTryBlocks` if it matches a handler (we've exited the try scope)
35 + - If `activeTryBlocks` is not empty (we're inside a try block):
36 + - Check each instruction for `JsxExpression` or `JsxFragment`
37 + - If found, push an error
38 + - If the block's terminal is a `try` terminal, push its handler block ID to `activeTryBlocks`
39 +
40 +### Block tracking with `retainWhere`
41 +The `retainWhere` utility is used to remove the current block from `activeTryBlocks` at the start of each block. When we reach a catch handler block, it gets removed from the active list, allowing JSX in catch blocks (unless there's an outer try).
42 +
43 +## Edge Cases
44 +
45 +### Allowed: JSX in catch (no outer try)
46 +```javascript
47 +// Valid - catch block is not inside a try
48 +function Component() {
49 + try {
50 + doSomething();
51 + } catch {
52 + return <ErrorMessage />; // OK
53 + }
54 +}
55 +```
56 +
57 +### Error: JSX in catch with outer try
58 +```javascript
59 +// Error - catch is inside outer try
60 +function Component() {
61 + try {
62 + try {
63 + doSomething();
64 + } catch {
65 + return <ErrorMessage />; // Error!
66 + }
67 + } catch {
68 + return null;
69 + }
70 +}
71 +```
72 +
73 +### Error: JSX assigned in try
74 +```javascript
75 +// Error - JSX creation is in try block
76 +function Component() {
77 + let el;
78 + try {
79 + el = <div />; // Error here
80 + } catch {
81 + return null;
82 + }
83 + return el;
84 +}
85 +```
86 +
87 +### Finally blocks
88 +The validation currently has TODOs for handling try/catch/finally properly. Files like `error.todo-invalid-jsx-in-try-with-finally.js` indicate these are known unsupported cases.
89 +
90 +## TODOs
91 +Based on fixture naming patterns:
92 +- `error.todo-invalid-jsx-in-try-with-finally.js` - Try blocks with finally clauses
93 +- `error.todo-invalid-jsx-in-catch-in-outer-try-with-finally.js` - Nested try/catch in try with finally
94 +
95 +## Example
96 +
97 +### Fixture: `invalid-jsx-in-try-with-catch.js`
98 +
99 +**Input:**
100 +```javascript
101 +// @loggerTestOnly @validateNoJSXInTryStatements @outputMode:"lint"
102 +function Component(props) {
103 + let el;
104 + try {
105 + el = <div />;
106 + } catch {
107 + return null;
108 + }
109 + return el;
110 +}
111 +```
112 +
113 +**Error:**
114 +```
115 +Error: Avoid constructing JSX within try/catch
116 +
117 +React does not immediately render components when JSX is rendered, so any errors from this component will not be caught by the try/catch. To catch errors in rendering a given component, wrap that component in an error boundary.
118 +
119 +invalid-jsx-in-try-with-catch.ts:5:9
120 + 3 | let el;
121 + 4 | try {
122 +> 5 | el = <div />;
123 + | ^^^^^^^ Avoid constructing JSX within try/catch
124 + 6 | } catch {
125 + 7 | return null;
126 + 8 | }
127 +```
128 +
129 +**Why it fails:** The `<div />` JSX element is created inside a try block. If the developer expects this to catch errors from rendering the div, they will be surprised - the try/catch will only catch errors from creating the JSX object (which is rare), not from React actually rendering it later. The correct approach is to use an error boundary component to catch rendering errors.
130 +
131 +### Fixture: `invalid-jsx-in-catch-in-outer-try-with-catch.js`
132 +
133 +**Input:**
134 +```javascript
135 +// @loggerTestOnly @validateNoJSXInTryStatements @outputMode:"lint"
136 +import {identity} from 'shared-runtime';
137 +
138 +function Component(props) {
139 + let el;
140 + try {
141 + let value;
142 + try {
143 + value = identity(props.foo);
144 + } catch {
145 + el = <div value={value} />;
146 + }
147 + } catch {
148 + return null;
149 + }
150 + return el;
151 +}
152 +```
153 +
154 +**Error:**
155 +```
156 +Error: Avoid constructing JSX within try/catch
157 +
158 +...
159 +
160 +invalid-jsx-in-catch-in-outer-try-with-catch.ts:11:11
161 + 9 | value = identity(props.foo);
162 + 10 | } catch {
163 +> 11 | el = <div value={value} />;
164 + | ^^^^^^^^^^^^^^^^^^^^^ Avoid constructing JSX within try/catch
165 +```
166 +
167 +**Why it fails:** Even though the JSX is in a catch block, that catch block is itself inside an outer try block. The outer try's catch won't catch rendering errors from the JSX any more than the inner try would.
compiler/packages/babel-plugin-react-compiler/docs/passes/48-validateNoImpureValuesInRender.md new
+180
@@ -0,0 +1,180 @@
1 +# validateNoImpureValuesInRender
2 +
3 +## File
4 +`src/Validation/ValidateNoImpureValuesInRender.ts`
5 +
6 +## Purpose
7 +This validation pass ensures that impure values (values derived from non-deterministic function calls) are not used in render output. Impure values can produce unstable results that update unpredictably when the component re-renders, violating React's requirement that components be pure and idempotent.
8 +
9 +The pass tracks values produced by impure functions (like `Date.now()`, `Math.random()`, `performance.now()`) and errors if those values flow into JSX props, component return values, or other render-time contexts.
10 +
11 +## Input Invariants
12 +- The function has been through effect inference
13 +- Aliasing effects have been computed on instructions
14 +- `Impure` effects mark values from non-deterministic sources
15 +- `Render` effects mark values used in render context
16 +
17 +## Validation Rules
18 +The pass produces errors when:
19 +
20 +1. **Impure value in render context**: A value marked with an `Impure` effect flows into a position marked with a `Render` effect
21 +2. **Impure function returns in render**: A function that returns an impure value is called during render
22 +
23 +Error messages produced:
24 +- Category: `ImpureValues`
25 +- Reason: "Cannot access impure value during render"
26 +- Description: "Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render."
27 +
28 +The error points to two locations:
29 +1. Where the impure value is used in render (e.g., as a JSX prop)
30 +2. Where the impure value originates (e.g., the `Date.now()` call)
31 +
32 +## Algorithm
33 +
34 +### Phase 1: Infer Impure Values
35 +The pass iterates over all instructions to build a map of which identifiers contain impure values:
36 +
37 +```typescript
38 +function inferImpureValues(
39 + fn: HIRFunction,
40 + impure: Map<IdentifierId, ImpureEffect>,
41 + impureFunctions: Map<IdentifierId, ImpuritySignature>,
42 + cache: FunctionCache,
43 +): ImpuritySignature
44 +```
45 +
46 +The algorithm uses a fixed-point iteration that propagates impurity through data flow:
47 +
48 +1. **Process phi nodes**: If any operand of a phi is impure, the phi result is impure
49 +2. **Process effects**: For each instruction's effects:
50 + - `Impure` effect: Mark the destination identifier as impure
51 + - `Alias/Assign/Capture/CreateFrom/ImmutableCapture`: Propagate impurity from source to destination
52 + - `CreateFunction`: Recursively analyze function expressions
53 + - `Apply`: When calling a function with an impurity signature, propagate impurity to call results
54 +
55 +3. **Control flow sensitivity**: The pass also considers control-flow dominators to detect impure values that flow through conditional branches
56 +
57 +### Phase 2: Validate Render Effects
58 +After impurity inference converges, the pass validates all `Render` effects:
59 +
60 +```typescript
61 +function validateRenderEffect(effect: RenderEffect): void {
62 + const impureEffect = impure.get(effect.place.identifier.id);
63 + if (impureEffect != null) {
64 + // Emit error
65 + }
66 +}
67 +```
68 +
69 +### Special Cases
70 +- Values stored in refs (`isUseRefType`) are allowed to be impure since refs are not rendered
71 +- JSX elements are excluded from impurity propagation (`isJsxType`)
72 +
73 +## Edge Cases
74 +
75 +### Impure Values Through Helper Functions
76 +If a helper function returns an impure value and is called during render, both the call site and the original impure source are reported:
77 +
78 +```javascript
79 +function Component() {
80 + const now = () => Date.now(); // Source of impurity
81 + const render = () => {
82 + return <div>{now()}</div>; // Error: impure value in render
83 + };
84 + return <div>{render()}</div>; // Error: impure value in render
85 +}
86 +```
87 +
88 +### Indirect Impurity Through Mutation
89 +When an impure value is captured into another value through mutation, the destination becomes impure:
90 +
91 +```javascript
92 +function Component() {
93 + const obj = {};
94 + obj.time = Date.now(); // obj becomes impure
95 + return <Foo obj={obj} />; // Error
96 +}
97 +```
98 +
99 +### Phi Node Propagation
100 +Impurity propagates through control flow merges:
101 +
102 +```javascript
103 +function Component({cond}) {
104 + let x;
105 + if (cond) {
106 + x = Date.now(); // Impure path
107 + } else {
108 + x = 0; // Pure path
109 + }
110 + return <Foo x={x} />; // Error: x may be impure
111 +}
112 +```
113 +
114 +## TODOs
115 +From the source file:
116 +
117 +```typescript
118 +/**
119 + * TODO: consider propagating impurity for assignments/mutations that
120 + * are controlled by an impure value.
121 + *
122 + * Example: This should error since we know the semantics of array.push,
123 + * it's a definite Mutate and definite Capture, not maybemutate+maybecapture:
124 + *
125 + * let x = [];
126 + * if (Date.now() < START_DATE) {
127 + * x.push(1);
128 + * }
129 + * return <Foo x={x} />
130 + */
131 +```
132 +
133 +## Example
134 +
135 +### Fixture: `error.invalid-impure-functions-in-render.js`
136 +
137 +**Input:**
138 +```javascript
139 +// @validateNoImpureFunctionsInRender
140 +
141 +function Component() {
142 + const date = Date.now();
143 + const now = performance.now();
144 + const rand = Math.random();
145 + return <Foo date={date} now={now} rand={rand} />;
146 +}
147 +```
148 +
149 +**Error:**
150 +```
151 +Found 3 errors:
152 +
153 +Error: Cannot access impure value during render
154 +
155 +Calling an impure function can produce unstable results that update unpredictably
156 +when the component happens to re-render.
157 +(https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
158 +
159 +error.invalid-impure-functions-in-render.ts:7:20
160 + 5 | const now = performance.now();
161 + 6 | const rand = Math.random();
162 +> 7 | return <Foo date={date} now={now} rand={rand} />;
163 + | ^^^^ Cannot access impure value during render
164 + 8 | }
165 +
166 +error.invalid-impure-functions-in-render.ts:4:15
167 + 2 |
168 + 3 | function Component() {
169 +> 4 | const date = Date.now();
170 + | ^^^^^^^^^^ `Date.now` is an impure function.
171 + 5 | const now = performance.now();
172 +
173 +Error: Cannot access impure value during render
174 +...
175 +```
176 +
177 +Key observations:
178 +- Each impure function call (`Date.now`, `performance.now`, `Math.random`) produces a separate error
179 +- The error shows both the usage location (in JSX) and the source location (the impure call)
180 +- The pass is enabled via the `@validateNoImpureFunctionsInRender` pragma
compiler/packages/babel-plugin-react-compiler/docs/passes/49-validateNoRefAccessInRender.md new
+268
@@ -0,0 +1,268 @@
1 +# validateNoRefAccessInRender
2 +
3 +## File
4 +`src/Validation/ValidateNoRefAccessInRender.ts`
5 +
6 +## Purpose
7 +This validation pass ensures that React refs are not mutated during render. Refs are mutable containers for values that are not needed for rendering. Accessing or mutating `ref.current` during render can cause components to not update as expected because React does not track ref mutations.
8 +
9 +The pass validates both direct ref mutations at the component level and ref mutations inside functions that are called during render.
10 +
11 +## Input Invariants
12 +- The function has been through type inference
13 +- Ref types are properly identified (`useRef` return values)
14 +- Function expressions have been lowered
15 +
16 +## Validation Rules
17 +The pass produces errors for:
18 +
19 +1. **Direct ref mutation in render**: Assigning to `ref.current` at the top level of a component
20 +2. **Ref mutation in render helper**: Mutating a ref inside a function that is called during render
21 +3. **Duplicate ref initialization**: Initializing a ref more than once within null-guard blocks
22 +
23 +**Exception - Null-guard initialization pattern**: The pass allows a single initialization of `ref.current` inside an `if (ref.current == null)` block. This is a common pattern for lazy initialization:
24 +
25 +```javascript
26 +// ALLOWED - null-guard initialization
27 +if (ref.current == null) {
28 + ref.current = expensiveComputation();
29 +}
30 +```
31 +
32 +Error messages produced:
33 +- Category: `Refs`
34 +- Reason: "Cannot access refs during render"
35 +- Messages:
36 + - "Cannot update ref during render"
37 + - "Ref is initialized more than once during render"
38 + - "Ref was first initialized here" (for duplicate initialization)
39 +
40 +## Algorithm
41 +
42 +### Phase 1: Initialize Ref Tracking
43 +Track refs from function parameters and context (captured variables):
44 +
45 +```typescript
46 +for (const param of fn.params) {
47 + if (isUseRefType(place.identifier)) {
48 + refs.set(place.identifier.id, {kind: 'Ref', refId: makeRefId()});
49 + }
50 +}
51 +```
52 +
53 +### Phase 2: Single Forward Pass
54 +Process all blocks in order, tracking:
55 +- `refs`: Map of identifier IDs to ref information
56 +- `nullables`: Set of identifiers known to be null/undefined
57 +- `guards`: Map of comparison results (e.g., `ref.current == null`)
58 +- `safeBlocks`: Map of blocks where null-guard allows initialization
59 +- `refMutatingFunctions`: Map of function identifiers that mutate refs
60 +
61 +### Phase 3: Process Instructions
62 +For each instruction, handle:
63 +
64 +```typescript
65 +switch (value.kind) {
66 + case 'PropertyLoad': {
67 + // Track ref.current access
68 + if (objRef?.kind === 'Ref' && value.property === 'current') {
69 + refs.set(lvalue.identifier.id, {kind: 'RefValue', refId: objRef.refId});
70 + }
71 + break;
72 + }
73 + case 'PropertyStore': {
74 + // Check for ref mutation
75 + if (isRef && isCurrentProperty && !isNullGuardInit) {
76 + if (isTopLevel) {
77 + errors.pushDiagnostic(makeRefMutationError(instr.loc));
78 + }
79 + return mutation;
80 + }
81 + break;
82 + }
83 + case 'FunctionExpression': {
84 + // Recursively validate with isTopLevel=false
85 + const mutation = validateFunction(..., false, errors);
86 + if (mutation != null) {
87 + refMutatingFunctions.set(lvalue.identifier.id, mutation);
88 + }
89 + break;
90 + }
91 + case 'CallExpression': {
92 + // Check if calling a ref-mutating function
93 + if (refMutatingFunctions.has(callee.identifier.id) && isTopLevel) {
94 + errors.pushDiagnostic(makeRefMutationError(mutationInfo.loc));
95 + }
96 + break;
97 + }
98 +}
99 +```
100 +
101 +### Phase 4: Guard Detection and Propagation
102 +When encountering an `if` terminal with a null-guard condition:
103 +
104 +```typescript
105 +if (block.terminal.kind === 'if') {
106 + const guard = guards.get(block.terminal.test.identifier.id);
107 + if (guard != null) {
108 + // For equality checks (==, ===), consequent is safe
109 + // For inequality checks (!=, !==), alternate is safe
110 + const safeBlock = guard.isEquality
111 + ? block.terminal.consequent
112 + : block.terminal.alternate;
113 + // Propagate safety through control flow
114 + }
115 +}
116 +```
117 +
118 +## Edge Cases
119 +
120 +### Null-Guard Initialization Pattern (Allowed)
121 +```javascript
122 +function Component() {
123 + const ref = useRef(null);
124 + if (ref.current == null) {
125 + ref.current = computeValue(); // OK - first initialization
126 + }
127 + return <div />;
128 +}
129 +```
130 +
131 +### Duplicate Initialization (Error)
132 +```javascript
133 +function Component() {
134 + const ref = useRef(null);
135 + if (ref.current == null) {
136 + ref.current = value1; // First init - tracked
137 + }
138 + if (ref.current == null) {
139 + ref.current = value2; // Error: duplicate initialization
140 + }
141 +}
142 +```
143 +
144 +### Negated Null Check
145 +The pass correctly handles negated null checks:
146 +```javascript
147 +if (ref.current !== null) {
148 + // NOT safe for initialization
149 +} else {
150 + // Safe for initialization (ref.current is null here)
151 +}
152 +```
153 +
154 +### Ref Mutation in Called Function
155 +```javascript
156 +function Component(props) {
157 + const ref = useRef(null);
158 + const renderItem = item => {
159 + ref.current = item; // Mutation tracked in function
160 + return <Item item={item} />;
161 + };
162 + // Error: calling function that mutates ref during render
163 + return <List>{props.items.map(renderItem)}</List>;
164 +}
165 +```
166 +
167 +### Ref Mutation in Event Handler (Allowed)
168 +```javascript
169 +function Component() {
170 + const ref = useRef(null);
171 + const onClick = () => {
172 + ref.current = value; // OK - not called during render
173 + };
174 + return <button onClick={onClick} />; // onClick is passed, not called
175 +}
176 +```
177 +
178 +### Arbitrary Comparison Values (Error)
179 +Only `null` or `undefined` comparisons are recognized as null guards:
180 +```javascript
181 +const DEFAULT_VALUE = 1;
182 +if (ref.current == DEFAULT_VALUE) {
183 + ref.current = 1; // Error: not a null guard
184 +}
185 +```
186 +
187 +## TODOs
188 +None in the source file.
189 +
190 +## Example
191 +
192 +### Fixture: `error.invalid-disallow-mutating-ref-in-render.js`
193 +
194 +**Input:**
195 +```javascript
196 +// @validateRefAccessDuringRender
197 +function Component() {
198 + const ref = useRef(null);
199 + ref.current = false;
200 +
201 + return <button ref={ref} />;
202 +}
203 +```
204 +
205 +**Error:**
206 +```
207 +Found 1 error:
208 +
209 +Error: Cannot access refs during render
210 +
211 +React refs are values that are not needed for rendering. Refs should only be
212 +accessed outside of render, such as in event handlers or effects. Accessing a
213 +ref value (the `current` property) during render can cause your component not
214 +to update as expected (https://react.dev/reference/react/useRef).
215 +
216 +error.invalid-disallow-mutating-ref-in-render.ts:4:2
217 + 2 | function Component() {
218 + 3 | const ref = useRef(null);
219 +> 4 | ref.current = false;
220 + | ^^^^^^^^^^^ Cannot update ref during render
221 + 5 |
222 + 6 | return <button ref={ref} />;
223 + 7 | }
224 +```
225 +
226 +### Fixture: `error.invalid-ref-in-callback-invoked-during-render.js`
227 +
228 +**Input:**
229 +```javascript
230 +// @validateRefAccessDuringRender
231 +function Component(props) {
232 + const ref = useRef(null);
233 + const renderItem = item => {
234 + const current = ref.current;
235 + return <Foo item={item} current={current} />;
236 + };
237 + return <Items>{props.items.map(item => renderItem(item))}</Items>;
238 +}
239 +```
240 +
241 +**Error:**
242 +```
243 +Found 1 error:
244 +
245 +Error: Cannot access ref value during render
246 +
247 +React refs are values that are not needed for rendering...
248 +
249 +error.invalid-ref-in-callback-invoked-during-render.ts:6:37
250 + 4 | const renderItem = item => {
251 + 5 | const current = ref.current;
252 +> 6 | return <Foo item={item} current={current} />;
253 + | ^^^^^^^ Ref value is used during render
254 + 7 | };
255 + 8 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
256 +
257 +error.invalid-ref-in-callback-invoked-during-render.ts:5:20
258 + 3 | const ref = useRef(null);
259 + 4 | const renderItem = item => {
260 +> 5 | const current = ref.current;
261 + | ^^^^^^^^^^^ Ref is initially accessed
262 +```
263 +
264 +Key observations:
265 +- Direct mutation at render level is an immediate error
266 +- Functions that mutate refs are tracked; errors occur when those functions are called at render level
267 +- The null-guard pattern allows a single initialization
268 +- The pass distinguishes between refs (`useRef` return type) and ref values (`.current` property)
compiler/packages/babel-plugin-react-compiler/docs/passes/50-validateNoFreezingKnownMutableFunctions.md new
+285
@@ -0,0 +1,285 @@
1 +# validateNoFreezingKnownMutableFunctions
2 +
3 +## File
4 +`src/Validation/ValidateNoFreezingKnownMutableFunctions.ts`
5 +
6 +## Purpose
7 +This validation pass ensures that functions with known mutations (functions that mutate captured local variables) are not passed where a frozen value is expected. Frozen contexts include JSX props, hook arguments, and return values from hooks.
8 +
9 +The key insight is that a function which mutates captured variables is effectively a mutable value itself. Unlike a mutable array (which a receiver can choose not to mutate), there is no way for the receiver of a function to prevent the mutation from happening when the function is called. Therefore, passing such functions to props or hooks violates React's expectation that rendered values are immutable.
10 +
11 +## Input Invariants
12 +- The function has been through aliasing effect inference
13 +- `aliasingEffects` on FunctionExpression values have been computed
14 +- `Mutate` and `MutateTransitive` effects identify definite mutations to captured variables
15 +
16 +## Validation Rules
17 +The pass produces errors when:
18 +
19 +1. **Mutable function passed as JSX prop**: A function that mutates a captured variable is passed as a prop to a JSX element
20 +2. **Mutable function passed to hook**: A function that mutates a captured variable is passed as an argument to a hook
21 +3. **Mutable function returned from hook**: A function that mutates a captured variable is returned from a hook
22 +
23 +**Exception - Ref mutations**: Functions that mutate refs (`isRefOrRefLikeMutableType`) are allowed, since refs are mutable by design and not tracked for rendering purposes.
24 +
25 +Error messages produced:
26 +- Category: `Immutability`
27 +- Reason: "Cannot modify local variables after render completes"
28 +- Description: "This argument is a function which may reassign or mutate [variable] after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead"
29 +- Messages:
30 + - "This function may (indirectly) reassign or modify [variable] after render"
31 + - "This modifies [variable]"
32 +
33 +## Algorithm
34 +
35 +### Phase 1: Track Context Mutation Effects
36 +The pass maintains a map from identifier IDs to their associated mutation effects:
37 +
38 +```typescript
39 +const contextMutationEffects: Map<
40 + IdentifierId,
41 + Extract<AliasingEffect, {kind: 'Mutate'} | {kind: 'MutateTransitive'}>
42 +> = new Map();
43 +```
44 +
45 +### Phase 2: Single Forward Pass
46 +Process all blocks in order, handling specific instruction types:
47 +
48 +```typescript
49 +for (const block of fn.body.blocks.values()) {
50 + for (const instr of block.instructions) {
51 + switch (value.kind) {
52 + case 'LoadLocal': {
53 + // Propagate mutation effect from source to loaded value
54 + const effect = contextMutationEffects.get(value.place.identifier.id);
55 + if (effect != null) {
56 + contextMutationEffects.set(lvalue.identifier.id, effect);
57 + }
58 + break;
59 + }
60 + case 'StoreLocal': {
61 + // Propagate mutation effect to both lvalue and stored variable
62 + const effect = contextMutationEffects.get(value.value.identifier.id);
63 + if (effect != null) {
64 + contextMutationEffects.set(lvalue.identifier.id, effect);
65 + contextMutationEffects.set(value.lvalue.place.identifier.id, effect);
66 + }
67 + break;
68 + }
69 + case 'FunctionExpression': {
70 + // Check function's aliasing effects for context mutations
71 + if (value.loweredFunc.func.aliasingEffects != null) {
72 + const context = new Set(
73 + value.loweredFunc.func.context.map(p => p.identifier.id)
74 + );
75 + for (const effect of value.loweredFunc.func.aliasingEffects) {
76 + if (effect.kind === 'Mutate' || effect.kind === 'MutateTransitive') {
77 + // Mark function as mutable if it mutates a context variable
78 + if (context.has(effect.value.identifier.id) &&
79 + !isRefOrRefLikeMutableType(effect.value.identifier.type)) {
80 + contextMutationEffects.set(lvalue.identifier.id, effect);
81 + }
82 + }
83 + }
84 + }
85 + break;
86 + }
87 + default: {
88 + // Check all operands for freeze effect violations
89 + for (const operand of eachInstructionValueOperand(value)) {
90 + visitOperand(operand); // Check if mutable function is being frozen
91 + }
92 + }
93 + }
94 + }
95 +}
96 +```
97 +
98 +### Phase 3: Validate Freeze Effects
99 +When an operand has a `Freeze` effect, check if it's a known mutable function:
100 +
101 +```typescript
102 +function visitOperand(operand: Place): void {
103 + if (operand.effect === Effect.Freeze) {
104 + const effect = contextMutationEffects.get(operand.identifier.id);
105 + if (effect != null) {
106 + // Emit error with both usage location and mutation location
107 + errors.pushDiagnostic(
108 + CompilerDiagnostic.create({
109 + category: ErrorCategory.Immutability,
110 + reason: 'Cannot modify local variables after render completes',
111 + description: `This argument is a function which may reassign or mutate ${variable} after render...`,
112 + })
113 + .withDetails({loc: operand.loc, message: 'This function may...'})
114 + .withDetails({loc: effect.value.loc, message: 'This modifies...'})
115 + );
116 + }
117 + }
118 +}
119 +```
120 +
121 +## Edge Cases
122 +
123 +### Function Passed as JSX Prop (Error)
124 +```javascript
125 +function Component() {
126 + const cache = new Map();
127 + const fn = () => {
128 + cache.set('key', 'value'); // Mutates captured variable
129 + };
130 + return <Foo fn={fn} />; // Error: fn is frozen but mutates cache
131 +}
132 +```
133 +
134 +### Function Passed to Hook (Error)
135 +```javascript
136 +function useFoo() {
137 + const cache = new Map();
138 + useHook(() => {
139 + cache.set('key', 'value'); // Error: function mutates cache
140 + });
141 +}
142 +```
143 +
144 +### Function Returned from Hook (Error)
145 +```javascript
146 +function useFoo() {
147 + useHook(); // For hook inference
148 + const cache = new Map();
149 + return () => {
150 + cache.set('key', 'value'); // Error: returned function mutates cache
151 + };
152 +}
153 +```
154 +
155 +### Ref Mutation (Allowed)
156 +```javascript
157 +function Component() {
158 + const ref = useRef(null);
159 + const fn = () => {
160 + ref.current = value; // OK: refs are mutable by design
161 + };
162 + return <Foo fn={fn} />; // Allowed
163 +}
164 +```
165 +
166 +### Conditional Mutations
167 +The pass only errors on definite mutations (`Mutate`, `MutateTransitive`), not conditional mutations (`MutateConditionally`, `MutateTransitiveConditionally`). However, if a function already has a known mutation effect, conditional mutations will propagate that effect:
168 +
169 +```javascript
170 +function Component(cond) {
171 + const cache = new Map();
172 + const fn = () => {
173 + cache.set('a', 1); // Definite mutation
174 + };
175 + const fn2 = fn; // fn2 inherits mutation effect
176 + return <Foo fn={fn2} />; // Error
177 +}
178 +```
179 +
180 +### Nested Function Expressions
181 +Mutation effects propagate through assignments:
182 +
183 +```javascript
184 +function Component() {
185 + const cache = new Map();
186 + const inner = () => cache.set('key', 'value');
187 + const outer = inner; // outer inherits mutation effect
188 + return <Foo fn={outer} />; // Error
189 +}
190 +```
191 +
192 +## TODOs
193 +None in the source file.
194 +
195 +## Example
196 +
197 +### Fixture: `error.invalid-pass-mutable-function-as-prop.js`
198 +
199 +**Input:**
200 +```javascript
201 +// @validateNoFreezingKnownMutableFunctions
202 +function Component() {
203 + const cache = new Map();
204 + const fn = () => {
205 + cache.set('key', 'value');
206 + };
207 + return <Foo fn={fn} />;
208 +}
209 +```
210 +
211 +**Error:**
212 +```
213 +Found 1 error:
214 +
215 +Error: Cannot modify local variables after render completes
216 +
217 +This argument is a function which may reassign or mutate `cache` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
218 +
219 +error.invalid-pass-mutable-function-as-prop.ts:7:18
220 + 5 | cache.set('key', 'value');
221 + 6 | };
222 +> 7 | return <Foo fn={fn} />;
223 + | ^^ This function may (indirectly) reassign or modify `cache` after render
224 + 8 | }
225 + 9 |
226 +
227 +error.invalid-pass-mutable-function-as-prop.ts:5:4
228 + 3 | const cache = new Map();
229 + 4 | const fn = () => {
230 +> 5 | cache.set('key', 'value');
231 + | ^^^^^ This modifies `cache`
232 + 6 | };
233 + 7 | return <Foo fn={fn} />;
234 + 8 | }
235 +```
236 +
237 +### Fixture: `error.invalid-hook-function-argument-mutates-local-variable.js`
238 +
239 +**Input:**
240 +```javascript
241 +// @validateNoFreezingKnownMutableFunctions
242 +
243 +function useFoo() {
244 + const cache = new Map();
245 + useHook(() => {
246 + cache.set('key', 'value');
247 + });
248 +}
249 +```
250 +
251 +**Error:**
252 +```
253 +Found 1 error:
254 +
255 +Error: Cannot modify local variables after render completes
256 +
257 +This argument is a function which may reassign or mutate `cache` after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.
258 +
259 +error.invalid-hook-function-argument-mutates-local-variable.ts:5:10
260 + 3 | function useFoo() {
261 + 4 | const cache = new Map();
262 +> 5 | useHook(() => {
263 + | ^^^^^^^
264 +> 6 | cache.set('key', 'value');
265 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
266 +> 7 | });
267 + | ^^^^ This function may (indirectly) reassign or modify `cache` after render
268 + 8 | }
269 + 9 |
270 +
271 +error.invalid-hook-function-argument-mutates-local-variable.ts:6:4
272 + 4 | const cache = new Map();
273 + 5 | useHook(() => {
274 +> 6 | cache.set('key', 'value');
275 + | ^^^^^ This modifies `cache`
276 + 7 | });
277 + 8 | }
278 + 9 |
279 +```
280 +
281 +Key observations:
282 +- The pass detects functions that mutate captured local variables (not refs)
283 +- Errors show both where the function is used (frozen) and where the mutation occurs
284 +- The validation prevents inconsistent re-render behavior by catching mutations that happen after render
285 +- The suggestion to "use state instead" guides users toward the correct React pattern
compiler/packages/babel-plugin-react-compiler/docs/passes/51-validateExhaustiveDependencies.md new
+376
@@ -0,0 +1,376 @@
1 +# validateExhaustiveDependencies
2 +
3 +## File
4 +`src/Validation/ValidateExhaustiveDependencies.ts`
5 +
6 +## Purpose
7 +This validation pass ensures that manual memoization (useMemo, useCallback) and effect hooks (useEffect, useLayoutEffect) have correct dependency arrays. The pass compares developer-specified dependencies against the actual values referenced within the memoized function or effect callback to detect:
8 +
9 +1. **Missing dependencies**: Values used in the function that are not listed in the dependency array, causing the memoized value or effect to update less frequently than expected
10 +2. **Extra dependencies**: Values listed in the dependency array that are not actually used, causing unnecessary re-computation or effect re-runs
11 +3. **Overly precise dependencies**: Dependencies that access deeper property paths than what is actually used (e.g., `x.y.z` when only `x.y` is accessed)
12 +
13 +The goal is to ensure that auto-memoization by the compiler will not substantially change program behavior.
14 +
15 +## Input Invariants
16 +- The function has been through `StartMemoize` and `FinishMemoize` instruction insertion
17 +- Manual dependency arrays have been parsed and associated with memoization blocks
18 +- Reactive identifiers have been computed
19 +- Optional chaining paths have been analyzed
20 +
21 +## Validation Rules
22 +The pass produces errors for:
23 +
24 +1. **Missing dependency in useMemo/useCallback**: A reactive value is used but not listed in deps
25 +2. **Extra dependency in useMemo/useCallback**: A value is listed but not used
26 +3. **Missing dependency in useEffect**: A value used in the effect callback is not in the deps array
27 +4. **Extra dependency in useEffect**: A value in deps is not used in the callback
28 +5. **Overly precise dependency**: The manual dep accesses a deeper path than what's actually used
29 +6. **Global as dependency**: Module-level values should not be listed as dependencies
30 +7. **useEffectEvent in dependency array**: Functions from useEffectEvent must not be in deps
31 +
32 +**Exception - Optional dependencies**: Non-reactive values of stable types (refs, setState) or primitive types are optional and don't need to be listed.
33 +
34 +Error messages produced:
35 +- Categories: `MemoDependencies` or `EffectExhaustiveDependencies`
36 +- Reasons:
37 + - "Found missing memoization dependencies"
38 + - "Found extra memoization dependencies"
39 + - "Found missing/extra memoization dependencies"
40 + - "Found missing effect dependencies"
41 + - "Found extra effect dependencies"
42 + - "Found missing/extra effect dependencies"
43 +- Messages:
44 + - "Missing dependency `{dep}`"
45 + - "Unnecessary dependency `{dep}`"
46 + - "Overly precise dependency `{manual}`, use `{inferred}` instead"
47 + - "Functions returned from `useEffectEvent` must not be included in the dependency array"
48 + - "Values declared outside of a component/hook should not be listed as dependencies"
49 +
50 +## Algorithm
51 +
52 +### Phase 1: Collect Reactive Identifiers
53 +Scan all instructions to identify which identifiers are reactive:
54 +
55 +```typescript
56 +function collectReactiveIdentifiersHIR(fn: HIRFunction): Set<IdentifierId> {
57 + const reactive = new Set<IdentifierId>();
58 + for (const block of fn.body.blocks.values()) {
59 + for (const instr of block.instructions) {
60 + for (const lvalue of eachInstructionLValue(instr)) {
61 + if (lvalue.reactive) {
62 + reactive.add(lvalue.identifier.id);
63 + }
64 + }
65 + // ... also check operands
66 + }
67 + }
68 + return reactive;
69 +}
70 +```
71 +
72 +### Phase 2: Find Optional Places
73 +Identify places that are within optional chaining expressions:
74 +
75 +```typescript
76 +function findOptionalPlaces(fn: HIRFunction): Map<IdentifierId, boolean> {
77 + // Walks through optional terminals to track which identifiers
78 + // are accessed via optional chaining (?.property)
79 +}
80 +```
81 +
82 +### Phase 3: Collect Dependencies
83 +The core algorithm processes each block, tracking:
84 +- `temporaries`: Map of identifier IDs to their dependency information
85 +- `locals`: Set of identifiers declared within the current scope
86 +- `dependencies`: Set of inferred dependencies
87 +
88 +```typescript
89 +function collectDependencies(
90 + fn: HIRFunction,
91 + temporaries: Map<IdentifierId, Temporary>,
92 + callbacks: {
93 + onStartMemoize: (...) => void;
94 + onFinishMemoize: (...) => void;
95 + onEffect: (...) => void;
96 + },
97 + isFunctionExpression: boolean,
98 +): Temporary {
99 + for (const block of fn.body.blocks.values()) {
100 + // Process phi nodes - merge dependencies from control flow
101 + for (const phi of block.phis) {
102 + // Aggregate dependencies from all operands
103 + }
104 +
105 + for (const instr of block.instructions) {
106 + switch (value.kind) {
107 + case 'LoadLocal':
108 + case 'LoadContext':
109 + // Track dependency path through the temporary
110 + break;
111 + case 'PropertyLoad':
112 + // Extend dependency path: x -> x.y
113 + break;
114 + case 'FunctionExpression':
115 + // Recursively collect dependencies from nested function
116 + break;
117 + case 'StartMemoize':
118 + // Begin tracking dependencies for this memo block
119 + break;
120 + case 'FinishMemoize':
121 + // Validate collected dependencies against manual deps
122 + break;
123 + case 'CallExpression':
124 + case 'MethodCall':
125 + // Check for effect hooks and validate their deps
126 + break;
127 + }
128 + }
129 + }
130 +}
131 +```
132 +
133 +### Phase 4: Validate Dependencies
134 +Compare inferred dependencies against manual dependencies:
135 +
136 +```typescript
137 +function validateDependencies(
138 + inferred: Array<InferredDependency>,
139 + manualDependencies: Array<ManualMemoDependency>,
140 + reactive: Set<IdentifierId>,
141 + ...
142 +): CompilerDiagnostic | null {
143 + // Sort and deduplicate inferred dependencies
144 + // For each inferred dep, check if there's a matching manual dep
145 + // For each manual dep, check if it corresponds to an inferred dep
146 + // Report missing and extra dependencies
147 +}
148 +```
149 +
150 +### Dependency Matching Rules
151 +- If `x.y.z` is inferred, `x`, `x.y`, or `x.y.z` are valid manual deps
152 +- Optional chaining is handled: `x?.y` inferred can match `x.y` manual (ignoring optionals)
153 +- Stable types (refs, setState) that are non-reactive are optional
154 +- Global values should not be in dependency arrays
155 +- useEffectEvent return values should not be in dependency arrays
156 +
157 +## Edge Cases
158 +
159 +### Overly Precise Dependency (Error)
160 +```javascript
161 +const a = useMemo(() => {
162 + return x?.y.z?.a;
163 +}, [x?.y.z?.a.b]); // Error: should be [x?.y.z?.a]
164 +```
165 +
166 +### Unnecessary Dependencies (Error)
167 +```javascript
168 +const f = useMemo(() => {
169 + return [];
170 +}, [x, y.z, GLOBAL]); // Error: all deps are unnecessary
171 +```
172 +
173 +### Reactive Stable Type (Error)
174 +```javascript
175 +const ref1 = useRef(null);
176 +const ref2 = useRef(null);
177 +const ref = z ? ref1 : ref2; // ref is reactive (depends on z)
178 +const cb = useMemo(() => {
179 + return () => ref.current;
180 +}, []); // Error: missing dep 'ref' (reactive even though stable type)
181 +```
182 +
183 +### useEffectEvent in Dependencies (Error)
184 +```javascript
185 +const effectEvent = useEffectEvent(() => log(x));
186 +useEffect(() => {
187 + effectEvent();
188 +}, [effectEvent]); // Error: useEffectEvent returns should not be in deps
189 +```
190 +
191 +### Effect with Missing and Extra Dependencies (Error)
192 +```javascript
193 +useEffect(() => {
194 + log(x, z);
195 +}, [x, y]); // Error: missing z, extra y
196 +```
197 +
198 +### Valid Dependency Specifications
199 +```javascript
200 +// All valid - deps cover or exceed what's used
201 +const b = useMemo(() => x.y.z?.a, [x.y.z.a]); // OK
202 +const d = useMemo(() => x?.y?.[(console.log(y), z?.b)], [x?.y, y, z?.b]); // OK
203 +const e = useMemo(() => { e.push(x); return e; }, [x]); // OK
204 +```
205 +
206 +## Configuration
207 +The validation can be configured via compiler options:
208 +
209 +```typescript
210 +// For useMemo/useCallback
211 +validateExhaustiveMemoizationDependencies: boolean
212 +
213 +// For useEffect and similar
214 +validateExhaustiveEffectDependencies: 'off' | 'all' | 'missing-only' | 'extra-only'
215 +```
216 +
217 +The `missing-only` and `extra-only` modes allow validating only one category of errors.
218 +
219 +## TODOs
220 +From the source file:
221 +
222 +```typescript
223 +/**
224 + * TODO: Invalid, Complex Deps
225 + *
226 + * Handle cases where the user deps were not simple identifiers + property chains.
227 + * We try to detect this in ValidateUseMemo but we miss some cases. The problem
228 + * is that invalid forms can be value blocks or function calls that don't get
229 + * removed by DCE, leaving a structure like:
230 + *
231 + * StartMemoize
232 + * t0 = <value to memoize>
233 + * ...non-DCE'd code for manual deps...
234 + * FinishMemoize decl=t0
235 + */
236 +```
237 +
238 +## Example
239 +
240 +### Fixture: `error.invalid-exhaustive-deps.js`
241 +
242 +**Input:**
243 +```javascript
244 +// @validateExhaustiveMemoizationDependencies @validateRefAccessDuringRender:false
245 +import {useMemo} from 'react';
246 +
247 +function Component({x, y, z}) {
248 + const a = useMemo(() => {
249 + return x?.y.z?.a;
250 + // error: too precise
251 + }, [x?.y.z?.a.b]);
252 + const f = useMemo(() => {
253 + return [];
254 + // error: unnecessary
255 + }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
256 + const ref1 = useRef(null);
257 + const ref2 = useRef(null);
258 + const ref = z ? ref1 : ref2;
259 + const cb = useMemo(() => {
260 + return () => ref.current;
261 + // error: ref is a stable type but reactive
262 + }, []);
263 + return <Stringify results={[a, f, cb]} />;
264 +}
265 +```
266 +
267 +**Error:**
268 +```
269 +Found 4 errors:
270 +
271 +Error: Found missing/extra memoization dependencies
272 +
273 +Missing dependencies can cause a value to update less often than it should, resulting in stale UI. Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
274 +
275 +error.invalid-exhaustive-deps.ts:7:11
276 + 5 | function Component({x, y, z}) {
277 + 6 | const a = useMemo(() => {
278 +> 7 | return x?.y.z?.a;
279 + | ^^^^^^^^^ Missing dependency `x?.y.z?.a`
280 + 8 | // error: too precise
281 + 9 | }, [x?.y.z?.a.b]);
282 +
283 +error.invalid-exhaustive-deps.ts:9:6
284 +> 9 | }, [x?.y.z?.a.b]);
285 + | ^^^^^^^^^^^ Overly precise dependency `x?.y.z?.a.b`, use `x?.y.z?.a` instead
286 +
287 +Inferred dependencies: `[x?.y.z?.a]`
288 +
289 +Error: Found extra memoization dependencies
290 +...
291 +error.invalid-exhaustive-deps.ts:31:6
292 +> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
293 + | ^ Unnecessary dependency `x`
294 +...
295 + | ^^^^^^^^^^^^^ Unnecessary dependency `UNUSED_GLOBAL`. Values declared outside of a component/hook should not be listed as dependencies as the component will not re-render if they change
296 +
297 +Inferred dependencies: `[]`
298 +
299 +Error: Found missing memoization dependencies
300 +...
301 +error.invalid-exhaustive-deps.ts:37:13
302 +> 37 | return ref.current;
303 + | ^^^ Missing dependency `ref`. Refs, setState functions, and other "stable" values generally do not need to be added as dependencies, but this variable may change over time to point to different values
304 +
305 +Inferred dependencies: `[ref]`
306 +```
307 +
308 +### Fixture: `error.invalid-exhaustive-effect-deps.js`
309 +
310 +**Input:**
311 +```javascript
312 +// @validateExhaustiveEffectDependencies:"all"
313 +import {useEffect} from 'react';
314 +
315 +function Component({x, y, z}) {
316 + // error: missing dep - x
317 + useEffect(() => {
318 + log(x);
319 + }, []);
320 +
321 + // error: extra dep - y
322 + useEffect(() => {
323 + log(x);
324 + }, [x, y]);
325 +
326 + // error: missing dep - z; extra dep - y
327 + useEffect(() => {
328 + log(x, z);
329 + }, [x, y]);
330 +}
331 +```
332 +
333 +**Error:**
334 +```
335 +Found 4 errors:
336 +
337 +Error: Found missing effect dependencies
338 +
339 +Missing dependencies can cause an effect to fire less often than it should.
340 +
341 +error.invalid-exhaustive-effect-deps.ts:7:8
342 +> 7 | log(x);
343 + | ^ Missing dependency `x`
344 +
345 +Inferred dependencies: `[x]`
346 +
347 +Error: Found extra effect dependencies
348 +
349 +Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects.
350 +
351 +error.invalid-exhaustive-effect-deps.ts:13:9
352 +> 13 | }, [x, y]);
353 + | ^ Unnecessary dependency `y`
354 +
355 +Inferred dependencies: `[x]`
356 +
357 +Error: Found missing/extra effect dependencies
358 +...
359 +error.invalid-exhaustive-effect-deps.ts:17:11
360 +> 17 | log(x, z);
361 + | ^ Missing dependency `z`
362 +
363 +error.invalid-exhaustive-effect-deps.ts:18:9
364 +> 18 | }, [x, y]);
365 + | ^ Unnecessary dependency `y`
366 +
367 +Inferred dependencies: `[x, z]`
368 +```
369 +
370 +Key observations:
371 +- The pass validates both useMemo/useCallback and useEffect dependency arrays
372 +- Dependencies are inferred by analyzing actual value usage within the function
373 +- Optional chaining paths are tracked and included in dependency paths
374 +- Reactive stable types (like conditionally assigned refs) must still be listed
375 +- Globals and useEffectEvent returns should not be in dependency arrays
376 +- The validation provides fix suggestions showing the inferred correct dependencies
compiler/packages/babel-plugin-react-compiler/docs/passes/52-validateMemoizedEffectDependencies.md new
+93
@@ -0,0 +1,93 @@
1 +# validateMemoizedEffectDependencies
2 +
3 +## File
4 +`src/Validation/ValidateMemoizedEffectDependencies.ts`
5 +
6 +## Purpose
7 +Validates that all known effect dependencies (for `useEffect`, `useLayoutEffect`, and `useInsertionEffect`) are properly memoized. This prevents a common bug where unmemoized effect dependencies can cause infinite re-render loops or other unexpected behavior.
8 +
9 +## Input Invariants
10 +- Operates on ReactiveFunction (post-reactive scope inference)
11 +- Reactive scopes have been assigned to values that need memoization
12 +- Must run after scope inference but before codegen
13 +
14 +## Validation Rules
15 +This pass checks two conditions:
16 +
17 +1. **Unmemoized dependencies with assigned scopes**: Disallows effect dependencies that should be memoized (have a reactive scope assigned) but where that reactive scope does not exist in the output. This catches cases where a reactive scope was pruned, such as when it spans a hook call.
18 +
19 +2. **Mutable dependencies at effect call site**: Disallows effect dependencies whose mutable range encompasses the effect call. This catches values that the compiler knows may be mutated after the effect is set up.
20 +
21 +When either condition is violated, the pass produces:
22 +```
23 +Compilation Skipped: React Compiler has skipped optimizing this component because
24 +the effect dependencies could not be memoized. Unmemoized effect dependencies can
25 +trigger an infinite loop or other unexpected behavior
26 +```
27 +
28 +## Algorithm
29 +1. Traverse the reactive function using a visitor pattern
30 +2. Track all scopes that exist in the AST by adding them to a `Set<ScopeId>` during `visitScope`
31 +3. Only record a scope if its dependencies are also memoized (transitive memoization check)
32 +4. When visiting an instruction that is an effect hook call (`useEffect`, `useLayoutEffect`, `useInsertionEffect`) with at least 2 arguments (function + deps array):
33 + - Check if the dependency array is mutable at the call site using `isMutable()`
34 + - Check if the dependency array's scope exists using `isUnmemoized()`
35 + - If either check fails, push an error
36 +
37 +### Key Helper Functions
38 +
39 +**isEffectHook(identifier)**: Returns true if the identifier is `useEffect`, `useLayoutEffect`, or `useInsertionEffect`.
40 +
41 +**isUnmemoized(operand, scopes)**: Returns true if the operand has a scope assigned (`operand.scope != null`) but that scope doesn't exist in the set of valid scopes.
42 +
43 +## Edge Cases
44 +- Only validates effects with 2+ arguments (ignores effects without dependency arrays)
45 +- Transitive memoization: A scope is only considered valid if all its dependencies are also memoized
46 +- Merged scopes are tracked together with their primary scope
47 +
48 +## TODOs
49 +From the source code:
50 +```typescript
51 +// TODO: isMutable is not safe to call here as it relies on identifier mutableRange
52 +// which is no longer valid at this point in the pipeline
53 +```
54 +
55 +## Example
56 +
57 +### Fixture: `error.invalid-useEffect-dep-not-memoized.js`
58 +
59 +**Input:**
60 +```javascript
61 +// @validateMemoizedEffectDependencies
62 +import {useEffect} from 'react';
63 +
64 +function Component(props) {
65 + const data = {};
66 + useEffect(() => {
67 + console.log(props.value);
68 + }, [data]);
69 + mutate(data);
70 + return data;
71 +}
72 +```
73 +
74 +**Error:**
75 +```
76 +Found 1 error:
77 +
78 +Compilation Skipped: React Compiler has skipped optimizing this component because
79 +the effect dependencies could not be memoized. Unmemoized effect dependencies can
80 +trigger an infinite loop or other unexpected behavior
81 +
82 +error.invalid-useEffect-dep-not-memoized.ts:6:2
83 + 4 | function Component(props) {
84 + 5 | const data = {};
85 +> 6 | useEffect(() => {
86 + | ^^^^^^^^^^^^^^^^^
87 +> 7 | console.log(props.value);
88 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
89 +> 8 | }, [data]);
90 + | ^^^^^^^^^^^^^
91 +```
92 +
93 +**Why it fails:** The `data` object is mutated after the `useEffect` call, which extends its mutable range past the effect. This means `data` cannot be safely memoized as an effect dependency because it might change after the effect is set up.
compiler/packages/babel-plugin-react-compiler/docs/passes/53-validatePreservedManualMemoization.md new
+152
@@ -0,0 +1,152 @@
1 +# validatePreservedManualMemoization
2 +
3 +## File
4 +`src/Validation/ValidatePreservedManualMemoization.ts`
5 +
6 +## Purpose
7 +Validates that all explicit manual memoization (`useMemo`/`useCallback`) from the original source code is accurately preserved in the compiled output. This ensures that values the developer intended to be memoized remain memoized after compilation.
8 +
9 +## Input Invariants
10 +- Operates on ReactiveFunction (post-reactive scope inference)
11 +- Manual memoization markers (`StartMemoize`/`FinishMemoize`) are present from earlier passes
12 +- Scopes have been assigned and merged as appropriate
13 +
14 +## Validation Rules
15 +This pass validates three conditions:
16 +
17 +### 1. Dependencies not mutated later
18 +Validates that dependencies of manual memoization are not mutated after the memoization call:
19 +```
20 +Existing memoization could not be preserved. This dependency may be modified later
21 +```
22 +
23 +### 2. Inferred dependencies match source
24 +Validates that the compiler's inferred dependencies match the manually specified dependencies:
25 +```
26 +Existing memoization could not be preserved. The inferred dependencies did not match
27 +the manually specified dependencies, which could cause the value to change more or
28 +less frequently than expected. The inferred dependency was `X`, but the source
29 +dependencies were [Y, Z].
30 +```
31 +
32 +### 3. Output value is memoized
33 +Validates that the memoized value actually ends up in a reactive scope:
34 +```
35 +Existing memoization could not be preserved. This value was memoized in source but
36 +not in compilation output
37 +```
38 +
39 +## Algorithm
40 +
41 +### State Management
42 +The visitor tracks:
43 +- `scopes: Set<ScopeId>` - All completed reactive scopes
44 +- `prunedScopes: Set<ScopeId>` - Scopes that were pruned
45 +- `temporaries: Map<IdentifierId, ManualMemoDependency>` - Temporary variable mappings
46 +- `manualMemoState: ManualMemoBlockState | null` - Current manual memoization context
47 +
48 +### ManualMemoBlockState
49 +```typescript
50 +type ManualMemoBlockState = {
51 + reassignments: Map<DeclarationId, Set<Identifier>>; // Track inlined useMemo reassignments
52 + loc: SourceLocation; // Source location for errors
53 + decls: Set<DeclarationId>; // Declarations within the memo block
54 + depsFromSource: Array<ManualMemoDependency> | null; // Original deps from source
55 + manualMemoId: number; // Unique ID for this memoization
56 +};
57 +```
58 +
59 +### Processing Flow
60 +
61 +1. **On `StartMemoize` instruction:**
62 + - Validate that dependencies' scopes have completed (not mutated later)
63 + - Initialize `manualMemoState` with source dependencies
64 + - Push error if any dependency's scope hasn't completed yet
65 +
66 +2. **During memo block (between Start/Finish):**
67 + - Track all declarations made within the block
68 + - Track reassignments for inlined useMemo handling
69 + - Record property loads and temporaries
70 +
71 +3. **On scope completion:**
72 + - Validate each scope dependency against source dependencies using `compareDeps()`
73 + - An inferred dependency matches if:
74 + - Root identifiers are the same (same named variable)
75 + - Paths are identical, OR
76 + - Inferred path is more specific (not involving `.current` refs)
77 +
78 +4. **On `FinishMemoize` instruction:**
79 + - Validate that the memoized value is in a completed scope
80 + - Handle inlined useMemo with reassignment tracking
81 + - Push error if value is unmemoized
82 +
83 +### Dependency Comparison Results
84 +```typescript
85 +enum CompareDependencyResult {
86 + Ok = 0, // Dependencies match
87 + RootDifference = 1, // Different root variables
88 + PathDifference = 2, // Different property paths
89 + Subpath = 3, // Inferred is less specific
90 + RefAccessDifference = 4, // ref.current access differs
91 +}
92 +```
93 +
94 +## Edge Cases
95 +
96 +### Inlined useMemo Handling
97 +When useMemo is inlined, it produces `let` declarations followed by reassignments. The pass tracks these reassignments to ensure all code paths produce memoized values.
98 +
99 +### Ref Access
100 +Special handling for `.current` property access on refs. Since `ref_prev === ref_new` does not imply `ref_prev.current === ref_new.current`, the pass is strict about ref access differences.
101 +
102 +### More Specific Dependencies
103 +If the compiler infers a more specific dependency (e.g., `obj.prop.value` instead of `obj`), this is acceptable as long as it doesn't involve ref access.
104 +
105 +## TODOs
106 +None found in the source.
107 +
108 +## Example
109 +
110 +### Fixture: `error.preserve-use-memo-ref-missing-reactive.ts`
111 +
112 +**Input:**
113 +```javascript
114 +// @validatePreserveExistingMemoizationGuarantees
115 +import {useCallback, useRef} from 'react';
116 +
117 +function useFoo({cond}) {
118 + const ref1 = useRef<undefined | (() => undefined)>();
119 + const ref2 = useRef<undefined | (() => undefined)>();
120 + const ref = cond ? ref1 : ref2;
121 +
122 + return useCallback(() => {
123 + if (ref != null) {
124 + ref.current();
125 + }
126 + }, []);
127 +}
128 +```
129 +
130 +**Error:**
131 +```
132 +Found 1 error:
133 +
134 +Compilation Skipped: Existing memoization could not be preserved
135 +
136 +React Compiler has skipped optimizing this component because the existing manual
137 +memoization could not be preserved. The inferred dependencies did not match the
138 +manually specified dependencies, which could cause the value to change more or
139 +less frequently than expected. The inferred dependency was `ref`, but the source
140 +dependencies were []. Inferred dependency not present in source.
141 +
142 +error.preserve-use-memo-ref-missing-reactive.ts:9:21
143 +> 9 | return useCallback(() => {
144 + | ^^^^^^^
145 +> 10 | if (ref != null) {
146 +> 11 | ref.current();
147 +> 12 | }
148 +> 13 | }, []);
149 + | ^^^^ Could not preserve existing manual memoization
150 +```
151 +
152 +**Why it fails:** The callback uses `ref` which is conditionally assigned based on `cond`. The compiler infers `ref` as a dependency, but the source specifies an empty dependency array `[]`. This mismatch means the memoization cannot be preserved as-is.
compiler/packages/babel-plugin-react-compiler/docs/passes/54-validateStaticComponents.md new
+153
@@ -0,0 +1,153 @@
1 +# validateStaticComponents
2 +
3 +## File
4 +`src/Validation/ValidateStaticComponents.ts`
5 +
6 +## Purpose
7 +Validates that components used in JSX are not created dynamically during render. Components created during render will have their state reset on every re-render because React sees them as new component types each time. This is a common React anti-pattern that causes bugs and poor performance.
8 +
9 +## Input Invariants
10 +- Operates on HIRFunction (pre-reactive transformation)
11 +- All instructions and phi nodes are present
12 +- JSX expressions have been lowered to `JsxExpression` instruction values
13 +
14 +## Validation Rules
15 +When a JSX element uses a component that was dynamically created during render, the pass produces:
16 +```
17 +Cannot create components during render. Components created during render will reset
18 +their state each time they are created. Declare components outside of render
19 +```
20 +
21 +The error includes two locations:
22 +1. Where the component is used in JSX
23 +2. Where the component was originally created
24 +
25 +### What constitutes "dynamically created"?
26 +The following instruction kinds mark a value as dynamically created:
27 +- `FunctionExpression` - An inline function definition
28 +- `NewExpression` - A `new` constructor call
29 +- `MethodCall` - A method call that returns a value
30 +- `CallExpression` - A function call that returns a value
31 +
32 +## Algorithm
33 +
34 +1. Create a `Map<IdentifierId, SourceLocation>` called `knownDynamicComponents` to track identifiers whose values are dynamically created
35 +
36 +2. Iterate through all blocks in evaluation order
37 +
38 +3. For each block, first process phi nodes:
39 + - If any phi operand is in `knownDynamicComponents`, add the phi result to the map
40 + - This propagates dynamic-ness through control flow joins
41 +
42 +4. For each instruction in the block:
43 + - **FunctionExpression, NewExpression, MethodCall, CallExpression**: Add the lvalue to `knownDynamicComponents` with its source location
44 + - **LoadLocal**: If the loaded value is dynamic, mark the lvalue as dynamic
45 + - **StoreLocal**: If the stored value is dynamic, mark both the lvalue and the store target as dynamic
46 + - **JsxExpression**: If the JSX tag is an identifier that is in `knownDynamicComponents`, push a diagnostic error
47 +
48 +5. Return the collected errors
49 +
50 +### Data Flow Tracking
51 +The pass tracks how dynamic values flow through the program:
52 +- Through variable assignments (`StoreLocal`, `LoadLocal`)
53 +- Through phi nodes (conditional assignments)
54 +- Into JSX component positions
55 +
56 +## Edge Cases
57 +
58 +### Conditionally Assigned Components
59 +```javascript
60 +function Example({cond}) {
61 + let Component;
62 + if (cond) {
63 + Component = createComponent(); // Dynamic!
64 + } else {
65 + Component = OtherComponent; // Static
66 + }
67 + return <Component />; // Error: Component may be dynamic
68 +}
69 +```
70 +The phi node joins the conditional paths, and since one path is dynamic, the result is considered dynamic.
71 +
72 +### Component Returned from Hooks/Functions
73 +```javascript
74 +function Example() {
75 + const Component = useCreateComponent(); // CallExpression - dynamic
76 + return <Component />; // Error
77 +}
78 +```
79 +
80 +### Factory Functions
81 +```javascript
82 +function Example() {
83 + const Component = createComponent(); // CallExpression - dynamic
84 + return <Component />; // Error
85 +}
86 +```
87 +
88 +### Safe Patterns (No Error)
89 +```javascript
90 +// Component defined outside render
91 +const MyComponent = () => <div />;
92 +
93 +function Example() {
94 + return <MyComponent />; // OK - not created during render
95 +}
96 +```
97 +
98 +## TODOs
99 +None found in the source.
100 +
101 +## Example
102 +
103 +### Fixture: `static-components/invalid-dynamically-construct-component-in-render.js`
104 +
105 +**Input:**
106 +```javascript
107 +// @validateStaticComponents
108 +function Example(props) {
109 + const Component = createComponent();
110 + return <Component />;
111 +}
112 +```
113 +
114 +**Error (from logs):**
115 +```json
116 +{
117 + "kind": "CompileError",
118 + "detail": {
119 + "options": {
120 + "category": "StaticComponents",
121 + "reason": "Cannot create components during render",
122 + "description": "Components created during render will reset their state each time they are created. Declare components outside of render",
123 + "details": [
124 + {
125 + "kind": "error",
126 + "loc": { "start": { "line": 4, "column": 10 } },
127 + "message": "This component is created during render"
128 + },
129 + {
130 + "kind": "error",
131 + "loc": { "start": { "line": 3, "column": 20 } },
132 + "message": "The component is created during render here"
133 + }
134 + ]
135 + }
136 + }
137 +}
138 +```
139 +
140 +**Why it fails:** The `createComponent()` call creates a new component type on every render. When this component is used in JSX, React will see a different component type each time, causing the component to unmount and remount (losing all state) on every render.
141 +
142 +### Fixture: `static-components/invalid-dynamically-constructed-component-function.js`
143 +
144 +**Input:**
145 +```javascript
146 +// @validateStaticComponents
147 +function Example(props) {
148 + const Component = () => <div />;
149 + return <Component />;
150 +}
151 +```
152 +
153 +**Why it fails:** Even though this looks like a simple component definition, it creates a new function (and thus a new component type) on every render. The fix is to move the component definition outside of `Example`.
compiler/packages/babel-plugin-react-compiler/docs/passes/55-validateSourceLocations.md new
+199
@@ -0,0 +1,199 @@
1 +# validateSourceLocations
2 +
3 +## File
4 +`src/Validation/ValidateSourceLocations.ts`
5 +
6 +## Purpose
7 +**IMPORTANT: This validation is intended for unit tests only, not production use.**
8 +
9 +Validates that important source locations from the original code are preserved in the generated AST. This ensures that code coverage instrumentation tools (like Istanbul) can properly map back to the original source code for accurate coverage reports.
10 +
11 +## Input Invariants
12 +- Operates on the original Babel AST (`NodePath<FunctionDeclaration | ArrowFunctionExpression | FunctionExpression>`)
13 +- Operates on the generated CodegenFunction output
14 +- Must run after code generation
15 +
16 +## Validation Rules
17 +The pass checks that "important" source locations (as defined by Istanbul's instrumentation requirements) are preserved in the generated output.
18 +
19 +### Two types of errors:
20 +
21 +1. **Missing location:**
22 +```
23 +Important source location missing in generated code. Source location for [NodeType]
24 +is missing in the generated output. This can cause coverage instrumentation to fail
25 +to track this code properly, resulting in inaccurate coverage reports.
26 +```
27 +
28 +2. **Wrong node type:**
29 +```
30 +Important source location has wrong node type in generated code. Source location for
31 +[ExpectedType] exists in the generated output but with wrong node type(s): [ActualTypes].
32 +This can cause coverage instrumentation to fail to track this code properly.
33 +```
34 +
35 +### Important Node Types
36 +The following node types are considered important for coverage tracking:
37 +```typescript
38 +const IMPORTANT_INSTRUMENTED_TYPES = new Set([
39 + 'ArrowFunctionExpression',
40 + 'AssignmentPattern',
41 + 'ObjectMethod',
42 + 'ExpressionStatement',
43 + 'BreakStatement',
44 + 'ContinueStatement',
45 + 'ReturnStatement',
46 + 'ThrowStatement',
47 + 'TryStatement',
48 + 'VariableDeclarator',
49 + 'IfStatement',
50 + 'ForStatement',
51 + 'ForInStatement',
52 + 'ForOfStatement',
53 + 'WhileStatement',
54 + 'DoWhileStatement',
55 + 'SwitchStatement',
56 + 'SwitchCase',
57 + 'WithStatement',
58 + 'FunctionDeclaration',
59 + 'FunctionExpression',
60 + 'LabeledStatement',
61 + 'ConditionalExpression',
62 + 'LogicalExpression',
63 + 'VariableDeclaration',
64 + 'Identifier',
65 +]);
66 +```
67 +
68 +### Strict Node Types
69 +For these types, both the location AND node type must match:
70 +- `VariableDeclaration`
71 +- `VariableDeclarator`
72 +- `Identifier`
73 +
74 +## Algorithm
75 +
76 +### Step 1: Collect Important Original Locations
77 +Traverse the original AST and collect locations from nodes whose types are in `IMPORTANT_INSTRUMENTED_TYPES`:
78 +- Skip nodes that are manual memoization calls (`useMemo`/`useCallback`) since the compiler intentionally removes these
79 +- Build a map from location key to `{loc, nodeTypes}`
80 +
81 +### Step 2: Collect Generated Locations
82 +Recursively traverse the generated AST (main function body + outlined functions) and collect all locations with their node types.
83 +
84 +### Step 3: Validate Preservation
85 +For each important original location:
86 +- If the location is completely missing in generated output, report an error
87 +- For strict node types, verify the specific node type is present
88 +- Handle cases where a generated location has a different node type
89 +
90 +### Location Key Format
91 +Locations are compared using a string key:
92 +```typescript
93 +function locationKey(loc: SourceLocation): string {
94 + return `${loc.start.line}:${loc.start.column}-${loc.end.line}:${loc.end.column}`;
95 +}
96 +```
97 +
98 +## Edge Cases
99 +
100 +### Manual Memoization Removal
101 +The compiler intentionally removes `useMemo` and `useCallback` calls (replacing them with compiler-generated memoization). These are detected and exempted from validation:
102 +```typescript
103 +function isManualMemoization(node: Node): boolean {
104 + // Checks for useMemo/useCallback or React.useMemo/React.useCallback
105 +}
106 +```
107 +
108 +### Outlined Functions
109 +The validation also checks locations in outlined functions (functions extracted by the compiler for optimization purposes).
110 +
111 +### Multiple Node Types at Same Location
112 +Multiple node types can share the same location (e.g., a `VariableDeclarator` and its `Identifier` child). The pass tracks all node types for each location.
113 +
114 +## TODOs
115 +From the file documentation:
116 +> There's one big gotcha with this validation: it only works if the "important" original nodes are not optimized away by the compiler.
117 +>
118 +> When that scenario happens, we should just update the fixture to not include a node that has no corresponding node in the generated AST due to being completely removed during compilation.
119 +
120 +## Example
121 +
122 +### Fixture: `error.todo-missing-source-locations.js`
123 +
124 +**Input:**
125 +```javascript
126 +// @validateSourceLocations
127 +import {useEffect, useCallback} from 'react';
128 +
129 +function Component({prop1, prop2}) {
130 + const x = prop1 + prop2;
131 + const y = x * 2;
132 + const arr = [x, y];
133 + const obj = {x, y};
134 + let destA, destB;
135 + if (y > 5) {
136 + [destA, destB] = arr;
137 + }
138 +
139 + const [a, b] = arr;
140 + const {x: c, y: d} = obj;
141 + let sound;
142 +
143 + if (y > 10) {
144 + sound = 'woof';
145 + } else {
146 + sound = 'meow';
147 + }
148 +
149 + useEffect(() => {
150 + if (a > 10) {
151 + console.log(a);
152 + console.log(sound);
153 + console.log(destA, destB);
154 + }
155 + }, [a, sound, destA, destB]);
156 +
157 + const foo = useCallback(() => {
158 + return a + b;
159 + }, [a, b]);
160 +
161 + function bar() {
162 + return (c + d) * 2;
163 + }
164 +
165 + console.log('Hello, world!');
166 +
167 + return [y, foo, bar];
168 +}
169 +```
170 +
171 +**Error (partial):**
172 +```
173 +Found 25 errors:
174 +
175 +Todo: Important source location missing in generated code
176 +Source location for Identifier is missing in the generated output...
177 +
178 +error.todo-missing-source-locations.ts:4:9
179 +> 4 | function Component({prop1, prop2}) {
180 + | ^^^^^^^^^
181 +
182 +Todo: Important source location missing in generated code
183 +Source location for VariableDeclaration is missing in the generated output...
184 +
185 +error.todo-missing-source-locations.ts:9:2
186 +> 9 | let destA, destB;
187 + | ^^^^^^^^^^^^^^^^^
188 +
189 +Todo: Important source location missing in generated code
190 +Source location for ExpressionStatement is missing in the generated output...
191 +
192 +error.todo-missing-source-locations.ts:11:4
193 +> 11 | [destA, destB] = arr;
194 + | ^^^^^^^^^^^^^^^^^^^^^
195 +```
196 +
197 +**Why it fails:** The compiler transforms the code significantly, and many original source locations are not preserved in the output. This causes coverage tools to lose track of which lines were executed.
198 +
199 +**Note:** This fixture is prefixed with `error.todo-` indicating this is a known limitation that needs to be addressed.
compiler/packages/babel-plugin-react-compiler/docs/passes/README.md new
+305
@@ -0,0 +1,305 @@
1 +# React Compiler Passes Documentation
2 +
3 +This directory contains detailed documentation for each pass in the React Compiler pipeline. The compiler transforms React components and hooks to add automatic memoization.
4 +
5 +## High-Level Architecture
6 +
7 +```
8 + ┌─────────────────────────────────────────────────────────────┐
9 + │ COMPILATION PIPELINE │
10 + └─────────────────────────────────────────────────────────────┘
11 + │
12 + ▼
13 +┌─────────────────────────────────────────────────────────────────────────────────────┐
14 +│ PHASE 1: HIR CONSTRUCTION │
15 +│ ┌─────────┐ │
16 +│ │ Babel │──▶ lower ──▶ enterSSA ──▶ eliminateRedundantPhi │
17 +│ │ AST │ │ │
18 +│ └─────────┘ ▼ │
19 +│ ┌──────────┐ │
20 +│ │ HIR │ (Control Flow Graph in SSA Form) │
21 +│ └──────────┘ │
22 +└─────────────────────────────────────────────────────────────────────────────────────┘
23 + │
24 + ▼
25 +┌─────────────────────────────────────────────────────────────────────────────────────┐
26 +│ PHASE 2: OPTIMIZATION │
27 +│ │
28 +│ constantPropagation ──▶ deadCodeElimination ──▶ instructionReordering │
29 +│ │
30 +└─────────────────────────────────────────────────────────────────────────────────────┘
31 + │
32 + ▼
33 +┌─────────────────────────────────────────────────────────────────────────────────────┐
34 +│ PHASE 3: TYPE & EFFECT INFERENCE │
35 +│ │
36 +│ inferTypes ──▶ analyseFunctions ──▶ inferMutationAliasingEffects │
37 +│ │ │
38 +│ ▼ │
39 +│ inferMutationAliasingRanges ──▶ inferReactivePlaces │
40 +│ │
41 +└─────────────────────────────────────────────────────────────────────────────────────┘
42 + │
43 + ▼
44 +┌─────────────────────────────────────────────────────────────────────────────────────┐
45 +│ PHASE 4: REACTIVE SCOPE CONSTRUCTION │
46 +│ │
47 +│ inferReactiveScopeVariables ──▶ alignMethodCallScopes ──▶ alignObjectMethodScopes │
48 +│ │ │
49 +│ ▼ │
50 +│ alignReactiveScopesToBlockScopesHIR ──▶ mergeOverlappingReactiveScopesHIR │
51 +│ │ │
52 +│ ▼ │
53 +│ buildReactiveScopeTerminalsHIR ──▶ flattenReactiveLoopsHIR │
54 +│ │ │
55 +│ ▼ │
56 +│ flattenScopesWithHooksOrUseHIR ──▶ propagateScopeDependenciesHIR │
57 +│ │
58 +└─────────────────────────────────────────────────────────────────────────────────────┘
59 + │
60 + ▼
61 +┌─────────────────────────────────────────────────────────────────────────────────────┐
62 +│ PHASE 5: HIR → REACTIVE FUNCTION │
63 +│ │
64 +│ buildReactiveFunction │
65 +│ │ │
66 +│ ▼ │
67 +│ ┌───────────────────┐ │
68 +│ │ ReactiveFunction │ (Tree Structure) │
69 +│ └───────────────────┘ │
70 +│ │
71 +└─────────────────────────────────────────────────────────────────────────────────────┘
72 + │
73 + ▼
74 +┌─────────────────────────────────────────────────────────────────────────────────────┐
75 +│ PHASE 6: REACTIVE FUNCTION OPTIMIZATION │
76 +│ │
77 +│ pruneUnusedLabels ──▶ pruneNonEscapingScopes ──▶ pruneNonReactiveDependencies │
78 +│ │ │
79 +│ ▼ │
80 +│ pruneUnusedScopes ──▶ mergeReactiveScopesThatInvalidateTogether │
81 +│ │ │
82 +│ ▼ │
83 +│ pruneAlwaysInvalidatingScopes ──▶ propagateEarlyReturns ──▶ promoteUsedTemporaries │
84 +│ │
85 +└─────────────────────────────────────────────────────────────────────────────────────┘
86 + │
87 + ▼
88 +┌─────────────────────────────────────────────────────────────────────────────────────┐
89 +│ PHASE 7: CODE GENERATION │
90 +│ │
91 +│ renameVariables ──▶ codegenReactiveFunction │
92 +│ │ │
93 +│ ▼ │
94 +│ ┌─────────────┐ │
95 +│ │ Babel AST │ (With Memoization) │
96 +│ └─────────────┘ │
97 +│ │
98 +└─────────────────────────────────────────────────────────────────────────────────────┘
99 +```
100 +
101 +## Pass Categories
102 +
103 +### HIR Construction & SSA (1-3)
104 +
105 +| # | Pass | File | Description |
106 +|---|------|------|-------------|
107 +| 1 | [lower](01-lower.md) | `HIR/BuildHIR.ts` | Convert Babel AST to HIR control-flow graph |
108 +| 2 | [enterSSA](02-enterSSA.md) | `SSA/EnterSSA.ts` | Convert to Static Single Assignment form |
109 +| 3 | [eliminateRedundantPhi](03-eliminateRedundantPhi.md) | `SSA/EliminateRedundantPhi.ts` | Remove unnecessary phi nodes |
110 +
111 +### Optimization (4-5)
112 +
113 +| # | Pass | File | Description |
114 +|---|------|------|-------------|
115 +| 4 | [constantPropagation](04-constantPropagation.md) | `Optimization/ConstantPropagation.ts` | Sparse conditional constant propagation |
116 +| 5 | [deadCodeElimination](05-deadCodeElimination.md) | `Optimization/DeadCodeElimination.ts` | Remove unreferenced instructions |
117 +
118 +### Type Inference (6)
119 +
120 +| # | Pass | File | Description |
121 +|---|------|------|-------------|
122 +| 6 | [inferTypes](06-inferTypes.md) | `TypeInference/InferTypes.ts` | Constraint-based type unification |
123 +
124 +### Mutation/Aliasing Inference (7-10)
125 +
126 +| # | Pass | File | Description |
127 +|---|------|------|-------------|
128 +| 7 | [analyseFunctions](07-analyseFunctions.md) | `Inference/AnalyseFunctions.ts` | Analyze nested function effects |
129 +| 8 | [inferMutationAliasingEffects](08-inferMutationAliasingEffects.md) | `Inference/InferMutationAliasingEffects.ts` | Infer mutation/aliasing via abstract interpretation |
130 +| 9 | [inferMutationAliasingRanges](09-inferMutationAliasingRanges.md) | `Inference/InferMutationAliasingRanges.ts` | Compute mutable ranges from effects |
131 +| 10 | [inferReactivePlaces](10-inferReactivePlaces.md) | `Inference/InferReactivePlaces.ts` | Mark reactive places (props, hooks, derived) |
132 +
133 +### Reactive Scope Variables (11-12)
134 +
135 +| # | Pass | File | Description |
136 +|---|------|------|-------------|
137 +| 11 | [inferReactiveScopeVariables](11-inferReactiveScopeVariables.md) | `ReactiveScopes/InferReactiveScopeVariables.ts` | Group co-mutating variables into scopes |
138 +| 12 | [rewriteInstructionKindsBasedOnReassignment](12-rewriteInstructionKindsBasedOnReassignment.md) | `SSA/RewriteInstructionKindsBasedOnReassignment.ts` | Convert SSA loads to context loads for reassigned vars |
139 +
140 +### Scope Alignment (13-15)
141 +
142 +| # | Pass | File | Description |
143 +|---|------|------|-------------|
144 +| 13 | [alignMethodCallScopes](13-alignMethodCallScopes.md) | `ReactiveScopes/AlignMethodCallScopes.ts` | Align method call scopes with receivers |
145 +| 14 | [alignObjectMethodScopes](14-alignObjectMethodScopes.md) | `ReactiveScopes/AlignObjectMethodScopes.ts` | Align object method scopes |
146 +| 15 | [alignReactiveScopesToBlockScopesHIR](15-alignReactiveScopesToBlockScopesHIR.md) | `ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts` | Align to control-flow block boundaries |
147 +
148 +### Scope Construction (16-18)
149 +
150 +| # | Pass | File | Description |
151 +|---|------|------|-------------|
152 +| 16 | [mergeOverlappingReactiveScopesHIR](16-mergeOverlappingReactiveScopesHIR.md) | `HIR/MergeOverlappingReactiveScopesHIR.ts` | Merge overlapping scopes |
153 +| 17 | [buildReactiveScopeTerminalsHIR](17-buildReactiveScopeTerminalsHIR.md) | `HIR/BuildReactiveScopeTerminalsHIR.ts` | Insert scope terminals into CFG |
154 +| 18 | [flattenReactiveLoopsHIR](18-flattenReactiveLoopsHIR.md) | `ReactiveScopes/FlattenReactiveLoopsHIR.ts` | Prune scopes inside loops |
155 +
156 +### Scope Flattening & Dependencies (19-20)
157 +
158 +| # | Pass | File | Description |
159 +|---|------|------|-------------|
160 +| 19 | [flattenScopesWithHooksOrUseHIR](19-flattenScopesWithHooksOrUseHIR.md) | `ReactiveScopes/FlattenScopesWithHooksOrUseHIR.ts` | Prune scopes containing hooks |
161 +| 20 | [propagateScopeDependenciesHIR](20-propagateScopeDependenciesHIR.md) | `HIR/PropagateScopeDependenciesHIR.ts` | Derive minimal scope dependencies |
162 +
163 +### HIR → Reactive Conversion (21)
164 +
165 +| # | Pass | File | Description |
166 +|---|------|------|-------------|
167 +| 21 | [buildReactiveFunction](21-buildReactiveFunction.md) | `ReactiveScopes/BuildReactiveFunction.ts` | Convert CFG to tree structure |
168 +
169 +### Reactive Function Pruning (22-25)
170 +
171 +| # | Pass | File | Description |
172 +|---|------|------|-------------|
173 +| 22 | [pruneUnusedLabels](22-pruneUnusedLabels.md) | `ReactiveScopes/PruneUnusedLabels.ts` | Remove unused labels |
174 +| 23 | [pruneNonEscapingScopes](23-pruneNonEscapingScopes.md) | `ReactiveScopes/PruneNonEscapingScopes.ts` | Remove non-escaping scopes |
175 +| 24 | [pruneNonReactiveDependencies](24-pruneNonReactiveDependencies.md) | `ReactiveScopes/PruneNonReactiveDependencies.ts` | Remove non-reactive dependencies |
176 +| 25 | [pruneUnusedScopes](25-pruneUnusedScopes.md) | `ReactiveScopes/PruneUnusedScopes.ts` | Remove empty scopes |
177 +
178 +### Scope Optimization (26-28)
179 +
180 +| # | Pass | File | Description |
181 +|---|------|------|-------------|
182 +| 26 | [mergeReactiveScopesThatInvalidateTogether](26-mergeReactiveScopesThatInvalidateTogether.md) | `ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts` | Merge co-invalidating scopes |
183 +| 27 | [pruneAlwaysInvalidatingScopes](27-pruneAlwaysInvalidatingScopes.md) | `ReactiveScopes/PruneAlwaysInvalidatingScopes.ts` | Prune always-invalidating scopes |
184 +| 28 | [propagateEarlyReturns](28-propagateEarlyReturns.md) | `ReactiveScopes/PropagateEarlyReturns.ts` | Handle early returns in scopes |
185 +
186 +### Codegen Preparation (29-31)
187 +
188 +| # | Pass | File | Description |
189 +|---|------|------|-------------|
190 +| 29 | [promoteUsedTemporaries](29-promoteUsedTemporaries.md) | `ReactiveScopes/PromoteUsedTemporaries.ts` | Promote temps to named vars |
191 +| 30 | [renameVariables](30-renameVariables.md) | `ReactiveScopes/RenameVariables.ts` | Ensure unique variable names |
192 +| 31 | [codegenReactiveFunction](31-codegenReactiveFunction.md) | `ReactiveScopes/CodegenReactiveFunction.ts` | Generate final Babel AST |
193 +
194 +### Transformations (32-38)
195 +
196 +| # | Pass | File | Description |
197 +|---|------|------|-------------|
198 +| 32 | [transformFire](32-transformFire.md) | `Transform/TransformFire.ts` | Transform `fire()` calls in effects |
199 +| 33 | [lowerContextAccess](33-lowerContextAccess.md) | `Optimization/LowerContextAccess.ts` | Optimize context access with selectors |
200 +| 34 | [optimizePropsMethodCalls](34-optimizePropsMethodCalls.md) | `Optimization/OptimizePropsMethodCalls.ts` | Normalize props method calls |
201 +| 35 | [optimizeForSSR](35-optimizeForSSR.md) | `Optimization/OptimizeForSSR.ts` | SSR-specific optimizations |
202 +| 36 | [outlineJSX](36-outlineJSX.md) | `Optimization/OutlineJsx.ts` | Outline JSX to components |
203 +| 37 | [outlineFunctions](37-outlineFunctions.md) | `Optimization/OutlineFunctions.ts` | Outline pure functions |
204 +| 38 | [memoizeFbtAndMacroOperandsInSameScope](38-memoizeFbtAndMacroOperandsInSameScope.md) | `ReactiveScopes/MemoizeFbtAndMacroOperandsInSameScope.ts` | Keep FBT operands together |
205 +
206 +### Validation (39-55)
207 +
208 +| # | Pass | File | Description |
209 +|---|------|------|-------------|
210 +| 39 | [validateContextVariableLValues](39-validateContextVariableLValues.md) | `Validation/ValidateContextVariableLValues.ts` | Variable reference consistency |
211 +| 40 | [validateUseMemo](40-validateUseMemo.md) | `Validation/ValidateUseMemo.ts` | useMemo callback requirements |
212 +| 41 | [validateHooksUsage](41-validateHooksUsage.md) | `Validation/ValidateHooksUsage.ts` | Rules of Hooks |
213 +| 42 | [validateNoCapitalizedCalls](42-validateNoCapitalizedCalls.md) | `Validation/ValidateNoCapitalizedCalls.ts` | Component vs function calls |
214 +| 43 | [validateLocalsNotReassignedAfterRender](43-validateLocalsNotReassignedAfterRender.md) | `Validation/ValidateLocalsNotReassignedAfterRender.ts` | Variable mutation safety |
215 +| 44 | [validateNoSetStateInRender](44-validateNoSetStateInRender.md) | `Validation/ValidateNoSetStateInRender.ts` | No setState during render |
216 +| 45 | [validateNoDerivedComputationsInEffects](45-validateNoDerivedComputationsInEffects.md) | `Validation/ValidateNoDerivedComputationsInEffects.ts` | Effect optimization hints |
217 +| 46 | [validateNoSetStateInEffects](46-validateNoSetStateInEffects.md) | `Validation/ValidateNoSetStateInEffects.ts` | Effect performance |
218 +| 47 | [validateNoJSXInTryStatement](47-validateNoJSXInTryStatement.md) | `Validation/ValidateNoJSXInTryStatement.ts` | Error boundary usage |
219 +| 48 | [validateNoImpureValuesInRender](48-validateNoImpureValuesInRender.md) | `Validation/ValidateNoImpureValuesInRender.ts` | Impure value isolation |
220 +| 49 | [validateNoRefAccessInRender](49-validateNoRefAccessInRender.md) | `Validation/ValidateNoRefAccessInRender.ts` | Ref access constraints |
221 +| 50 | [validateNoFreezingKnownMutableFunctions](50-validateNoFreezingKnownMutableFunctions.md) | `Validation/ValidateNoFreezingKnownMutableFunctions.ts` | Mutable function isolation |
222 +| 51 | [validateExhaustiveDependencies](51-validateExhaustiveDependencies.md) | `Validation/ValidateExhaustiveDependencies.ts` | Dependency array completeness |
223 +| 52 | [validateMemoizedEffectDependencies](52-validateMemoizedEffectDependencies.md) | `Validation/ValidateMemoizedEffectDependencies.ts` | Effect scope memoization |
224 +| 53 | [validatePreservedManualMemoization](53-validatePreservedManualMemoization.md) | `Validation/ValidatePreservedManualMemoization.ts` | Manual memo preservation |
225 +| 54 | [validateStaticComponents](54-validateStaticComponents.md) | `Validation/ValidateStaticComponents.ts` | Component identity stability |
226 +| 55 | [validateSourceLocations](55-validateSourceLocations.md) | `Validation/ValidateSourceLocations.ts` | Source location preservation |
227 +
228 +## Key Data Structures
229 +
230 +### HIR (High-level Intermediate Representation)
231 +
232 +The compiler converts source code to HIR for analysis. Key types:
233 +
234 +- **HIRFunction**: A function being compiled
235 + - `body.blocks`: Map of BasicBlocks (control flow graph)
236 + - `context`: Captured variables from outer scope
237 + - `params`: Function parameters
238 + - `returns`: The function's return place
239 +
240 +- **BasicBlock**: A sequence of instructions with a terminal
241 + - `instructions`: Array of Instructions
242 + - `terminal`: Control flow (return, branch, etc.)
243 + - `phis`: Phi nodes for SSA
244 +
245 +- **Instruction**: A single operation
246 + - `lvalue`: The place being assigned to
247 + - `value`: The instruction kind (CallExpression, FunctionExpression, etc.)
248 + - `effects`: Array of AliasingEffects
249 +
250 +- **Place**: A reference to a value
251 + - `identifier.id`: Unique IdentifierId
252 + - `effect`: How the place is used (read, mutate, etc.)
253 +
254 +### ReactiveFunction
255 +
256 +After HIR is analyzed, it's converted to ReactiveFunction:
257 +
258 +- Tree structure instead of CFG
259 +- Contains ReactiveScopes that define memoization boundaries
260 +- Each scope has dependencies and declarations
261 +
262 +### AliasingEffects
263 +
264 +Effects describe data flow and operations:
265 +
266 +- **Capture/Alias**: Value relationships
267 +- **Mutate/MutateTransitive**: Mutation tracking
268 +- **Freeze**: Immutability marking
269 +- **Render**: JSX usage context
270 +- **Create/CreateFunction**: Value creation
271 +
272 +## Feature Flags
273 +
274 +Many passes are controlled by feature flags in `Environment.ts`:
275 +
276 +| Flag | Enables Pass |
277 +|------|--------------|
278 +| `enableFire` | transformFire |
279 +| `lowerContextAccess` | lowerContextAccess |
280 +| `enableJsxOutlining` | outlineJSX |
281 +| `enableFunctionOutlining` | outlineFunctions |
282 +| `validateNoSetStateInRender` | validateNoSetStateInRender |
283 +| `enableUseMemoCacheInterop` | Preserves manual memoization |
284 +
285 +## Running Tests
286 +
287 +```bash
288 +# Run all tests
289 +yarn snap
290 +
291 +# Run specific fixture
292 +yarn snap -p <fixture-name>
293 +
294 +# Run with debug output (shows all passes)
295 +yarn snap -p <fixture-name> -d
296 +
297 +# Update expected outputs
298 +yarn snap -u
299 +```
300 +
301 +## Further Reading
302 +
303 +- [MUTABILITY_ALIASING_MODEL.md](../../src/Inference/MUTABILITY_ALIASING_MODEL.md): Detailed aliasing model docs
304 +- [Pipeline.ts](../../src/Entrypoint/Pipeline.ts): Pass ordering and orchestration
305 +- [HIR.ts](../../src/HIR/HIR.ts): Core data structure definitions