main
md 1,460 lines 90 KB
Rendered Raw
1 # React Compiler: Rust Port Feasibility Research
2
3 ## Table of Contents
4
5 1. [Executive Summary](#executive-summary)
6 2. [Key Data Structures](#key-data-structures)
7 3. [The Shared Mutable Reference Problem](#the-shared-mutable-reference-problem)
8 4. [Environment as Shared Mutable State](#environment-as-shared-mutable-state)
9 5. [Side Maps: Passes Storing HIR References](#side-maps-passes-storing-hir-references)
10 6. [AliasingEffect: Shared References and Rust Ownership](#aliasingeffect-shared-references-and-rust-ownership)
11 7. [Recommended Rust Architecture](#recommended-rust-architecture)
12 8. [Input/Output Format](#inputoutput-format)
13 9. [Error Handling](#error-handling)
14 10. [Structural Similarity: TypeScript ↔ Rust Alignment](#structural-similarity-typescript--rust-alignment)
15 11. [Pipeline Overview](#pipeline-overview)
16 12. [Pass-by-Pass Analysis](#pass-by-pass-analysis)
17 - [Phase 1: Lowering (AST to HIR)](#phase-1-lowering)
18 - [Phase 2: Normalization](#phase-2-normalization)
19 - [Phase 3: SSA Construction](#phase-3-ssa-construction)
20 - [Phase 4: Optimization (Pre-Inference)](#phase-4-optimization-pre-inference)
21 - [Phase 5: Type and Effect Inference](#phase-5-type-and-effect-inference)
22 - [Phase 6: Mutation/Aliasing Analysis](#phase-6-mutationaliasing-analysis)
23 - [Phase 7: Optimization (Post-Inference)](#phase-7-optimization-post-inference)
24 - [Phase 8: Reactivity Inference](#phase-8-reactivity-inference)
25 - [Phase 9: Scope Construction](#phase-9-scope-construction)
26 - [Phase 10: Scope Alignment and Merging](#phase-10-scope-alignment-and-merging)
27 - [Phase 11: Scope Terminal Construction](#phase-11-scope-terminal-construction)
28 - [Phase 12: Scope Dependency Propagation](#phase-12-scope-dependency-propagation)
29 - [Phase 13: Reactive Function Construction](#phase-13-reactive-function-construction)
30 - [Phase 14: Reactive Function Transforms](#phase-14-reactive-function-transforms)
31 - [Phase 15: Codegen](#phase-15-codegen)
32 - [Validation Passes](#validation-passes)
33 13. [External Dependencies](#external-dependencies)
34 14. [Risk Assessment](#risk-assessment)
35 15. [Recommended Migration Strategy](#recommended-migration-strategy)
36
37 ---
38
39 ## Executive Summary
40
41 Porting the React Compiler from TypeScript to Rust is **feasible and the Rust code can remain structurally very close to the TypeScript**. The compiler's algorithms are well-suited to Rust. The TypeScript implementation relies on patterns that conflict with Rust's ownership model, but all have clean, well-understood solutions using arenas and indirect references:
42
43 1. **Shared Identifier references**: Multiple `Place` objects reference the same `Identifier` object. **Solution**: Arena-allocated identifiers on `Environment`, referenced by copyable `IdentifierId` index.
44
45 2. **Shared ReactiveScope references**: Multiple identifiers share the same `ReactiveScope` object (including its mutable range). **Solution**: Arena-allocated scopes on `Environment`, referenced by `ScopeId`.
46
47 3. **Inner function storage**: `FunctionExpression`/`ObjectMethod` instructions store inner `HIRFunction` values inline. **Solution**: Arena-allocated functions on `Environment`, referenced by `FunctionId`.
48
49 4. **Type storage**: Types stored inline on identifiers. **Solution**: Arena-allocated types on `Environment`, referenced by `TypeId`.
50
51 5. **Instructions stored inline in blocks**: `BasicBlock.instructions` stores `Instruction` objects directly. **Solution**: Flat instruction table on `HIRFunction`, referenced by `InstructionId`. The existing `InstructionId` (evaluation order counter) is renamed to `EvaluationOrder` since it applies to both instructions and terminals.
52
53 6. **Environment as shared mutable singleton**: The `Environment` object is threaded through the entire compilation via `fn.env` and mutated by many passes. **Solution**: Remove `HIRFunction.env` and pass `env: &mut Environment` separately. Maintain existing fields (no sub-struct grouping) to allow precise sliced borrows via direct field access.
54
55 **Key finding on structural similarity**: After deep analysis of every pass, the vast majority of compiler passes can be ported to Rust with **~85-95% structural correspondence** — meaning you could view the TypeScript and Rust side-by-side and easily trace the logic. The main mechanical differences are:
56 - `match` instead of `switch` (exhaustive by default in Rust)
57 - `HashMap<IdentifierId, T>` instead of `Map<Identifier, T>` (reference identity → value identity)
58 - `Vec::retain()` instead of delete-during-Set-iteration
59 - `std::mem::replace` / `std::mem::take` for in-place enum variant swaps
60 - Two-phase collect/apply instead of mutate-through-stored-references
61
62 **Complexity breakdown** (revised after deep per-pass analysis):
63 - ~25 passes are straightforward to port (simple traversal, local mutation, ID-only side maps)
64 - ~13 passes require moderate refactoring (stored references → IDs, iteration order changes)
65 - ~4 passes require significant redesign (InferMutationAliasingRanges, BuildHIR, CodegenReactiveFunction, AnalyseFunctions)
66 - Input/output boundaries use JSON AST interchange via serde, with a Rust Babel AST type
67
68 **Input/output format**: Define a Rust representation of the Babel AST format using serde with custom serialization/deserialization (ensuring the `"type"` field is always produced, even outside of enum positions). Include full information from Babel, including source locations. A `Scope` type encodes the tree of scope information mapping to Babel's scope tree. The main public API is `compile(BabelAst, Scope) -> Option<BabelAst>`, returning `None` if no changes.
69
70 **Error handling**: Two categories — errors that would have thrown in TypeScript (invariants, todo errors, short-circuiting) return `Err(CompilerDiagnostic)` via `Result`, while non-throwing accumulated diagnostics are recorded directly on `Environment`. TypeScript non-null assertions become `.unwrap()` panics.
71
72 **Note on InferMutationAliasingEffects**: Previously categorized as "significant redesign" due to maps using JS reference identity with `InstructionValue` keys. An upstream refactor ([PR #33650](https://github.com/facebook/react/pull/33650)) replaces `InstructionValue` with interned `AliasingEffect` as allocation-site keys, eliminating synthetic InstructionValues and the `effectInstructionValueCache`. Since effects are already interned by content hash, they map directly to a copyable `EffectId` index in Rust. Additionally, `AliasingEffect` variants share `Place` references with `InstructionValue` fields — in Rust, Places are cloned cheaply (with arena-based `IdentifierId`). The `CreateFunction` variant's `FunctionExpression` reference is replaced with a `FunctionId` referencing the function arena on `Environment`. See [§AliasingEffect section](#aliasingeffect-shared-references-and-rust-ownership) for the full analysis. This is "moderate refactoring" — no algorithmic redesign needed.
73
74 ---
75
76 ## Key Data Structures
77
78 ### HIRFunction
79 ```
80 HIRFunction {
81 body: HIR {
82 entry: BlockId,
83 blocks: Map<BlockId, BasicBlock> // ordered map, reverse postorder
84 },
85 instructions: Vec<Instruction>, // flat instruction table, indexed by InstructionId
86 params: Array<Place | SpreadPattern>,
87 returns: Place,
88 context: Array<Place>, // captured variables from outer scope
89 aliasingEffects: Array<AliasingEffect> | null,
90 }
91 ```
92
93 **Note**: `env` is removed from `HIRFunction` and passed separately as `env: &mut Environment`. Inner functions are stored in the function arena on `Environment` (see [§Recommended Rust Architecture](#recommended-rust-architecture)).
94
95 ### BasicBlock
96 ```
97 BasicBlock {
98 id: BlockId,
99 kind: 'block' | 'value' | 'loop' | 'sequence' | 'catch',
100 instructions: Vec<InstructionId>, // indices into HIRFunction.instructions
101 terminal: Terminal, // control flow (goto, if, for, return, etc.)
102 preds: Set<BlockId>,
103 phis: Set<Phi>, // SSA join points
104 }
105 ```
106
107 ### Instruction
108 ```
109 Instruction {
110 order: EvaluationOrder, // evaluation order (renamed from InstructionId)
111 lvalue: Place, // destination
112 value: InstructionValue, // discriminated union (~40 variants)
113 effects: Array<AliasingEffect> | null, // populated by InferMutationAliasingEffects
114 loc: SourceLocation,
115 }
116 ```
117
118 **Note**: The previous `InstructionId` type is renamed to `EvaluationOrder` because it represents evaluation order and is not instruction-specific (terminals also carry it). A new `InstructionId` type is introduced as an index into the `HIRFunction.instructions` table, allowing passes to reference instructions by a single copyable ID rather than `(BlockId, usize)`.
119
120 ### Place (CRITICAL for Rust port)
121 ```
122 Place {
123 kind: 'Identifier',
124 identifier: IdentifierId, // ← index into Identifier arena on Environment (shared reference in TS)
125 effect: Effect, // Read, Mutate, Capture, Freeze, etc.
126 reactive: boolean, // set by InferReactivePlaces
127 loc: SourceLocation,
128 }
129 ```
130
131 ### Identifier (CRITICAL for Rust port)
132 ```
133 Identifier {
134 id: IdentifierId, // unique after SSA (opaque number)
135 declarationId: DeclarationId,
136 name: IdentifierName | null, // null for temporaries, mutated by RenameVariables
137 mutableRange: MutableRange, // { start, end } — mutated by InferMutationAliasingRanges
138 scope: ScopeId | null, // index into scope arena — mutated by InferReactiveScopeVariables
139 type: TypeId, // index into type arena — mutated by InferTypes
140 loc: SourceLocation,
141 }
142 ```
143
144 ### FunctionExpression / ObjectMethod
145 ```
146 FunctionExpression {
147 loweredFunc: FunctionId, // index into function arena on Environment
148 ... // other fields remain inline
149 }
150 ```
151
152 **Note**: Inner `HIRFunction` values are stored in a function arena on `Environment`, referenced by `FunctionId`. This replaces inline storage and provides a stable, copyable reference for passes that need to cache or access inner functions.
153
154 ### ReactiveScope
155 ```
156 ReactiveScope {
157 id: ScopeId,
158 range: MutableRange, // mutated by alignment passes
159 dependencies: Set<ReactiveScopeDependency>, // populated by PropagateScopeDependencies
160 declarations: Map<IdentifierId, ReactiveScopeDeclaration>,
161 reassignments: Set<IdentifierId>,
162 earlyReturnValue: { value: IdentifierId, loc, label } | null,
163 merged: Set<ScopeId>,
164 }
165 ```
166
167 ### MutableRange
168 ```
169 MutableRange {
170 start: EvaluationOrder, // inclusive (renamed from InstructionId)
171 end: EvaluationOrder, // exclusive
172 }
173 ```
174
175 ---
176
177 ## The Shared Mutable Reference Problem
178
179 This is the **central challenge** for a Rust port. In TypeScript, the compiler relies on JavaScript's reference semantics in three pervasive patterns:
180
181 ### Pattern 1: Shared Identifier Mutation
182 ```typescript
183 // Multiple Place objects share the SAME Identifier object
184 const place1: Place = { identifier: someIdentifier, ... };
185 const place2: Place = { identifier: someIdentifier, ... }; // same object!
186
187 // A pass mutates the identifier through one place...
188 place1.identifier.mutableRange.end = 42;
189
190 // ...and the change is visible through the other
191 console.log(place2.identifier.mutableRange.end); // 42
192 ```
193
194 Used by: InferMutationAliasingRanges, InferReactiveScopeVariables, InferTypes, InferReactivePlaces, RenameVariables, PromoteUsedTemporaries, EnterSSA, EliminateRedundantPhi, AnalyseFunctions, and many more.
195
196 ### Pattern 2: Shared ReactiveScope References
197 ```typescript
198 // Multiple Identifiers share the same ReactiveScope AND MutableRange
199 identifier.mutableRange = scope.range; // line 132 of InferReactiveScopeVariables
200
201 // Now identifier.mutableRange IS scope.range (same JS object)
202 // A pass expands the scope range...
203 scope.range.end = 100;
204
205 // ...visible through the identifier
206 console.log(identifier.mutableRange.end); // 100
207 ```
208
209 This is explicitly noted in AnalyseFunctions.ts (line 30-34): "NOTE: inferReactiveScopeVariables makes identifiers in the scope point to the *same* mutableRange instance."
210
211 Used by: AlignMethodCallScopes, AlignObjectMethodScopes, AlignReactiveScopesToBlockScopesHIR, MergeOverlappingReactiveScopesHIR, MemoizeFbtAndMacroOperandsInSameScope.
212
213 ### Pattern 3: Iterate-and-Mutate / Side Map References
214 ```typescript
215 // Store a reference to an HIR object in a side map
216 const nodes: Map<Identifier, Node> = new Map();
217 nodes.set(identifier, { id: identifier, ... });
218
219 // Later, mutate the object through the stored reference
220 node.id.mutableRange.end = 42; // mutates HIR through map reference
221 ```
222
223 Used by: InferMutationAliasingRanges (AliasingState.nodes), EnterSSA (SSABuilder.#states.defs), InferMutationAliasingEffects (Context caches — see note below about upstream simplification), DropManualMemoization (sidemap.manualMemos), InlineIIFEs (functions map), AlignReactiveScopesToBlockScopesHIR (activeScopes), and others.
224
225 ---
226
227 ## Environment as Shared Mutable State
228
229 ### Complete Environment Analysis
230
231 Environment is created once per top-level function compilation and stored on `HIRFunction.env`. It is shared via reference across the entire compilation, including nested functions.
232
233 #### Mutable State (mutated by passes)
234 | Field | Mutated by | Pattern |
235 |-------|-----------|---------|
236 | `#nextIdentifer: number` | BuildHIR, EnterSSA, OutlineJSX, InferMutationAliasingEffects (via `createTemporaryPlace`) | Auto-increment counter |
237 | `#nextBlock: number` | BuildHIR, InlineIIFEs | Auto-increment counter |
238 | `#nextScope: number` | InferReactiveScopeVariables | Auto-increment counter |
239 | `#errors: CompilerError` | All validation passes, DropManualMemoization, InferMutationAliasingRanges, CodegenReactiveFunction | Append-only accumulator |
240 | `#outlinedFunctions: Array` | OutlineJSX, OutlineFunctions | Append-only list |
241 | `#moduleTypes: Map` | `getGlobalDeclaration` (lazy cache fill) | One-time lazy initialization |
242
243 #### Read-Only State (accessed but never mutated)
244 | Field | Accessed by |
245 |-------|------------|
246 | `config: EnvironmentConfig` | Pipeline.ts (feature flags), InferMutationAliasingEffects, DropManualMemoization, MemoizeFbtAndMacroOperandsInSameScope, InferReactiveScopeVariables |
247 | `fnType: ReactFunctionType` | Pipeline.ts |
248 | `outputMode: CompilerOutputMode` | Pipeline.ts, DeadCodeElimination |
249 | `#globals: GlobalRegistry` | InferTypes (via `getGlobalDeclaration`), DropManualMemoization |
250 | `#shapes: ShapeRegistry` | InferTypes (via `getPropertyType`, `getFunctionSignature`), InferMutationAliasingEffects, InferReactivePlaces, FlattenScopesWithHooksOrUseHIR, NameAnonymousFunctions |
251 | `logger` | Pipeline.ts, AnalyseFunctions |
252 | `programContext` | BuildHIR, CodegenReactiveFunction, OutlineJSX |
253
254 #### How Environment is Shared with Nested Functions
255
256 Parent and nested functions share the **exact same Environment instance**. When `lower()` is called for a nested function expression, it receives the same `env`. This means:
257 - ID counters are globally unique across the entire function tree
258 - Errors from inner function compilation are visible to the parent
259 - Outlined functions from inner compilations accumulate on the shared list
260 - Configuration is shared (same feature flags everywhere)
261
262 This sharing is sequential, not concurrent: `AnalyseFunctions` processes each child function synchronously before returning to the parent.
263
264 ### Recommended Rust Representation
265
266 Remove `HIRFunction.env` and pass `env: &mut Environment` as a separate parameter to passes. Maintain the existing fields and types of the `Environment` struct — do not group them into sub-structs. Use direct field access (rather than methods) to allow precise sliced borrows of portions of the environment.
267
268 ```rust
269 struct Environment {
270 // Configuration (read-only after construction)
271 config: EnvironmentConfig,
272 fn_type: ReactFunctionType,
273 output_mode: CompilerOutputMode,
274
275 // Type registries (read-only after lazy init)
276 globals: GlobalRegistry,
277 shapes: ShapeRegistry,
278 module_types: HashMap<String, Option<Global>>,
279
280 // Mutable counters
281 next_identifier: IdentifierId,
282 next_block: BlockId,
283 next_scope: ScopeId,
284
285 // Arenas
286 identifiers: Vec<Identifier>, // indexed by IdentifierId
287 scopes: Vec<ReactiveScope>, // indexed by ScopeId
288 functions: Vec<HIRFunction>, // indexed by FunctionId
289 types: Vec<Type>, // indexed by TypeId
290
291 // Accumulated state
292 errors: Vec<CompilerDiagnostic>,
293 outlined_functions: Vec<OutlinedFunction>,
294
295 // Other
296 logger: Option<Logger>,
297 program_context: ProgramContext,
298 }
299 ```
300
301 **Why no sub-structs**: Keeping all fields flat on `Environment` allows Rust's borrow checker to reason about independent field borrows. For example, a pass can simultaneously borrow `env.identifiers` and `env.config` without conflict, because the borrow checker can see they are distinct fields. Grouping fields into sub-structs would require borrowing the entire sub-struct even when only one field is needed.
302
303 **Pass signatures** return `Result` for errors that would have thrown in TypeScript:
304
305 ```rust
306 // Most passes: need mutable HIR + mutable environment
307 fn enter_ssa(func: &mut HIRFunction, env: &mut Environment) -> Result<(), CompilerDiagnostic> { ... }
308
309 // Validation passes:
310 fn validate_hooks_usage(func: &HIRFunction, env: &mut Environment) -> Result<(), CompilerDiagnostic> { ... }
311
312 // Passes that don't use env at all (many!):
313 fn merge_consecutive_blocks(func: &mut HIRFunction) { ... }
314 fn constant_propagation(func: &mut HIRFunction) { ... }
315 ```
316
317 **Key insight from per-pass analysis**: The majority of passes (PruneMaybeThrows, MergeConsecutiveBlocks, ConstantPropagation, EliminateRedundantPhi, OptimizePropsMethodCalls, DeadCodeElimination, RewriteInstructionKinds, PruneUnusedLabelsHIR, FlattenReactiveLoopsHIR, and all reactive function transforms) do NOT use Environment at all. Only ~12 passes need `env`, and most only read config flags or call `getHookKind()`.
318
319 For the `AnalyseFunctions` recursive pattern (where parent and child share the same Environment), `&mut Environment` works naturally because the recursive call completes before the parent continues — there is only one `&mut` active at a time.
320
321 ---
322
323 ## Side Maps: Passes Storing HIR References
324
325 ### The Core Problem
326
327 Many passes store references to HIR values (Places, Identifiers, Instructions, InstructionValues, ReactiveScopes) in "side maps" (HashMaps, Sets, arrays) while simultaneously mutating the HIR. In Rust, this creates borrow conflicts because you cannot hold an immutable reference (in the map) while mutating through a different path.
328
329 ### Classification of Side Map Patterns
330
331 After analyzing every pass, side map patterns fall into four categories:
332
333 #### Category 1: ID-Only Maps (No Borrow Issues)
334 Maps keyed and valued by opaque IDs (`IdentifierId`, `BlockId`, `ScopeId`, `InstructionId`, `DeclarationId`). These are `Copy` types with no aliasing concerns.
335
336 **Passes**: PruneMaybeThrows, MergeConsecutiveBlocks, ConstantPropagation, DeadCodeElimination, RewriteInstructionKinds, InferReactivePlaces (reactive set), PruneUnusedLabelsHIR, FlattenReactiveLoopsHIR, FlattenScopesWithHooksOrUseHIR, StabilizeBlockIds, and most reactive function transforms.
337
338 **Rust approach**: Direct `HashMap<IdType, T>` / `HashSet<IdType>`. No changes needed.
339
340 #### Category 2: Reference-Identity Maps (Replace Keys with IDs)
341 Maps using JavaScript object identity (`===`) as the key, typically `Map<Identifier, T>` or `Map<BasicBlock, T>` or `DisjointSet<Identifier>` / `DisjointSet<ReactiveScope>`.
342
343 **Passes**: EnterSSA (`Map<BasicBlock, State>`, `Map<Identifier, Identifier>`), EliminateRedundantPhi (`Map<Identifier, Identifier>`), InferMutationAliasingRanges (`Map<Identifier, Node>`), InferReactiveScopeVariables (`DisjointSet<Identifier>`), InferReactivePlaces (`DisjointSet<Identifier>`), AlignMethodCallScopes (`DisjointSet<ReactiveScope>`), AlignObjectMethodScopes (`Set<Identifier>`, `DisjointSet<ReactiveScope>`), MergeOverlappingReactiveScopes (`DisjointSet<ReactiveScope>`).
344
345 **Rust approach**: Replace with `HashMap<IdentifierId, T>`, `HashMap<BlockId, T>`, `DisjointSet<IdentifierId>`, `DisjointSet<ScopeId>`. This is **always simpler and more correct** than the TypeScript — it eliminates an entire class of bugs where cloned objects silently fail identity checks.
346
347 #### Category 3: Instruction/Value Reference Maps (Store Indices Instead)
348 Maps that store references to actual `Instruction`, `FunctionExpression`, or `InstructionValue` objects, then later access fields on those objects or mutate them.
349
350 **Passes**: InferMutationAliasingEffects (`Map<Instruction, InstructionSignature>`, `Map<FunctionExpression, AliasingSignature>`), DropManualMemoization (`Map<IdentifierId, TInstruction<FunctionExpression>>`, `ManualMemoCallee.loadInstr`), InlineIIFEs (`Map<IdentifierId, FunctionExpression>`), NameAnonymousFunctions (`Node.fn: FunctionExpression`).
351
352 **Note**: InferMutationAliasingEffects currently uses `Map<InstructionValue, AbstractValue>` and `Map<IdentifierId, Set<InstructionValue>>` with `InstructionValue` objects as allocation-site identity tokens (JS reference identity), including both real InstructionValues from the HIR (for `CreateFunction`) and synthetic objects fabricated as allocation-site markers. An upstream refactor ([PR #33650](https://github.com/facebook/react/pull/33650)) replaces all `InstructionValue` keys with interned `AliasingEffect` objects, eliminating the synthetic InstructionValues and `effectInstructionValueCache` entirely. Since effects are already interned by content hash, reference identity equals content identity — exactly what's needed for Rust. In Rust, the `EffectId` (index into the interning table) serves as the allocation-site key directly. See [§AliasingEffect section](#aliasingeffect-shared-references-and-rust-ownership) for the full analysis.
353
354 **Rust approach**: Store only what is actually needed:
355 - If the map is for existence checking: use `HashSet<IdentifierId>`
356 - If specific fields are needed later: extract and store those fields (e.g., store `InstructionId` to reference the instruction table)
357 - Instructions are stored in a flat table on `HIRFunction`, referenced by `InstructionId` — passes can reference any instruction by a single copyable ID
358 - `FunctionExpression`/`ObjectMethod` inner functions are accessed via `FunctionId` referencing the function arena on `Environment`
359 - For InferMutationAliasingEffects: use `InstructionId` for instruction signature cache, `EffectId` (interning table index) for value-identity maps, `FunctionId` for function signature caches
360
361 #### Category 4: Scope Reference Sets with In-Place Mutation (Arena Access)
362 Sets or maps of `ReactiveScope` references where the scope's `range` fields are mutated while the scope is in the collection.
363
364 **Passes**: AlignReactiveScopesToBlockScopesHIR (`Set<ReactiveScope>` iterated while mutating `scope.range`), AlignMethodCallScopes (DisjointSet forEach with range mutation), AlignObjectMethodScopes (same pattern), MergeOverlappingReactiveScopesHIR (DisjointSet with range mutation), MemoizeFbtAndMacroOperandsInSameScope (scope range mutation).
365
366 **Rust approach**: Store `ScopeId` in sets/DisjointSets. Mutate through arena: `env.scopes[scope_id].range.start = ...`. The set holds copyable IDs, and the mutation goes through the arena — completely disjoint borrows.
367
368 ### Critical Insight: The Shared MutableRange Aliasing
369
370 The most architecturally significant side map pattern is in `InferReactiveScopeVariables` (line 132):
371 ```typescript
372 identifier.mutableRange = scope.range;
373 ```
374
375 This makes ALL identifiers in a scope share the SAME `MutableRange` object as the scope. Every subsequent scope-alignment pass relies on this: mutating `scope.range.start` automatically updates all identifiers' `mutableRange`.
376
377 **Recommended Rust approach**: Identifiers store `scope: Option<ScopeId>`. The "effective mutable range" is always accessed through the scope arena:
378 ```rust
379 fn effective_mutable_range(id: &Identifier, scopes: &[ReactiveScope]) -> MutableRange {
380 match id.scope {
381 Some(scope_id) => scopes[scope_id.index()].range,
382 None => id.mutable_range, // pre-scope original range
383 }
384 }
385 ```
386
387 All downstream passes that read `identifier.mutableRange` (like `isMutable()`, `inRange()`) would need access to `env.scopes`. This is a mechanical refactor — every call site accesses the scope arena via `Environment`.
388
389 ---
390
391 ## AliasingEffect: Shared References and Rust Ownership
392
393 ### Overview
394
395 `AliasingEffect` is a discriminated union (17 variants) that describes data flow, mutation, and other side effects of instructions and terminals. Effects are **created** by `InferMutationAliasingEffects`, stored on `Instruction.effects` and `Terminal.effects`, and **consumed** by `InferMutationAliasingRanges`, `AnalyseFunctions`, validation passes, and `PrintHIR`. This section analyzes the shared references between `AliasingEffect` variants, `Instruction`, and `InstructionValue`, and how they map to Rust ownership.
396
397 ### Shared Reference Inventory
398
399 Every `AliasingEffect` variant contains `Place` objects. In the TypeScript implementation, these are the **same JS object references** as the Places in the `InstructionValue` and `Instruction.lvalue` — not copies. This creates a web of shared references:
400
401 #### Category A: Place Sharing (Instruction/InstructionValue → Effect)
402
403 Nearly every instruction kind in `computeSignatureForInstruction` creates effects that directly reference Places from the instruction:
404
405 | InstructionValue Kind | Effect Created | Shared Place Fields |
406 |---|---|---|
407 | `ArrayExpression` | `Create into:lvalue`, `Capture from:element into:lvalue` | `lvalue`, each `element` from `value.elements` |
408 | `ObjectExpression` | `Create into:lvalue`, `Capture from:property.place into:lvalue` | `lvalue`, each `property.place` from `value.properties` |
409 | `PropertyStore/ComputedStore` | `Mutate value:object`, `Capture from:value into:object` | `value.object`, `value.value`, `lvalue` |
410 | `PropertyLoad/ComputedLoad` | `CreateFrom from:object into:lvalue` | `value.object`, `lvalue` |
411 | `PropertyDelete/ComputedDelete` | `Mutate value:object` | `value.object`, `lvalue` |
412 | `Destructure` | `CreateFrom from:value.value into:place` per pattern item | `value.value`, each pattern item place |
413 | `JsxExpression` | `Freeze value:operand`, `Capture`, `Render place:tag/child` | `lvalue`, `value.tag`, each child, each prop place |
414 | `GetIterator` | `Alias/Capture from:collection into:lvalue` | `value.collection`, `lvalue` |
415 | `IteratorNext` | `MutateConditionally value:iterator`, `CreateFrom from:collection` | `value.iterator`, `value.collection`, `lvalue` |
416 | `StoreLocal` | `Assign from:value.value into:value.lvalue.place` | `value.value`, `value.lvalue.place`, `lvalue` |
417 | `LoadLocal` | `Assign from:value.place into:lvalue` | `value.place`, `lvalue` |
418 | `Await` | `MutateTransitiveConditionally value:value.value`, `Capture` | `value.value`, `lvalue` |
419
420 #### Category B: Call Instructions — Deep Sharing via Apply
421
422 For `CallExpression`, `MethodCall`, and `NewExpression`, a single `Apply` effect is created that shares **multiple fields** including the args array itself:
423
424 ```typescript
425 // From computeSignatureForInstruction (line 1832-1841)
426 effects.push({
427 kind: 'Apply',
428 receiver, // same Place as value.receiver or value.callee
429 function: callee, // same Place as value.callee or value.property
430 mutatesFunction: ...,
431 args: value.args, // THE SAME ARRAY REFERENCE from InstructionValue
432 into: lvalue, // same Place as instruction.lvalue
433 signature, // shared FunctionSignature from type registry
434 loc: value.loc,
435 });
436 ```
437
438 The `args` field is the **exact same array object** as the InstructionValue's `args`. In Rust, this must be either cloned or accessed via the instruction.
439
440 #### Category C: FunctionExpression — The Deepest Sharing
441
442 The `CreateFunction` variant holds a direct reference to the `FunctionExpression` or `ObjectMethod` InstructionValue:
443
444 ```typescript
445 // From computeSignatureForInstruction (line 1946-1953)
446 effects.push({
447 kind: 'CreateFunction',
448 into: lvalue,
449 function: value, // THE SAME FunctionExpression/ObjectMethod InstructionValue
450 captures: value.loweredFunc.func.context.filter(
451 operand => operand.effect === Effect.Capture,
452 ),
453 });
454 ```
455
456 This is the most architecturally significant sharing because `effect.function` is used in three distinct ways:
457
458 1. **As an allocation-site token** in abstract interpretation (reference identity):
459 - `state.initialize(effect.function, {...})``#values.set(value, kind)` — FunctionExpression as map key
460 - `state.define(effect.into, effect.function)``#variables.set(id, new Set([value]))` — FunctionExpression as set value
461
462 2. **For deep structural access**:
463 - `effect.function.loweredFunc.func.aliasingEffects` — reads the nested function's inferred effects
464 - `effect.function.loweredFunc.func.context` — iterates captured variables
465
466 3. **For mutation** of the nested function's context:
467 - `operand.effect = Effect.Read` (line 838) — mutates `Place.effect` on the nested function's context variables
468
469 **Rust approach**: `CreateFunction` stores a `FunctionId` referencing the function arena on `Environment`. Allocation-site identity uses `EffectId` (from effect interning), deep structural access uses `env.functions[function_id]`, and context mutation uses `&mut env.functions[function_id].context`.
470
471 ### Allocation-Site Identity: InstructionValue → AliasingEffect (PR #33650)
472
473 The abstract interpretation in `InferenceState` tracks the abstract kind (Mutable, Frozen, Primitive, etc.) of each "allocation site" and which allocation sites each identifier points to. Currently this uses `InstructionValue` objects as allocation-site identity tokens via JS reference identity:
474
475 ```
476 #values: Map<InstructionValue, AbstractValue> // InstructionValue as KEY (reference identity)
477 #variables: Map<IdentifierId, Set<InstructionValue>> // InstructionValue as SET VALUE
478 ```
479
480 Allocation sites are created from:
481 - **Params/context variables**: Synthetic `{kind: 'Primitive'}` or `{kind: 'ObjectExpression'}` objects
482 - **`Create`/`CreateFrom` effects**: Synthetic InstructionValues via `effectInstructionValueCache` (maps interned effect → synthetic InstructionValue)
483 - **`CreateFunction` effects**: The actual `FunctionExpression` InstructionValue from the HIR
484
485 **Upstream simplification** ([facebook/react#33650](https://github.com/facebook/react/pull/33650)): This PR replaces `InstructionValue` with the interned `AliasingEffect` itself as the allocation-site key:
486
487 ```
488 #values: Map<AliasingEffect, AbstractValue> // interned AliasingEffect as KEY
489 #variables: Map<IdentifierId, Set<AliasingEffect>>
490 ```
491
492 The changes:
493 1. **Params/context**: Synthetic `InstructionValue` objects are replaced with `AliasingEffect` objects (e.g., `{kind: 'Create', into: place, value: ValueKind.Context, reason: ValueReason.Other}`)
494 2. **`Create`/`CreateFrom` effects**: `effectInstructionValueCache` is eliminated entirely. `state.initialize(effect, ...)` and `state.define(place, effect)` use the interned effect directly as the key/value
495 3. **`CreateFunction` effects**: `state.initialize(effect.function, ...)``state.initialize(effect, ...)` — the CreateFunction effect itself is the key, not the FunctionExpression
496 4. **`state.values()` return type**: Changes from `Array<InstructionValue>` to `Array<AliasingEffect>`. Code that checks function values now uses `values[0].kind === 'CreateFunction'` and accesses `values[0].function` for the FunctionExpression
497 5. **`freezeValue` method**: Checks `value.kind === 'CreateFunction'` and accesses `value.function.loweredFunc.func.context` instead of `value.kind === 'FunctionExpression'`
498
499 Since effects are already interned by content hash (via `context.internEffect()`), reference identity equals content identity. This means the interned `AliasingEffect` maps directly to a copyable `EffectId` index in Rust — no separate `AllocationSiteId` type is needed.
500
501 **Key insight for CreateFunction**: After PR #33650, the `CreateFunction` effect's `function` field (the FunctionExpression/ObjectMethod reference) is **no longer used as a map key** for allocation-site tracking. It is only used for:
502 1. **Deep structural access**: `effect.function.loweredFunc.func.context` and `.aliasingEffects`
503 2. **As a key in `functionSignatureCache`**: `Map<FunctionExpression, AliasingSignature>` (the one remaining reference-identity map using FunctionExpression)
504 3. **Mutation**: `operand.effect = Effect.Read` on context variables
505
506 In Rust, `CreateFunction` stores a `FunctionId` referencing the function arena on `Environment`. The function's context and aliasing effects are accessed via `env.functions[function_id]`. The allocation-site identity is the `EffectId` of the interned CreateFunction effect. The `functionSignatureCache` keys by `FunctionId` instead of FunctionExpression reference.
507
508 ### Effect Interning
509
510 Effects are interned by content hash in `Context.internEffect()`:
511
512 ```typescript
513 internEffect(effect: AliasingEffect): AliasingEffect {
514 const hash = hashEffect(effect); // hash based on identifier IDs, not Place references
515 let interned = this.internedEffects.get(hash);
516 if (interned == null) {
517 this.internedEffects.set(hash, effect);
518 interned = effect;
519 }
520 return interned;
521 }
522 ```
523
524 The hash uses `place.identifier.id` (a number) rather than Place reference identity. The interned effect retains the Place references from whichever instruction first created that hash. In the fixpoint loop, re-processing an instruction may produce an effect with the same hash but different Place objects; interning returns the **original** effect with its original Place references. This is safe in TypeScript (both Places point to the same shared Identifier), but in Rust it means the interned effect's Places may not be the "current" instruction's Places — they are equivalent by ID but different allocations.
525
526 With PR #33650, the interned effect is also the allocation-site key. Since interning guarantees that the same `EffectId` is returned for structurally identical effects, the fixpoint loop correctly converges — the same allocation site is used across iterations.
527
528 ### Consumers: How Effects Are Read
529
530 #### InferMutationAliasingRanges (primary consumer)
531
532 Iterates `instr.effects` for every instruction and reads Place fields:
533 - `effect.into.identifier` → used as key in `AliasingState.nodes` and to call `state.create()`
534 - `effect.from.identifier` → used in `state.assign()`, `state.capture()`, `state.maybeAlias()`
535 - `effect.value.identifier` → stored in `mutations` array, passed to `state.mutate()`
536 - `effect.function.loweredFunc.func` → used in `state.create()` for Function nodes
537 - `effect.place.identifier` → stored in `renders` array for Render effects
538 - `effect.error` → for MutateFrozen/MutateGlobal/Impure, recorded on Environment
539
540 Also reads terminal effects: `block.terminal.effects` for Alias and Freeze effects on maybe-throw/return terminals.
541
542 Also reads effects a second time (Part 2, lines 359-421) to compute legacy per-operand `Effect` enum values. This pass accesses `effect.*.identifier.id` and `effect.*.identifier.mutableRange.end` through effect Places.
543
544 **Key observation**: InferMutationAliasingRanges reads `identifier.id`, `identifier` (for the reference-identity map key), and `identifier.mutableRange` from effect Places. It never mutates them through the effect's Places (mutations go through the graph nodes). With arena-based identifiers, `place.identifier` is an `IdentifierId` (`Copy`), and `mutableRange` is accessed via the identifier arena. No Place reference comparison is done — all passes access identifiers through their IDs, never by comparing Place object references.
545
546 #### AnalyseFunctions
547
548 Reads `fn.aliasingEffects` (the function-level effects from `InferMutationAliasingRanges`) to populate context variable effect annotations:
549 - `effect.from.identifier.id` — for Assign/Alias/Capture/CreateFrom/MaybeAlias variants
550 - `effect.value.identifier.id` — for Mutate/MutateConditionally/MutateTransitive/MutateTransitiveConditionally
551
552 Only reads identifier IDs. Does not access Places beyond `.identifier.id`.
553
554 #### ValidateNoFreezingKnownMutableFunctions
555
556 Reads `fn.aliasingEffects` on nested `FunctionExpression` values:
557 - Stores `Mutate`/`MutateTransitive` effects in `Map<IdentifierId, AliasingEffect>`
558 - Reads `effect.value.identifier.id`, `effect.value.identifier.name`, `effect.value.loc`
559
560 Accesses Identifier fields (name, loc) beyond just the ID, but these are read-only.
561
562 #### Other Passes (do NOT read AliasingEffects)
563
564 `ValidateLocalsNotReassignedAfterRender`, `ValidateNoImpureFunctionsInRender`, and `PruneNonEscapingScopes` import from AliasingEffects.ts or InferMutationAliasingEffects.ts but only use `getFunctionCallSignature` or the legacy `Effect` enum on Places — they do not read `instr.effects` or `fn.aliasingEffects`.
565
566 #### PrintHIR
567
568 Reads all effect fields for debug output. Read-only.
569
570 ### Recommended Rust Representation
571
572 #### AliasingEffect Enum
573
574 With arena-based identifiers, `Place` becomes a small `Copy`/`Clone` struct. Effects can own cloned Places:
575
576 ```rust
577 #[derive(Clone)]
578 enum AliasingEffect {
579 Freeze { value: Place, reason: ValueReason },
580 Mutate { value: Place, reason: Option<MutationReason> },
581 MutateConditionally { value: Place },
582 MutateTransitive { value: Place },
583 MutateTransitiveConditionally { value: Place },
584 Capture { from: Place, into: Place },
585 Alias { from: Place, into: Place },
586 MaybeAlias { from: Place, into: Place },
587 Assign { from: Place, into: Place },
588 Create { into: Place, value: ValueKind, reason: ValueReason },
589 CreateFrom { from: Place, into: Place },
590 ImmutableCapture { from: Place, into: Place },
591 Render { place: Place },
592
593 Apply {
594 receiver: Place,
595 function: Place,
596 mutates_function: bool,
597 args: Vec<PlaceOrSpreadOrHole>, // cloned from InstructionValue
598 into: Place,
599 signature: Option<FunctionSignature>,
600 loc: SourceLocation,
601 },
602 CreateFunction {
603 into: Place,
604 /// Index into function arena on Environment.
605 /// Used to access context variables, aliasing effects, etc.
606 function: FunctionId,
607 captures: Vec<Place>, // cloned from context, filtered
608 },
609
610 MutateFrozen { place: Place, error: CompilerDiagnostic },
611 MutateGlobal { place: Place, error: CompilerDiagnostic },
612 Impure { place: Place, error: CompilerDiagnostic },
613 }
614 ```
615
616 Key design decisions:
617 - **Place is cloned, not shared**: Since `Place` stores `IdentifierId` (a `Copy` type) + `Effect` + `bool` + `SourceLocation`, it is small enough to clone cheaply. No shared references needed.
618 - **`CreateFunction.function`** stores a `FunctionId` referencing the function arena on `Environment`. Code that needs `func.context` or `func.aliasingEffects` accesses `env.functions[function_id]` directly (see [Accessing Functions from CreateFunction](#accessing-functions-from-createfunction) below).
619 - **`Apply.args`** is a cloned `Vec`, not a shared reference to the InstructionValue's args. This is a shallow clone of `Place`/`SpreadPattern`/`Hole` values (all small, copyable types with arena IDs).
620
621 #### EffectId as Allocation-Site Identity
622
623 With PR #33650, the interned `AliasingEffect` replaces `InstructionValue` as the allocation-site key. In Rust, the `EffectId` (index into the interning table) serves directly as the allocation-site identity — no separate `AllocationSiteId` is needed:
624
625 ```rust
626 struct InferenceState {
627 /// The kind of each value, keyed by the EffectId of its creation effect
628 values: HashMap<EffectId, AbstractValue>,
629 /// The set of allocation sites pointed to by each identifier
630 variables: HashMap<IdentifierId, SmallVec<[EffectId; 2]>>,
631 }
632
633 impl InferenceState {
634 /// Initialize a value at the given allocation site
635 fn initialize(&mut self, effect_id: EffectId, kind: AbstractValue) {
636 self.values.insert(effect_id, kind);
637 }
638
639 /// Define a variable to point at an allocation site
640 fn define(&mut self, place: &Place, effect_id: EffectId) {
641 self.variables.insert(place.identifier, smallvec![effect_id]);
642 }
643
644 /// Look up which allocation sites a place points to
645 fn values(&self, place: &Place) -> &[EffectId] {
646 self.variables.get(&place.identifier).expect("uninitialized").as_slice()
647 }
648 }
649 ```
650
651 Each call to `state.initialize(effect, kind)` / `state.define(place, effect)` in TypeScript becomes `state.initialize(effect_id, kind)` / `state.define(place, effect_id)` in Rust, where `effect_id` is the `EffectId` returned by the effect interner. This applies uniformly to all creation effects:
652 - **`Create`/`CreateFrom`**: The interned effect's `EffectId` is both the interning key and the allocation-site key
653 - **`CreateFunction`**: Same — the interned CreateFunction effect's `EffectId` is the allocation-site key (the `FunctionExpression` reference is no longer used as a key)
654 - **Params/context**: Synthetic `AliasingEffect::Create` values are interned and their `EffectId` serves as the allocation site
655
656 The `effectInstructionValueCache` is eliminated entirely (PR #33650 removes it). The `functionSignatureCache: Map<FunctionExpression, AliasingSignature>` becomes `HashMap<FunctionId, AliasingSignature>` — keyed by the `FunctionId` rather than the FunctionExpression reference.
657
658 #### Effect Interning
659
660 ```rust
661 struct EffectInterner {
662 effects: Vec<AliasingEffect>, // indexed by EffectId
663 by_hash: HashMap<String, EffectId>, // dedup by content hash
664 }
665
666 #[derive(Copy, Clone, Hash, Eq, PartialEq)]
667 struct EffectId(u32);
668
669 impl EffectInterner {
670 fn intern(&mut self, effect: AliasingEffect) -> EffectId {
671 let hash = hash_effect(&effect);
672 *self.by_hash.entry(hash).or_insert_with(|| {
673 let id = EffectId(self.effects.len() as u32);
674 self.effects.push(effect);
675 id
676 })
677 }
678 }
679 ```
680
681 Since the interned effect IS the allocation-site key, there is no additional cache or mapping needed. The `EffectId` serves as interning dedup key, allocation-site identity, and cache key for `applySignatureCache`. The `functionSignatureCache` is keyed by `FunctionId`.
682
683 #### Accessing Functions from CreateFunction
684
685 In Rust, `CreateFunction` stores `function: FunctionId`, so the inner function is accessed directly from the function arena on `Environment`:
686
687 ```rust
688 // Read access:
689 let inner_func = &env.functions[effect.function];
690
691 // Mutable access:
692 let inner_func = &mut env.functions[effect.function];
693 ```
694
695 No instruction lookup or index is needed — the `FunctionId` provides direct O(1) access to the inner function's context variables, aliasing effects, and other data.
696
697 #### Context Variable Mutation
698
699 The mutation `operand.effect = Effect.Read` (in `applyEffect` for `CreateFunction`) modifies Places on the nested function's context. In Rust:
700
701 ```rust
702 // During CreateFunction processing, after determining abstract kinds:
703 let inner_func = &mut env.functions[effect.function];
704 for operand in &mut inner_func.context {
705 if operand.effect == Effect::Capture {
706 let kind = state.kind(operand).kind;
707 if matches!(kind, ValueKind::Primitive | ValueKind::Frozen | ValueKind::Global) {
708 operand.effect = Effect::Read;
709 }
710 }
711 }
712 ```
713
714 Since inner functions live in the function arena on `Environment` (not inline in the instruction), the borrow to `env.functions[function_id]` is completely disjoint from the outer `HIRFunction` being processed. No collect-then-apply workaround is needed.
715
716 ### Summary of Rust Approach for AliasingEffect
717
718 | TypeScript Pattern | Rust Equivalent | Complexity |
719 |---|---|---|
720 | Effect Places share InstructionValue Places | Clone Places (cheap with `IdentifierId`) | Trivial |
721 | `Apply.args` shares InstructionValue's args array | Clone the `Vec<PlaceOrSpreadOrHole>` | Trivial |
722 | `CreateFunction.function` = the FunctionExpression | Store `FunctionId`, direct arena access | Trivial |
723 | `InstructionValue` as allocation-site key (→ `AliasingEffect` after #33650) | `EffectId` from interning table | Trivial |
724 | `effectInstructionValueCache` (eliminated by #33650) | Not needed — `EffectId` is the allocation site directly | N/A |
725 | `functionSignatureCache` (FunctionExpr → Signature) | `HashMap<FunctionId, AliasingSignature>` | Trivial |
726 | Effect interning by content hash | `EffectInterner` with `Vec` + `HashMap` | Low |
727 | `operand.effect = Effect.Read` mutation | `&mut env.functions[function_id].context` — disjoint borrow | Trivial |
728 | `applySignatureCache` (Signature × Apply → Effects) | `HashMap<(EffectId, EffectId), Vec<AliasingEffect>>` | Low |
729 | `state.values(place)` returning `AliasingEffect[]` | Returns `&[EffectId]` | Trivial |
730
731 **Overall assessment**: AliasingEffect translates cleanly to Rust. With PR #33650, the interned `EffectId` serves as both the dedup key and allocation-site identity, eliminating the need for a separate `AllocationSiteId`. Place sharing is resolved by cloning (cheap with arena-based identifiers), and inner function access uses `FunctionId` into the function arena on `Environment`. No fundamental algorithmic redesign is needed. The fixpoint loop, effect interning, and abstract interpretation structure remain structurally identical.
732
733 ---
734
735 ## Recommended Rust Architecture
736
737 ### Arena-Based Identifier Storage
738
739 Stored as `identifiers: Vec<Identifier>` directly on `Environment`.
740
741 ```rust
742 #[derive(Copy, Clone, Hash, Eq, PartialEq)]
743 struct IdentifierId(u32);
744
745 #[derive(Clone)]
746 struct Place {
747 identifier: IdentifierId, // index into Environment.identifiers
748 effect: Effect,
749 reactive: bool,
750 loc: SourceLocation,
751 }
752
753 struct Identifier {
754 id: IdentifierId,
755 declaration_id: DeclarationId,
756 name: Option<IdentifierName>,
757 mutable_range: MutableRange,
758 scope: Option<ScopeId>,
759 ty: TypeId, // index into Environment.types
760 loc: SourceLocation,
761 }
762 ```
763
764 ### Arena-Based Scope Storage
765
766 Stored as `scopes: Vec<ReactiveScope>` directly on `Environment`.
767
768 ```rust
769 #[derive(Copy, Clone, Hash, Eq, PartialEq)]
770 struct ScopeId(u32);
771 ```
772
773 ### Arena-Based Function Storage
774
775 Stored as `functions: Vec<HIRFunction>` directly on `Environment`. `FunctionExpression` and `ObjectMethod` instruction values store a `FunctionId` instead of inline function data.
776
777 ```rust
778 #[derive(Copy, Clone, Hash, Eq, PartialEq)]
779 struct FunctionId(u32);
780 ```
781
782 ### Arena-Based Type Storage
783
784 Stored as `types: Vec<Type>` directly on `Environment`. `Identifier.ty` stores a `TypeId` instead of an inline `Type` value.
785
786 ```rust
787 #[derive(Copy, Clone, Hash, Eq, PartialEq)]
788 struct TypeId(u32);
789 ```
790
791 ### Instructions Table
792
793 Instructions are stored in a flat table on `HIRFunction` (`instructions: Vec<Instruction>`), indexed by `InstructionId`. `BasicBlock.instructions` becomes `Vec<InstructionId>`, referencing into this table. The existing `InstructionId` type is renamed to `EvaluationOrder` since it represents evaluation order and is present on both instructions and terminals.
794
795 ```rust
796 #[derive(Copy, Clone, Hash, Eq, PartialEq)]
797 struct InstructionId(u32);
798
799 #[derive(Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
800 struct EvaluationOrder(u32);
801 ```
802
803 This allows passes to cache or reference an instruction's location via a single copyable ID, avoiding `(BlockId, usize)` tuples.
804
805 ### CFG Representation
806
807 ```rust
808 /// Use IndexMap for insertion-order iteration (matching JS Map semantics)
809 struct HIR {
810 entry: BlockId,
811 blocks: IndexMap<BlockId, BasicBlock>,
812 }
813 ```
814
815 ### Pass Signature Patterns
816
817 Passes return `Result` for errors that would have thrown in TypeScript.
818
819 ```rust
820 /// Most passes: mutable HIR + mutable environment
821 fn enter_ssa(func: &mut HIRFunction, env: &mut Environment) -> Result<(), CompilerDiagnostic> { ... }
822
823 /// Validation passes
824 fn validate_hooks_usage(func: &HIRFunction, env: &mut Environment) -> Result<(), CompilerDiagnostic> { ... }
825
826 /// Passes that don't need env at all (many!)
827 fn merge_consecutive_blocks(func: &mut HIRFunction) { ... }
828 fn constant_propagation(func: &mut HIRFunction) { ... }
829 ```
830
831 ### Key Rust Patterns for Common TypeScript Idioms
832
833 #### Pattern A: InstructionValue Variant Swap (`std::mem::replace`)
834 ```rust
835 // TypeScript: instr.value = { kind: 'CallExpression', callee: instr.value.property, ... }
836 // Rust: take ownership, destructure, construct new variant
837 let old = std::mem::replace(&mut instr.value, InstructionValue::Tombstone);
838 if let InstructionValue::MethodCall { property, args, loc, .. } = old {
839 instr.value = InstructionValue::CallExpression { callee: property, args, loc };
840 } else {
841 instr.value = old;
842 }
843 ```
844
845 #### Pattern B: Place Cloning via Spread (`{...place}`)
846 ```rust
847 // TypeScript: const newPlace = { ...place, effect: Effect.Read }
848 // Rust: Place is Clone (or Copy if small enough)
849 let new_place = Place { effect: Effect::Read, ..place.clone() };
850 ```
851
852 #### Pattern C: Delete-During-Set-Iteration (`retain`)
853 ```rust
854 // TypeScript: for (const phi of block.phis) { if (dead) block.phis.delete(phi); }
855 // Rust: retain is the idiomatic equivalent
856 block.phis.retain(|phi| !is_dead(phi));
857 ```
858
859 #### Pattern D: Map Iteration with Block Deletion
860 ```rust
861 // TypeScript: for (const [, block] of fn.body.blocks) { fn.body.blocks.delete(id); }
862 // Rust: collect keys first, then remove + get_mut
863 let block_ids: Vec<BlockId> = blocks.keys().copied().collect();
864 for block_id in block_ids {
865 if should_merge(block_id) {
866 let removed = blocks.remove(&block_id).unwrap();
867 let pred = blocks.get_mut(&pred_id).unwrap();
868 pred.instructions.extend(removed.instructions);
869 }
870 }
871 ```
872
873 #### Pattern E: Closure Variables Set Inside Builder Callbacks
874 ```rust
875 // TypeScript: let callee = null; builder.enter(() => { callee = ...; return terminal; });
876 // Rust: closure returns the value, or use Option<T> initialized before
877 let (block_id, callee) = builder.enter(|b| {
878 let callee = /* compute */;
879 let terminal = /* build */;
880 (terminal, callee) // return both
881 });
882 ```
883
884 ---
885
886 ## Input/Output Format
887
888 Define a Rust representation of the Babel AST format using serde with custom serialization/deserialization in order to ensure that the `"type"` field is always produced, even outside of enum positions. Include full information from Babel, including source locations. Define a `Scope` type that encodes the tree of scope information, mapping to the information that Babel represents in its own scope tree.
889
890 The main public API is roughly:
891
892 ```rust
893 /// Returns None if the function doesn't need changes, Some with the compiled output otherwise.
894 fn compile(ast: BabelAst, scope: Scope) -> Option<BabelAst>
895 ```
896
897 This replaces the current Babel-plugin integration pattern where the compiler receives NodePath objects. The JSON AST interchange decouples the Rust compiler from any specific JS parser or AST format at the implementation level while maintaining Babel compatibility at the serialization boundary.
898
899 ---
900
901 ## Error Handling
902
903 In general there are two categories of errors:
904 - Anything that would have thrown, or would have short-circuited, should return an `Err(...)` with the single diagnostic
905 - Otherwise, accumulate errors directly onto the environment
906 - Error handling must preserve the full details of the errors: reason, description, location, details, suggestions, category, etc
907
908 ### Specific Error Patterns and Approaches
909
910 | TypeScript Pattern | Example | Rust Approach |
911 |---|---|---|
912 | Non-null assertions (`!`) | `value!.field` | Panic via `.unwrap()` or similar |
913 | Throwing expressions | `throw ...`, `CompilerError.invariant()`, `CompilerError.throwTodo()`, `CompilerError.throw*()` | Make the function return `Result<_, CompilerDiagnostic>`, return `Err(...)` |
914 | Non-throwing (invariant) | Local `error` + `error.pushDiagnostic()` where the error IS an invariant | Make the function return `Result<_, CompilerDiagnostic>`, change `pushDiagnostic()` to `return Err(...)` |
915 | Non-throwing (non-invariant) | Local `error` + `error.pushDiagnostic()`, `env.recordError()` | Keep as-is — accumulate on environment |
916
917 ### Pass and Pipeline Structure
918
919 ```rust
920 // pipeline.rs
921 fn compile(
922 ast: Ast,
923 scope: Scope,
924 env: &mut Environment,
925 ) -> Result<CompileResult, CompilerDiagnostic> {
926 // "?" to handle cases that would have thrown or produced an invariant
927 let mut hir = lower(ast, scope, env)?;
928 some_compiler_pass(&mut hir, env)?;
929 // ...
930 let ast = codegen(...)?;
931
932 if env.has_errors() {
933 Ok(CompileResult::Failure(env.take_errors()))
934 } else {
935 Ok(CompileResult::Success(ast))
936 }
937 }
938
939 // <compiler_pass>.rs
940 fn pass_name(
941 func: &mut HirFunction,
942 env: &mut Environment,
943 ) -> Result<(), CompilerDiagnostic>;
944 ```
945
946 ---
947
948 ## Structural Similarity: TypeScript ↔ Rust Alignment
949
950 ### Design Goal
951
952 The Rust code should be visually and structurally aligned with the original TypeScript. A developer should be able to have the TypeScript on the left side of the screen and the Rust on the right, scroll them together, and easily see how the logic corresponds.
953
954 ### What Looks Nearly Identical (~95% match)
955
956 Most passes consist of these patterns that translate almost line-for-line:
957
958 | TypeScript Pattern | Rust Equivalent |
959 |---|---|
960 | `switch (value.kind) { case 'X': ... }` | `match &value { InstructionValue::X { .. } => ... }` |
961 | `for (const [, block] of fn.body.blocks)` | `for block in func.body.blocks.values()` |
962 | `for (const instr of block.instructions)` | `for instr in &block.instructions` |
963 | `const map = new Map<K, V>()` | `let mut map: HashMap<K, V> = HashMap::new()` |
964 | `map.get(key) ?? defaultValue` | `map.get(&key).copied().unwrap_or(default)` |
965 | `if (x === null) { ... }` | `if x.is_none() { ... }` or `let Some(x) = x else { ... }` |
966 | `CompilerError.invariant(cond, ...)` | `assert!(cond, "...")` or `panic!("...")` |
967 | `do { ... } while (changed)` | `loop { ... if !changed { break; } }` |
968 | `array.push(item)` | `vec.push(item)` |
969 | `set.has(item)` | `set.contains(&item)` |
970
971 ### What Looks Slightly Different (~80% match)
972
973 | TypeScript Pattern | Rust Equivalent | Reason |
974 |---|---|---|
975 | `Map<Identifier, T>` (reference keys) | `HashMap<IdentifierId, T>` | Reference identity → value identity |
976 | `DisjointSet<ReactiveScope>` | `DisjointSet<ScopeId>` | Same reason |
977 | `place.identifier.mutableRange.end = x` | `env.identifiers[place.identifier].mutable_range.end = x` | Arena indirection |
978 | `identifier.scope = sharedScope` | `identifier.scope = Some(scope_id)` | Reference → ID |
979 | `for...of` with `Set.delete()` | `set.retain(|x| ...)` | Different idiom, same semantics |
980 | `instr.value = { kind: 'X', ... }` | `instr.value = InstructionValue::X { ... }` (with `mem::replace`) | Ownership swap |
981
982 ### What Looks Substantially Different (~60% match)
983
984 | TypeScript Pattern | Rust Equivalent | Reason |
985 |---|---|---|
986 | Storing `&Instruction` in side map | Store `InstructionId`, access via instruction table | Cannot hold references during mutation |
987 | Builder closures capturing outer `&mut` | Return values from closures, or split borrows | Borrow checker |
988 | `node.id.mutableRange.end = x` (graph node → HIR mutation) | Collect updates, apply to `env.identifiers` after traversal | Cannot mutate HIR through graph references |
989 | `identifier.mutableRange = scope.range` (shared object aliasing) | `identifier.scope = Some(scope_id)` + lookup via arena | Fundamental ownership model difference |
990
991 ### Passes Ranked by Structural Similarity to Rust
992
993 **Nearly identical (95%+)**: PruneMaybeThrows, OptimizePropsMethodCalls, FlattenReactiveLoopsHIR, FlattenScopesWithHooksOrUseHIR, MergeConsecutiveBlocks, DeadCodeElimination, PruneUnusedLabelsHIR, RewriteInstructionKindsBasedOnReassignment, EliminateRedundantPhi, all validation passes, PruneUnusedLabels, PruneUnusedScopes, PruneNonReactiveDependencies, PruneAlwaysInvalidatingScopes, StabilizeBlockIds, PruneHoistedContexts
994
995 **Very similar (85-95%)**: ConstantPropagation, EnterSSA, InferTypes, InferReactivePlaces, DropManualMemoization, InlineIIFEs, MemoizeFbtAndMacroOperandsInSameScope, AlignMethodCallScopes, AlignObjectMethodScopes, OutlineFunctions, NameAnonymousFunctions, BuildReactiveScopeTerminalsHIR, PropagateScopeDependenciesHIR, PropagateEarlyReturns, MergeReactiveScopesThatInvalidateTogether, PromoteUsedTemporaries, RenameVariables, ExtractScopeDeclarationsFromDestructuring
996
997 **Moderately similar (70-85%)**: AnalyseFunctions, InferReactiveScopeVariables, AlignReactiveScopesToBlockScopesHIR, MergeOverlappingReactiveScopesHIR, OutlineJSX, BuildReactiveFunction, PruneNonEscapingScopes, OptimizeForSSR, PruneUnusedLValues
998
999 **Moderately similar (70-85%)** *(additional)*: InferMutationAliasingEffects (after [PR #33650](https://github.com/facebook/react/pull/33650): allocation-site keys → `EffectId` via interning, Place sharing → Clone, CreateFunction → FunctionId arena access — see [§AliasingEffect section](#aliasingeffect-shared-references-and-rust-ownership))
1000
1001 **Requires redesign (50-70%)**: InferMutationAliasingRanges (graph-through-HIR mutation), BuildHIR (Babel AST coupling), CodegenReactiveFunction (Babel AST output)
1002
1003 ---
1004
1005 ## Pipeline Overview
1006
1007 ```
1008 Babel AST
1009
1010
1011 ┌─────────────────────────────────────────────┐
1012 │ Phase 1: Lowering │
1013 │ BuildHIR (lower) │
1014 └─────────────────────────────────────────────┘
1015
1016
1017 ┌─────────────────────────────────────────────┐
1018 │ Phase 2-3: Normalization + SSA │
1019 │ PruneMaybeThrows │
1020 │ DropManualMemoization │
1021 │ InlineIIFEs │
1022 │ MergeConsecutiveBlocks │
1023 │ EnterSSA │
1024 │ EliminateRedundantPhi │
1025 └─────────────────────────────────────────────┘
1026
1027
1028 ┌─────────────────────────────────────────────┐
1029 │ Phase 4-5: Optimization + Type Inference │
1030 │ ConstantPropagation │
1031 │ InferTypes │
1032 │ OptimizePropsMethodCalls │
1033 └─────────────────────────────────────────────┘
1034
1035
1036 ┌─────────────────────────────────────────────┐
1037 │ Phase 6: Mutation/Aliasing Analysis │
1038 │ AnalyseFunctions │
1039 │ InferMutationAliasingEffects │
1040 │ DeadCodeElimination │
1041 │ InferMutationAliasingRanges │
1042 └─────────────────────────────────────────────┘
1043
1044
1045 ┌─────────────────────────────────────────────┐
1046 │ Phase 7-8: Post-Inference + Reactivity │
1047 │ InferReactivePlaces │
1048 │ RewriteInstructionKindsBasedOnReassignment│
1049 └─────────────────────────────────────────────┘
1050
1051
1052 ┌─────────────────────────────────────────────┐
1053 │ Phase 9-12: Scope Construction + Alignment │
1054 │ InferReactiveScopeVariables │
1055 │ MemoizeFbtAndMacroOperandsInSameScope │
1056 │ OutlineJSX / OutlineFunctions │
1057 │ AlignMethodCallScopes │
1058 │ AlignObjectMethodScopes │
1059 │ AlignReactiveScopesToBlockScopesHIR │
1060 │ MergeOverlappingReactiveScopesHIR │
1061 │ BuildReactiveScopeTerminalsHIR │
1062 │ FlattenReactiveLoopsHIR │
1063 │ FlattenScopesWithHooksOrUseHIR │
1064 │ PropagateScopeDependenciesHIR │
1065 └─────────────────────────────────────────────┘
1066
1067
1068 ┌─────────────────────────────────────────────┐
1069 │ Phase 13-14: Reactive Function │
1070 │ BuildReactiveFunction (CFG → tree) │
1071 │ PruneUnusedLabels │
1072 │ PruneNonEscapingScopes │
1073 │ PruneNonReactiveDependencies │
1074 │ PruneUnusedScopes │
1075 │ MergeReactiveScopesThatInvalidateTogether │
1076 │ PruneAlwaysInvalidatingScopes │
1077 │ PropagateEarlyReturns │
1078 │ PruneUnusedLValues │
1079 │ PromoteUsedTemporaries │
1080 │ ExtractScopeDeclarationsFromDestructuring │
1081 │ StabilizeBlockIds │
1082 │ RenameVariables │
1083 │ PruneHoistedContexts │
1084 └─────────────────────────────────────────────┘
1085
1086
1087 ┌─────────────────────────────────────────────┐
1088 │ Phase 15: Codegen │
1089 │ CodegenReactiveFunction (tree → Babel AST)│
1090 └─────────────────────────────────────────────┘
1091
1092
1093 Babel AST (with memoization)
1094 ```
1095
1096 ---
1097
1098 ## Pass-by-Pass Analysis
1099
1100 ### Phase 1: Lowering
1101
1102 #### BuildHIR (`lower`)
1103 **What it does**: Converts Babel AST to HIR by traversing the AST and building a control-flow graph with BasicBlocks, Instructions, and Terminals.
1104
1105 **Environment usage**: Heavy. Uses `env.nextIdentifierId`, `env.nextBlockId` for all ID allocation. Uses `env.recordError()` for fault-tolerant error handling. Uses `env.parentFunction.scope` for Babel scope analysis. Uses `env.isContextIdentifier()` and `env.programContext`. Environment is shared with nested function lowering via recursive `lower()` calls.
1106
1107 **Side maps**:
1108 - `#bindings: Map<string, {node, identifier}>` — caches Identifier objects by name, using Babel node reference equality to distinguish same-named variables in different scopes
1109 - `#context: Map<t.Identifier, SourceLocation>` — Babel node keys (reference identity)
1110 - `#completed: Map<BlockId, BasicBlock>` — ID-keyed (safe)
1111 - `followups: Array<{place, path}>` — temporary Place storage during destructuring
1112
1113 **Structural similarity**: ~65%. The HIRBuilder class maps to a Rust struct with `&mut self` methods. The `enter()/loop()/label()` closure patterns translate to methods taking `impl FnOnce(&mut Self) -> Terminal`. However, several patterns require restructuring:
1114 - Variables assigned inside closures and read outside (e.g., `let callee = null; builder.enter(() => { callee = ...; })`) must return values from the closure instead
1115 - `resolveBinding()` uses Babel node reference equality (`mapping.node === node`) — needs parser-specific node IDs
1116 - Recursive `lower()` for nested functions needs `std::mem::take` to extract child function data
1117 - The Babel AST input arrives as JSON (deserialized via serde), replacing direct Babel NodePath traversal
1118
1119 **Unexpected issues**: Babel bug workarounds (lines 413-418, 4488-4498) would not be needed with a different parser. The `promoteTemporary()` pattern is straightforward in Rust. The `fbtDepth` counter is trivial.
1120
1121 ---
1122
1123 ### Phase 2: Normalization
1124
1125 #### PruneMaybeThrows
1126 **Env usage**: None. **Side maps**: `Map<BlockId, BlockId>` (IDs only). **Similarity**: ~95%.
1127 Simple terminal mutation (`handler = null`), phi rewiring, and CFG cleanup. The phi operand mutation-during-iteration needs `drain().collect()` in Rust. Block iteration order must be RPO for chain resolution.
1128
1129 #### DropManualMemoization
1130 **Env usage**: `getGlobalDeclaration`, `getHookKindForType`, `recordError`, `createTemporaryPlace`, config flags. **Side maps**: `IdentifierSidemap` with 6 collections — `functions` stores `TInstruction` references (use `HashSet<IdentifierId>` instead), `manualMemos.loadInstr` stores instruction reference (store `InstructionId` instead), others are ID-keyed. **Similarity**: ~85%.
1131 Two-phase collect+rewrite. In Rust, the `functions` map needs only existence checking (not the actual instruction reference). `manualMemos.loadInstr` only needs `.id` — store the ID directly.
1132
1133 #### InlineImmediatelyInvokedFunctionExpressions
1134 **Env usage**: `env.nextBlockId`, `env.nextIdentifierId` (via `createTemporaryPlace`). **Side maps**: `functions: Map<IdentifierId, FunctionExpression>` stores instruction value references. **Similarity**: ~80%.
1135 The `functions` map stores `FunctionExpression` references — in Rust, store `FunctionId` for the inner function. The queue-while-iterating pattern needs index-based loop (`while i < queue.len()`). Block ownership transfer uses `blocks.remove()` + `blocks.insert()`.
1136
1137 #### MergeConsecutiveBlocks
1138 **Env usage**: None. **Side maps**: `MergedBlocks` (ID-only map), `fallthroughBlocks` (ID-only set). **Similarity**: ~90%.
1139 Main Rust challenge: iteration + deletion. Collect block IDs first, then `remove()` + `get_mut()`. Phi operand rewriting needs collect-then-apply.
1140
1141 ---
1142
1143 ### Phase 3: SSA Construction
1144
1145 #### EnterSSA
1146 **Env usage**: `env.nextIdentifierId` for fresh SSA identifiers. **Side maps**: `#states: Map<BasicBlock, State>` with `defs: Map<Identifier, Identifier>` (both reference-identity keyed), `unsealedPreds: Map<BasicBlock, number>`, `#unknown/#context: Set<Identifier>`. **Similarity**: ~85%.
1147 All reference-identity maps become ID-keyed: `Vec<State>` indexed by BlockId, `HashMap<IdentifierId, IdentifierId>` for defs. The recursive `getIdAt()` works cleanly because `IdentifierId` is `Copy` — no borrows held across recursive calls. The `enter()` closure for nested functions is just save/restore of `self.current`. `makeType()` global counter must become per-compilation.
1148
1149 #### EliminateRedundantPhi
1150 **Env usage**: None. **Side maps**: `rewrites: Map<Identifier, Identifier>` (reference keys). **Similarity**: ~95%.
1151 Becomes `HashMap<IdentifierId, IdentifierId>`. `rewritePlace` becomes `place.identifier_id = new_id`. Phi deletion during iteration becomes `block.phis.retain(|phi| ...)`. The fixpoint loop and labeled `continue` translate directly.
1152
1153 ---
1154
1155 ### Phase 4: Optimization (Pre-Inference)
1156
1157 #### ConstantPropagation
1158 **Env usage**: None. **Side maps**: `constants: Map<IdentifierId, Constant>` (ID-keyed, safe). **Similarity**: ~90%.
1159 The fixpoint loop, `evaluateInstruction()` switch, and terminal rewriting all map directly. Constants map stores cloned `Primitive`/`LoadGlobal` values (small, cheap to clone). The CFG cleanup cascade after branch elimination needs shared infrastructure. The `block.kind === 'sequence'` guard translates to an enum check.
1160
1161 #### OptimizePropsMethodCalls
1162 **Env usage**: None. **Side maps**: None. **Similarity**: ~98%.
1163 The simplest pass in the compiler. A single linear scan with one `match` arm and `std::mem::replace` for the value swap. ~20 lines of Rust.
1164
1165 ---
1166
1167 ### Phase 5: Type and Effect Inference
1168
1169 #### InferTypes
1170 **Env usage**: `getGlobalDeclaration`, `getPropertyType`, `getFallthroughPropertyType`, config flags. **Side maps**: `Unifier.substitutions: Map<TypeId, Type>` (ID-keyed), `names: Map<IdentifierId, string>` (ID-keyed). **Similarity**: ~90%.
1171 Unification-based type inference is very natural in Rust. The `Type` enum needs `Box<Type>` for recursive variants (`Function.return`, `Property.objectType`). The TypeScript generator pattern for constraint generation can be replaced with direct `unifier.unify()` calls during the walk. The `apply()` phase is straightforward mutable traversal. `makeType()` global counter needs per-compilation scope.
1172
1173 ---
1174
1175 ### Phase 6: Mutation/Aliasing Analysis
1176
1177 #### AnalyseFunctions
1178 **Env usage**: Shares Environment between parent and child via `fn.env`. Uses logger. **Side maps**: None (operates entirely through in-place HIR mutation). **Similarity**: ~85%.
1179 The recursive `lowerWithMutationAliasing` pattern works with `&mut` because it is sequential. Inner functions are stored in the function arena on `Environment` and accessed via `FunctionId`, so no extraction/replacement is needed. The mutableRange reset (`identifier.mutableRange = {start: 0, end: 0}`) is a simple value write in Rust (no aliasing to break because Rust uses values, not shared objects).
1180
1181 #### InferMutationAliasingEffects
1182 **Env usage**: `env.config` (3 reads), `env.getFunctionSignature`, `env.enableValidations`, `createTemporaryPlace`. InferenceState stores `env` as read-only reference. **Side maps**: `statesByBlock/queuedStates` (BlockId-keyed), Context class with caches (`Map<Instruction, InstructionSignature>`, `Map<FunctionExpression, AliasingSignature>`, `Map<AliasingSignature, Map<AliasingEffect, ...>>`), InferenceState with `#values: Map<InstructionValue, AbstractValue>` and `#variables: Map<IdentifierId, Set<InstructionValue>>`. **Similarity**: ~80%.
1183
1184 **Shared references in AliasingEffect** (see [§AliasingEffect: Shared References and Rust Ownership](#aliasingeffect-shared-references-and-rust-ownership) for full analysis): `computeSignatureForInstruction` creates effects that share Place objects with the Instruction's `lvalue` and `InstructionValue` fields. The `Apply` effect shares the args array reference. The `CreateFunction` effect stores the actual `FunctionExpression`/`ObjectMethod` InstructionValue. In Rust, Places are cloned (cheap with `IdentifierId`) and `CreateFunction` stores a `FunctionId` for function arena access.
1185
1186 **Allocation-site identity**: Currently uses `InstructionValue` as reference-identity keys. PR [#33650](https://github.com/facebook/react/pull/33650) replaces this with interned `AliasingEffect` objects — since effects are already interned by content hash, the interned effect IS the allocation-site key. In Rust, this maps to `EffectId` (index into the interning table). No separate `AllocationSiteId` is needed.
1187
1188 **Reference-identity maps and their Rust equivalents** (after PR #33650):
1189 - `instructionSignatureCache: Map<Instruction, ...>``HashMap<InstructionId, InstructionSignature>`
1190 - `#values: Map<AliasingEffect, AbstractValue>``HashMap<EffectId, AbstractValue>` (EffectId = interning index = allocation-site ID)
1191 - `#variables: Map<IdentifierId, Set<AliasingEffect>>``HashMap<IdentifierId, SmallVec<[EffectId; 2]>>`
1192 - `effectInstructionValueCache` → eliminated by PR #33650
1193 - `functionSignatureCache: Map<FunctionExpression, ...>``HashMap<FunctionId, AliasingSignature>` (key by FunctionId from arena)
1194 - `applySignatureCache: Map<AliasingSignature, Map<AliasingEffect, ...>>``HashMap<EffectId, HashMap<EffectId, ...>>`
1195 - `internedEffects: Map<string, AliasingEffect>``EffectInterner { effects: Vec<AliasingEffect>, by_hash: HashMap<String, EffectId> }`
1196
1197 All keys become `Copy` types (`InstructionId`, `EffectId`, `IdentifierId`), trivially `Hash + Eq`, with no reference identity needed.
1198
1199 The overall structure (fixpoint loop, InferenceState clone/merge, applyEffect recursion, Context caching) can remain nearly identical. The `applyEffect` recursive method works with `&mut InferenceState` + `&mut Context` parameters — Rust's reborrowing handles the recursion naturally.
1200
1201 **Context variable mutation**: During `CreateFunction` processing, `operand.effect = Effect.Read` mutates Places on the nested function's context. In Rust, the inner function is accessed via `&mut env.functions[function_id]`, which is completely disjoint from the outer `HIRFunction` being processed.
1202
1203 #### DeadCodeElimination
1204 **Env usage**: `env.outputMode` (one read for SSR hook pruning). **Side maps**: `State.identifiers: Set<IdentifierId>`, `State.named: Set<string>` (both value-keyed, safe). **Similarity**: ~95%.
1205 Two-phase mark-and-sweep is perfectly natural in Rust. `Vec::retain` replaces `retainWhere`. Destructuring pattern rewrites use `iter_mut()` + `truncate()`.
1206
1207 #### InferMutationAliasingRanges (HIGH COMPLEXITY)
1208 **Env usage**: `env.enableValidations` (one read), `env.recordError` (error recording). **Side maps**: `AliasingState.nodes: Map<Identifier, Node>` (reference-identity keys), each Node containing `createdFrom/captures/aliases/maybeAliases: Map<Identifier, number>` and `edges: Array<{node: Identifier, ...}>`. Also `mutations/renders` arrays storing Place references. **Similarity**: ~75%.
1209
1210 **Effect consumption**: Iterates `instr.effects` for every instruction, reading Place fields (`effect.into`, `effect.from`, `effect.value`, `effect.place`). For `CreateFunction` effects, accesses `effect.function.loweredFunc.func` to create Function graph nodes. In Rust, `CreateFunction` stores `FunctionId`; the function is accessed via `env.functions[function_id]` (see [§AliasingEffect section](#aliasingeffect-shared-references-and-rust-ownership)). All other effect Place accesses only need `place.identifier` (an `IdentifierId` in Rust), with no shared reference concerns.
1211
1212 **All Identifier-keyed maps become `HashMap<IdentifierId, T>`**. The critical `node.id.mutableRange.end = ...` pattern (mutating HIR through graph node references) needs restructuring: either store computed range updates on the Node and apply after traversal (recommended), or use arena-based identifiers. The BFS in `mutate()` collects edge targets into temporary `Vec<IdentifierId>` before pushing to queue, resolving borrow conflicts. The two-part structure (build graph → apply ranges) maps well to Rust's two-phase pattern. The temporal `index` counter and edge ordering translate directly.
1213
1214 **Potential latent issue**: The `edges` array uses `break` (line 763) assuming monotonic insertion order, but pending phi edges from back-edges could break this ordering. The Rust port should consider using `continue` instead of `break` for safety.
1215
1216 ---
1217
1218 ### Phase 7: Optimization (Post-Inference)
1219
1220 #### OptimizeForSSR
1221 **Env usage**: None directly (conditional on pipeline `outputMode` check). **Side maps**: `inlinedState: Map<IdentifierId, InstructionValue>` (ID-keyed). **Similarity**: ~90%.
1222 Stores cloned InstructionValue objects. The two-pass pattern translates directly.
1223
1224 ---
1225
1226 ### Phase 8: Reactivity Inference
1227
1228 #### InferReactivePlaces
1229 **Env usage**: `getHookKind(fn.env, ...)` for hook detection. **Side maps**: `ReactivityMap.reactive: Set<IdentifierId>` (safe), `ReactivityMap.aliasedIdentifiers: DisjointSet<Identifier>` (reference-identity), `StableSidemap.map: Map<IdentifierId, {isStable}>` (ID-keyed). **Similarity**: ~85%.
1230 DisjointSet becomes `DisjointSet<IdentifierId>`. The `isReactive()` side-effect pattern (sets `place.reactive = true` during reads) works in Rust as `fn is_reactive(&self, place: &mut Place) -> bool` — the ReactivityMap holds only IDs while `place` is mutably borrowed from the HIR, so borrows are disjoint. The fixpoint loop translates directly.
1231
1232 #### RewriteInstructionKindsBasedOnReassignment
1233 **Env usage**: None. **Side maps**: `declarations: Map<DeclarationId, LValue | LValuePattern>` stores references to lvalue objects for retroactive `.kind` mutation. **Similarity**: ~85%.
1234 The aliased-mutation-through-map pattern is best handled with a two-pass approach: Pass 1 collects `HashSet<DeclarationId>` of reassigned variables, Pass 2 assigns `InstructionKind` values. Or use `HashMap<DeclarationId, InstructionKind>` and apply in a final pass.
1235
1236 ---
1237
1238 ### Phase 9: Scope Construction
1239
1240 #### InferReactiveScopeVariables
1241 **Env usage**: `env.nextScopeId`, `env.config.enableForest`, `env.logger`. **Side maps**: `scopeIdentifiers: DisjointSet<Identifier>` (reference-identity), `declarations: Map<DeclarationId, Identifier>` (stores Identifier references), `scopes: Map<Identifier, ReactiveScope>` (reference keys). **Similarity**: ~75%.
1242
1243 **THE CRITICAL ALIASING PASS**: Line 132 `identifier.mutableRange = scope.range` creates the shared-MutableRange aliasing that all downstream scope passes depend on. In Rust with arenas: identifiers store `scope: Option<ScopeId>`. The "effective mutable range" is accessed via scope lookup. All downstream passes that read `mutableRange` access the scope arena via `env.scopes`. DisjointSet becomes `DisjointSet<IdentifierId>`, scopes map becomes `HashMap<IdentifierId, ScopeId>`.
1244
1245 #### MemoizeFbtAndMacroOperandsInSameScope
1246 **Env usage**: `fn.env.config.customMacros` (one read). **Side maps**: `macroKinds: Map<string, MacroDefinition>` (string keys), `macroTags: Map<IdentifierId, MacroDefinition>` (ID keys), `macroValues: Set<IdentifierId>` (IDs). **Similarity**: ~90%.
1247 All ID-keyed. The scope mutation (`operand.identifier.scope = scope`, `expandFbtScopeRange`) becomes `identifier.scope = Some(scope_id)` + `env.scopes[scope_id].range.start = min(...)`. The cyclic `MacroDefinition` structure can use arena indices or hardcoded match logic.
1248
1249 ---
1250
1251 ### Phase 10: Scope Alignment and Merging
1252
1253 #### AlignMethodCallScopes
1254 **Env usage**: None. **Side maps**: `scopeMapping: Map<IdentifierId, ReactiveScope | null>` (ID keys), `mergedScopes: DisjointSet<ReactiveScope>` (reference-identity). **Similarity**: ~90%.
1255 DisjointSet becomes `DisjointSet<ScopeId>`. Range merging through arena: `env.scopes[root_id].range.start = min(...)`. Scope rewriting: `identifier.scope = Some(root_id)`.
1256
1257 #### AlignObjectMethodScopes
1258 **Env usage**: None. **Side maps**: `objectMethodDecls: Set<Identifier>` (reference-identity), `DisjointSet<ReactiveScope>`. **Similarity**: ~88%.
1259 Same patterns as AlignMethodCallScopes. `Set<Identifier>` becomes `HashSet<IdentifierId>`. **Porting hazard**: The lvalue-only scope repointing (Phase 2b) relies on shared Identifier references. With arena-based identifiers where each Place has its own copy, repointing must cover ALL occurrences, not just lvalues. If using a central identifier arena (recommended), lvalue-only repointing is fine.
1260
1261 #### AlignReactiveScopesToBlockScopesHIR
1262 **Env usage**: None. **Side maps**: `activeScopes: Set<ReactiveScope>` (reference-identity, iterated while mutating `scope.range`), `seen: Set<ReactiveScope>`, `placeScopes: Map<Place, ReactiveScope>` (**dead code — never read**), `valueBlockNodes: Map<BlockId, ValueBlockNode>`. **Similarity**: ~85%.
1263 `activeScopes` becomes `HashSet<ScopeId>`. Scope mutation through arena: `for &scope_id in &active_scopes { env.scopes[scope_id].range.start = min(...); }` — perfectly clean borrows (HashSet is immutable, arena is mutable). The `placeScopes` map can be omitted entirely.
1264
1265 #### MergeOverlappingReactiveScopesHIR
1266 **Env usage**: None. **Side maps**: `joinedScopes: DisjointSet<ReactiveScope>` (reference-identity), `placeScopes: Map<Place, ReactiveScope>` (Place reference keys). **Similarity**: ~85%.
1267 DisjointSet becomes `DisjointSet<ScopeId>`. Same arena-based range merging pattern. Place-keyed maps become unnecessary with identifier-arena approach.
1268
1269 ---
1270
1271 ### Phase 11: Scope Terminal Construction
1272
1273 #### BuildReactiveScopeTerminalsHIR
1274 **Env usage**: None. **Side maps**: `rewrittenFinalBlocks: Map<BlockId, BlockId>` (IDs), `nextBlocks: Map<BlockId, BasicBlock>` (block storage), `queuedRewrites`. **Similarity**: ~85%.
1275 Complete blocks map replacement (`fn.body.blocks = nextBlocks`). Block splitting creates new blocks from instruction slices. Phi rewriting across old/new blocks. All structurally translatable.
1276
1277 #### FlattenReactiveLoopsHIR
1278 **Env usage**: None. **Side maps**: `activeLoops: Array<BlockId>` (IDs only). **Similarity**: ~98%.
1279 Simple terminal variant replacement (`scope``pruned-scope`). Uses `Vec::retain` for the active loops stack. ~40 lines of Rust logic. The terminal swap uses `std::mem::replace` or shared inner data struct.
1280
1281 #### FlattenScopesWithHooksOrUseHIR
1282 **Env usage**: `getHookKind(fn.env, ...)` (one hook resolution call). **Side maps**: `activeScopes: Array<{block, fallthrough}>`, `prune: Array<BlockId>` (both ID-only). **Similarity**: ~95%.
1283 Two-phase detect/rewrite. Stack-based scope tracking with `Vec::retain`. Terminal variant conversion. Very clean Rust translation.
1284
1285 ---
1286
1287 ### Phase 12: Scope Dependency Propagation
1288
1289 #### PropagateScopeDependenciesHIR
1290 **Env usage**: None directly. **Side maps**: `temporaries: Map<IdentifierId, ReactiveScopeDependency>` (ID-keyed, but `ReactiveScopeDependency` contains `identifier: Identifier` reference), `DependencyCollectionContext` with `#declarations: Map<DeclarationId, Decl>`, `#reassignments: Map<Identifier, Decl>` (reference keys), `deps: Map<ReactiveScope, Array<...>>` (reference keys). **Similarity**: ~80%.
1291 Reference-keyed maps become ID-keyed. `deps` becomes `HashMap<ScopeId, Vec<ReactiveScopeDependency>>`. The PropertyPathRegistry tree with parent pointers needs arena allocation. Scope mutation (`scope.declarations.set(...)`, `scope.dependencies.add(...)`) through arena.
1292
1293 ---
1294
1295 ### Phase 13: Reactive Function Construction
1296
1297 #### BuildReactiveFunction
1298 **Env usage**: Copies `fn.env` to reactive function. **Side maps**: Scheduling/traversal state during CFG-to-tree conversion. **Similarity**: ~80%.
1299 Major structural transformation (CFG → tree). The builder pattern works with `&mut` state. Deep recursion for value blocks is bounded by CFG depth. Shared Places/scopes/identifiers use arena indices in the new tree structure.
1300
1301 ---
1302
1303 ### Phase 14: Reactive Function Transforms
1304
1305 All reactive function transforms use the `ReactiveFunctionVisitor` / `ReactiveFunctionTransform` pattern.
1306
1307 **ReactiveFunctionVisitor/Transform pattern → Rust traits**:
1308 ```rust
1309 trait ReactiveFunctionTransform {
1310 type State;
1311 fn transform_terminal(&mut self, stmt: &mut ReactiveTerminalStatement, state: &mut Self::State)
1312 -> Transformed<ReactiveStatement> { Transformed::Keep }
1313 fn transform_instruction(&mut self, stmt: &mut ReactiveInstructionStatement, state: &mut Self::State)
1314 -> Transformed<ReactiveStatement> { Transformed::Keep }
1315 // ... default implementations for traversal ...
1316 }
1317
1318 enum Transformed<T> {
1319 Keep,
1320 Remove,
1321 Replace(T),
1322 ReplaceMany(Vec<T>),
1323 }
1324 ```
1325
1326 The `traverseBlock` method handles `ReplaceMany` by lazily building a new `Vec` (only allocating on first mutation). This maps to Rust's `Option<Vec<T>>` pattern.
1327
1328 Individual passes:
1329
1330 | Pass | Env | Side Maps | Similarity |
1331 |------|-----|-----------|------------|
1332 | PruneUnusedLabels | None | `Set<BlockId>` | ~95% |
1333 | PruneNonEscapingScopes | None | Dependency graph with cycle detection | ~85% |
1334 | PruneNonReactiveDependencies | None | None significant | ~95% |
1335 | PruneUnusedScopes | None | None significant | ~95% |
1336 | MergeReactiveScopesThatInvalidateTogether | None | Scope metadata comparison | ~85% |
1337 | PruneAlwaysInvalidatingScopes | None | None significant | ~95% |
1338 | PropagateEarlyReturns | None | Early return tracking state | ~85% |
1339 | PruneUnusedLValues | None | Lvalue usage tracking | ~90% |
1340 | PromoteUsedTemporaries | None | Identifier name mutation | ~90% |
1341 | ExtractScopeDeclarationsFromDestructuring | None | None significant | ~90% |
1342 | StabilizeBlockIds | None | `Map<BlockId, BlockId>` remapping | ~95% |
1343 | RenameVariables | None | Name collision tracking | ~90% |
1344 | PruneHoistedContexts | None | Context declaration tracking | ~95% |
1345
1346 ---
1347
1348 ### Phase 15: Codegen
1349
1350 #### CodegenReactiveFunction
1351 **Env usage**: `env.programContext` (imports, bindings), `env.getOutlinedFunctions()`, `env.recordErrors()`, `env.config`. **Side maps**: Context class with cache slot management, scope metadata tracking. **Similarity**: ~60%.
1352
1353 **The most significantly different pass** due to AST output generation. 1000+ lines of `t.*()` Babel API calls are replaced with constructing Rust Babel AST types that serialize to JSON via serde. Core scope logic (cache slot allocation, dependency checking, memoization code structure) can look structurally similar.
1354
1355 The `uniqueIdentifiers` and `fbtOperands` parameters translate directly.
1356
1357 ---
1358
1359 ### Validation Passes
1360
1361 ~15 validation passes share a common pattern: read-only HIR/ReactiveFunction traversal + error reporting via `env.recordError()`. They are the **easiest passes to port**. Common structure:
1362
1363 ```rust
1364 fn validate_hooks_usage(func: &HIRFunction, env: &mut Environment) -> Result<(), ()> {
1365 for block in func.body.blocks.values() {
1366 for instr in &block.instructions {
1367 match &instr.value {
1368 // check for violations, record errors
1369 }
1370 }
1371 }
1372 Ok(())
1373 }
1374 ```
1375
1376 All use `HashMap<IdentifierId, T>` for state tracking (ID-keyed, safe). Some return `CompilerError` directly instead of recording. The `tryRecord()` wrapping pattern maps to `Result` in Rust.
1377
1378 ---
1379
1380 ## External Dependencies
1381
1382 ### Input/Output: JSON AST Interchange
1383
1384 The Rust compiler defines its own representation of the Babel AST format using serde with custom serialization/deserialization, ensuring the `"type"` field is always produced (even outside of enum positions). Input ASTs are deserialized from JSON, and output ASTs are serialized back to JSON for consumption by the Babel plugin. A `Scope` type encodes the scope tree information that Babel provides. The main public API is `compile(BabelAst, Scope) -> Option<BabelAst>`, returning `None` if no changes are needed.
1385
1386 This approach decouples the Rust compiler from any specific JS parser — the JSON boundary handles the translation. The `resolveBinding()` pattern in BuildHIR (which uses Babel node reference equality in TypeScript) maps to scope-tree lookups via the `Scope` type.
1387
1388 ---
1389
1390 ## Risk Assessment
1391
1392 ### Low Risk (straightforward port)
1393 - All validation passes
1394 - Simple transformation passes (PruneMaybeThrows, PruneUnusedLabelsHIR, FlattenReactiveLoopsHIR, FlattenScopesWithHooksOrUseHIR, StabilizeBlockIds, RewriteInstructionKindsBasedOnReassignment, OptimizePropsMethodCalls, MergeConsecutiveBlocks)
1395 - Reactive pruning passes (PruneUnusedLabels, PruneUnusedScopes, PruneAlwaysInvalidatingScopes, PruneNonReactiveDependencies)
1396
1397 ### Medium Risk (requires systematic refactoring)
1398 - SSA passes (EnterSSA, EliminateRedundantPhi) — reference-identity maps → ID maps
1399 - Scope construction passes — centralized scope arena with ID-based references
1400 - Type inference (InferTypes) — arena-based Type storage, TypeId generation
1401 - Constant propagation — separated constants map, CFG cleanup infrastructure
1402 - Dead code elimination — two-phase collect/apply
1403 - Scope alignment passes — DisjointSet<ScopeId>, arena-based range mutation
1404 - Reactive function transforms — Visitor/MutVisitor trait design with Transformed enum
1405
1406 ### Medium Risk *(additional)*
1407 - **InferMutationAliasingEffects**: After [PR #33650](https://github.com/facebook/react/pull/33650), allocation-site identity uses interned `AliasingEffect` (→ `EffectId`), eliminating `InstructionValue` keys and `effectInstructionValueCache`. Remaining reference-identity maps use Instructions (→ `InstructionId`) and FunctionExpressions (→ `FunctionId`). All become copyable ID-keyed maps. Place sharing between effects and instructions is resolved by cloning (cheap with arena-based identifiers). `CreateFunction`'s FunctionExpression reference becomes a `FunctionId` referencing the function arena. Fixpoint loop and abstract interpretation structure port directly. See [§AliasingEffect section](#aliasingeffect-shared-references-and-rust-ownership) for full analysis.
1408
1409 ### High Risk (significant redesign)
1410 - **BuildHIR**: JSON AST deserialization, scope tree integration, closure-heavy builder patterns
1411 - **InferMutationAliasingRanges**: Graph-through-HIR mutation, temporal reasoning, deferred range updates
1412 - **CodegenReactiveFunction**: JSON AST output construction via serde, 1000+ lines of AST building
1413 - **AnalyseFunctions**: Recursive nested function processing via function arena, shared mutableRange semantics
1414
1415 ### Critical Architectural Decisions (must be designed upfront)
1416 1. **Arena-based storage on Environment**: Identifiers, scopes, functions, and types are stored as flat `Vec` fields on `Environment`, referenced by copyable ID types (`IdentifierId`, `ScopeId`, `FunctionId`, `TypeId`). Affects every pass.
1417 2. **Instructions table**: Instructions stored in flat `Vec<Instruction>` on `HIRFunction`, referenced by `InstructionId`. Old `InstructionId` renamed to `EvaluationOrder`.
1418 3. **Scope-based mutableRange access**: After InferReactiveScopeVariables, effective mutable range = scope's range. All downstream `isMutable()`/`inRange()` calls access the scope arena via `env.scopes`.
1419 4. **JSON AST interchange**: Input/output via serde-serialized Babel AST types and a `Scope` type for scope tree information.
1420 5. **Environment as single `&mut`**: No sub-struct grouping — flat fields allow precise sliced borrows. Passed separately from `HIRFunction`.
1421 6. **Error handling**: `Result<_, CompilerDiagnostic>` for thrown errors, accumulated errors on `Environment`.
1422
1423 ---
1424
1425 ## Recommended Migration Strategy
1426
1427 ### Phase 1: Foundation
1428 1. Define Rust data model (flat `Environment` with arena fields for Identifiers/Scopes/Functions/Types, all ID newtypes)
1429 2. Define HIR types as Rust enums/structs (InstructionValue ~40 variants, Terminal ~20 variants)
1430 3. Define flat `Environment` struct with arena fields, counters, config, and accumulated state
1431 4. Implement shared infrastructure: `DisjointSet<T: Copy>`, `IndexMap` wrappers, visitor utilities
1432 5. Define Babel AST types with serde serialization/deserialization for JSON AST interchange
1433 6. Build JSON serialization for HIR (enables testing against TypeScript implementation)
1434
1435 ### Phase 2: Core Pipeline
1436 1. Port BuildHIR (highest effort, most value — requires JSON AST deserialization and Scope type integration)
1437 2. Port normalization passes (PruneMaybeThrows, MergeConsecutiveBlocks — simple, builds confidence)
1438 3. Port SSA (EnterSSA, EliminateRedundantPhi — establishes arena patterns)
1439 4. Port ConstantPropagation, InferTypes
1440 5. Validate output matches TypeScript via JSON comparison at each stage
1441
1442 ### Phase 3: Analysis Engine
1443 1. Port AnalyseFunctions (establishes recursive compilation pattern)
1444 2. Port InferMutationAliasingEffects (establish EffectId interning table — EffectId serves as allocation-site identity, FunctionId-based function arena access for CreateFunction)
1445 3. Port DeadCodeElimination
1446 4. Port InferMutationAliasingRanges (establish deferred-range-update pattern)
1447 5. Port InferReactivePlaces
1448
1449 ### Phase 4: Scope System
1450 1. Port InferReactiveScopeVariables (establishes ScopeId → mutableRange indirection)
1451 2. Port scope alignment passes (Align*, Merge* — establish DisjointSet<ScopeId> pattern)
1452 3. Port BuildReactiveScopeTerminalsHIR
1453 4. Port PropagateScopeDependenciesHIR
1454
1455 ### Phase 5: Output
1456 1. Port BuildReactiveFunction (establishes reactive tree representation)
1457 2. Port reactive function transforms (Prune*, Promote*, Rename* — use trait-based visitor)
1458 3. Port CodegenReactiveFunction with JSON AST output
1459 4. Port validation passes (easiest, can be done in parallel)
1460 5. End-to-end integration testing