main
md 734 lines 35.4 KB
Rendered Raw
1 # Rust Port Step 4: BuildHIR / HIR Lowering
2
3 ## Goal
4
5 Port `BuildHIR.ts` (~4555 lines) and `HIRBuilder.ts` (~955 lines) into Rust equivalents in `compiler/crates/react_compiler_lowering/`. This is the first major compiler pass — it converts a Babel AST + scope info into the HIR control-flow graph representation.
6
7 The Rust port should be structurally as close to the TypeScript as possible: viewing the TS and Rust side by side, the logic should look, read, and feel similar while working naturally in Rust.
8
9 **Current status**: M1-M13 fully implemented. All statement types, expression types, destructuring, function expressions, JSX, switch/try-catch, for-of/in, optional chaining, and recursive lowering are complete. No `todo!()` stubs remain. `cargo check` passes. Remaining work: test against fixtures and fix divergences from TypeScript output.
10
11 **Known issues to fix:**
12 - All collection types must use `IndexMap`/`IndexSet` (from the `indexmap` crate), not `BTreeMap`/`BTreeSet`/`HashMap`/`HashSet`. This is critical for `HIR.blocks` where `BTreeMap` destroys RPO insertion ordering.
13 - Functions `lower_function`, `lower_function_to_value`, `gather_captured_context`, `lower_object_property_key`, `lower_type` take `&Expression`. The AST crate uses `Expression` for keys and doesn't have standalone `Function`/`ObjectPropertyKey`/`TypeAnnotation` types, so `&Expression` is correct for the current AST structure. When these functions are implemented, they should pattern-match on the specific expression variants internally.
14 - `VariableBinding::Identifier.binding_kind` is `String` — must be a `BindingKind` enum.
15 - `HirBuilder` is missing `component_scope: ScopeId` field (needed for `gather_captured_context` in M9).
16 - `build_temporary_place` helper is missing (listed in M4).
17 - `mark_predecessors` fallthrough handling: VERIFIED — matches TS `eachTerminalSuccessor` (does not include fallthroughs, correct).
18 - `GotoVariant::Break` usage: VERIFIED — matches TS for both `remove_unnecessary_try_catch` and `remove_dead_do_while_statements`.
19
20 ---
21
22 ## Crate Layout
23
24 ```
25 compiler/crates/
26 react_compiler_lowering/
27 Cargo.toml
28 src/
29 lib.rs # pub fn lower() entry point
30 build_hir.rs # lowerStatement, lowerExpression, lowerAssignment, etc.
31 hir_builder.rs # HIRBuilder struct
32 react_compiler_hir/
33 Cargo.toml
34 src/
35 lib.rs # HIR types: HirFunction, BasicBlock, Instruction, Terminal, Place, etc.
36 environment.rs # Environment struct (arenas, counters, config)
37 react_compiler_diagnostics/
38 Cargo.toml
39 src/
40 lib.rs # CompilerError, CompilerDiagnostic, ErrorCategory, etc.
41 ```
42
43 ### Dependencies
44
45 ```toml
46 # react_compiler_lowering/Cargo.toml
47 [dependencies]
48 react_compiler_ast = { path = "../react_compiler_ast" }
49 react_compiler_hir = { path = "../react_compiler_hir" }
50 react_compiler_diagnostics = { path = "../react_compiler_diagnostics" }
51 ```
52
53 ---
54
55 ## Key Design Decisions
56
57 ### 1. No NodePath — Work Directly with AST Structs + ScopeInfo
58
59 The TypeScript `lower()` takes a `NodePath<t.Function>` and uses Babel's traversal API (`path.get()`, `path.scope.getBinding()`, etc.) extensively. The Rust port works with deserialized `react_compiler_ast` structs and the `ScopeInfo` from step 2.
60
61 **TypeScript pattern:**
62 ```typescript
63 function lowerStatement(builder: HIRBuilder, stmtPath: NodePath<t.Statement>) {
64 switch (stmtPath.type) {
65 case 'IfStatement': {
66 const stmt = stmtPath as NodePath<t.IfStatement>;
67 const test = lowerExpressionToTemporary(builder, stmt.get('test'));
68 ...
69 }
70 }
71 }
72 ```
73
74 **Rust equivalent:**
75 ```rust
76 fn lower_statement(builder: &mut HirBuilder, stmt: &ast::Statement) {
77 match stmt {
78 ast::Statement::IfStatement(stmt) => {
79 let test = lower_expression_to_temporary(builder, &stmt.test);
80 ...
81 }
82 }
83 }
84 ```
85
86 The mapping is direct: `stmtPath.type` switch becomes `match stmt`, `stmt.get('test')` becomes `&stmt.test`, type narrowing via `as NodePath<T>` becomes Rust's `match` arm binding.
87
88 ### 2. Binding Resolution via ScopeInfo
89
90 The TypeScript `resolveIdentifier()` and `resolveBinding()` methods use Babel's scope API (`path.scope.getBinding()`, `babelBinding.scope`, `babelBinding.path.isImportSpecifier()`, etc.). The Rust port replaces all of this with `ScopeInfo` lookups.
91
92 **TypeScript** (`HIRBuilder.resolveIdentifier()`):
93 ```typescript
94 const babelBinding = path.scope.getBinding(originalName);
95 if (babelBinding === outerBinding) {
96 if (path.isImportDefaultSpecifier()) { ... }
97 }
98 const resolvedBinding = this.resolveBinding(babelBinding.identifier);
99 ```
100
101 **Rust equivalent:**
102 ```rust
103 fn resolve_identifier(&mut self, name: &str, start_offset: u32) -> VariableBinding {
104 // Look up via ScopeInfo instead of Babel's scope API
105 let binding_id = self.scope_info.resolve_reference(start_offset);
106 match binding_id {
107 None => VariableBinding::Global { name: name.to_string() },
108 Some(binding) => {
109 if binding.scope == self.scope_info.program_scope {
110 // Module-level binding — check import info
111 match &binding.import {
112 Some(import) => match import.kind {
113 ImportBindingKind::Default => VariableBinding::ImportDefault { ... },
114 ImportBindingKind::Named => VariableBinding::ImportSpecifier { ... },
115 ImportBindingKind::Namespace => VariableBinding::ImportNamespace { ... },
116 },
117 None => VariableBinding::ModuleLocal { name: name.to_string() },
118 }
119 } else {
120 let identifier = self.resolve_binding(name, binding_id.unwrap());
121 VariableBinding::Identifier { identifier, binding_kind: BindingKind::from(&binding.kind) }
122 }
123 }
124 }
125 }
126 ```
127
128 Key differences:
129 - **`resolveBinding()` keying**: TypeScript uses Babel node reference identity (`mapping.node === node`) to distinguish same-named variables in different scopes. Rust uses `BindingId` from `ScopeInfo` — the map becomes `IndexMap<BindingId, IdentifierId>` instead of `Map<string, {node, identifier}>`. This is simpler and more correct.
130 - **`isContextIdentifier()`**: TypeScript checks `env.isContextIdentifier(binding.identifier)`. Rust checks whether the binding's scope is an ancestor of the current function's scope but not the program scope — this is a `ScopeInfo` query.
131 - **`gatherCapturedContext()`**: TypeScript traverses the function with Babel's traverser to find free variable references. Rust walks the AST directly using `ScopeInfo.reference_to_binding` to identify references that resolve to bindings in ancestor scopes.
132
133 ### 3. HIRBuilder Struct
134
135 The `HIRBuilder` class maps to a Rust struct with `&mut self` methods. The closure-based APIs (`enter()`, `loop()`, `label()`, `switch()`) translate to methods that take `impl FnOnce(&mut Self) -> T`.
136
137 ```rust
138 pub struct HirBuilder<'a> {
139 completed: IndexMap<BlockId, BasicBlock>,
140 current: WipBlock,
141 entry: BlockId,
142 scopes: Vec<Scope>,
143 context: IndexMap<BindingId, Option<SourceLocation>>,
144 bindings: IndexMap<BindingId, IdentifierId>,
145 used_names: IndexMap<String, BindingId>,
146 instruction_table: Vec<Instruction>,
147 function_scope: ScopeId,
148 component_scope: ScopeId, // outermost component/hook scope, for gather_captured_context
149 env: &'a mut Environment,
150 scope_info: &'a ScopeInfo,
151 exception_handler_stack: Vec<BlockId>,
152 fbt_depth: u32,
153 }
154 ```
155
156 **Closure patterns**: The TypeScript `enter()` method creates a new block, sets it as current, runs a closure, then restores the previous block. In Rust:
157
158 ```rust
159 impl<'a> HirBuilder<'a> {
160 fn enter(&mut self, kind: BlockKind, f: impl FnOnce(&mut Self, BlockId) -> Terminal) -> BlockId {
161 let wip = self.reserve(kind);
162 let wip_id = wip.id;
163 self.enter_reserved(wip, |this| f(this, wip_id));
164 wip_id
165 }
166
167 fn enter_reserved(&mut self, wip: WipBlock, f: impl FnOnce(&mut Self) -> Terminal) {
168 let prev = std::mem::replace(&mut self.current, wip);
169 let terminal = f(self);
170 let completed = std::mem::replace(&mut self.current, prev);
171 self.completed.insert(completed.id, BasicBlock {
172 kind: completed.kind,
173 id: completed.id,
174 instructions: completed.instructions,
175 terminal,
176 preds: IndexSet::new(),
177 phis: Vec::new(),
178 });
179 }
180
181 fn loop_scope<T>(
182 &mut self,
183 label: Option<String>,
184 continue_block: BlockId,
185 break_block: BlockId,
186 f: impl FnOnce(&mut Self) -> T,
187 ) -> T {
188 self.scopes.push(Scope::Loop { label, continue_block, break_block });
189 let value = f(self);
190 self.scopes.pop();
191 value
192 }
193 }
194 ```
195
196 **Variable capture across closures**: TypeScript frequently assigns variables inside `enter()` closures that are read after:
197 ```typescript
198 let callee: Place | null = null;
199 builder.enter('block', () => {
200 callee = lowerExpressionToTemporary(builder, ...);
201 return { kind: 'goto', ... };
202 });
203 // callee is used here
204 ```
205
206 In Rust, this pattern is handled by returning values from the closure:
207 ```rust
208 let (block_id, callee) = {
209 let block_id = builder.enter('block', |builder, _block_id| {
210 // We can't easily return extra values from enter() since it expects Terminal
211 // Instead, compute callee before/after enter(), or restructure
212 ...
213 });
214 // Alternative: compute the value and store it on builder temporarily
215 };
216 ```
217
218 For cases where this is awkward, use a temporary field on the builder or restructure the code to compute the value outside the closure. The specific approach depends on the case — see the incremental implementation milestones for details.
219
220 ### 4. Source Locations
221
222 TypeScript accesses `node.loc` directly. Rust accesses `node.base.loc` (through the `BaseNode` flattened into each AST struct). Helper:
223
224 ```rust
225 fn loc_from_node(base: &BaseNode) -> SourceLocation {
226 base.loc.as_ref().map(|l| hir::SourceLocation::from(l)).unwrap_or(GENERATED_SOURCE)
227 }
228 ```
229
230 ### 5. Error Handling
231
232 Following the port notes:
233 - `CompilerError.invariant(cond, ...)``if !cond { panic!(...) }` or dedicated `compiler_invariant!` macro
234 - `CompilerError.throwTodo(...)``return Err(CompilerDiagnostic::todo(...))`
235 - `builder.recordError(...)``builder.record_error(...)` (accumulates on Environment)
236 - Non-null assertions (`!`) → `.unwrap()` or `.expect("...")`
237
238 The `lower()` function returns `Result<HirFunction, CompilerError>` for invariant/thrown errors, while accumulated errors go to `env.errors`.
239
240 ### 6. `todo!()` Strategy for Incremental Implementation
241
242 BuildHIR is too large (4555 lines) for a single implementation pass. Use Rust's `todo!()` macro to stub unimplemented branches:
243
244 ```rust
245 fn lower_statement(builder: &mut HirBuilder, stmt: &ast::Statement) {
246 match stmt {
247 ast::Statement::IfStatement(s) => lower_if_statement(builder, s),
248 ast::Statement::ReturnStatement(s) => lower_return_statement(builder, s),
249 ast::Statement::BlockStatement(s) => lower_block_statement(builder, s),
250 // Stubbed — will be filled in later milestones
251 ast::Statement::ForStatement(_) => todo!("lower ForStatement"),
252 ast::Statement::WhileStatement(_) => todo!("lower WhileStatement"),
253 ast::Statement::SwitchStatement(_) => todo!("lower SwitchStatement"),
254 ast::Statement::TryStatement(_) => todo!("lower TryStatement"),
255 // ... etc
256 }
257 }
258 ```
259
260 This "fog of war" approach allows:
261 1. The code to compile at every step
262 2. Tests to run for fixtures that only use implemented features
263 3. Clear visibility into what remains
264 4. Agents to pick up individual `todo!()` arms and implement them
265
266 ---
267
268 ## Structural Mapping: TypeScript → Rust
269
270 ### Top-Level Functions
271
272 | TypeScript (BuildHIR.ts) | Rust (build_hir.rs) | Notes |
273 |---|---|---|
274 | `lower(func, env, bindings, capturedRefs)` | `pub fn lower(ast: &ast::File, scope_info: &ScopeInfo, env: &mut Environment) -> Result<HirFunction, CompilerError>` | Entry point. Takes the full File (extracts the function internally) |
275 | `lowerStatement(builder, stmtPath, label)` | `fn lower_statement(builder: &mut HirBuilder, stmt: &ast::Statement, label: Option<&str>)` | ~30 match arms |
276 | `lowerExpression(builder, exprPath)` | `fn lower_expression(builder: &mut HirBuilder, expr: &ast::Expression) -> InstructionValue` | ~40 match arms |
277 | `lowerExpressionToTemporary(builder, exprPath)` | `fn lower_expression_to_temporary(builder: &mut HirBuilder, expr: &ast::Expression) -> Place` | |
278 | `lowerValueToTemporary(builder, value)` | `fn lower_value_to_temporary(builder: &mut HirBuilder, value: InstructionValue) -> Place` | |
279 | `lowerAssignment(builder, loc, kind, target, value, assignmentStyle)` | `fn lower_assignment(builder: &mut HirBuilder, ...)` | Handles destructuring patterns |
280 | `lowerIdentifier(builder, exprPath)` | `fn lower_identifier(builder: &mut HirBuilder, name: &str, start: u32, loc: SourceLocation) -> Place` | |
281 | `lowerMemberExpression(builder, exprPath)` | `fn lower_member_expression(builder: &mut HirBuilder, expr: &ast::MemberExpression) -> InstructionValue` | |
282 | `lowerOptionalMemberExpression(builder, exprPath)` | `fn lower_optional_member_expression(builder: &mut HirBuilder, expr: &ast::OptionalMemberExpression) -> InstructionValue` | |
283 | `lowerOptionalCallExpression(builder, exprPath)` | `fn lower_optional_call_expression(builder: &mut HirBuilder, expr: &ast::OptionalCallExpression) -> InstructionValue` | |
284 | `lowerArguments(builder, args, isDev)` | `fn lower_arguments(builder: &mut HirBuilder, args: &[ast::Expression], is_dev: bool) -> Vec<PlaceOrSpread>` | |
285 | `lowerFunctionToValue(builder, expr)` | `fn lower_function_to_value(builder: &mut HirBuilder, expr: &ast::Function) -> InstructionValue` | |
286 | `lowerFunction(builder, expr)` | `fn lower_function(builder: &mut HirBuilder, expr: &ast::Function) -> LoweredFunction` | Recursive `lower()` call. Returns `LoweredFunction` (not `FunctionId`) |
287 | `lowerJsxElementName(builder, name)` | `fn lower_jsx_element_name(builder: &mut HirBuilder, name: &ast::JSXElementName) -> JsxTag` | |
288 | `lowerJsxElement(builder, child)` | `fn lower_jsx_element(builder: &mut HirBuilder, child: &ast::JSXChild) -> Option<Place>` | |
289 | `lowerObjectMethod(builder, property)` | `fn lower_object_method(builder: &mut HirBuilder, method: &ast::ObjectMethod) -> ObjectProperty` | |
290 | `lowerObjectPropertyKey(builder, key)` | `fn lower_object_property_key(builder: &mut HirBuilder, key: &ast::ObjectPropertyKey) -> ObjectPropertyKey` | |
291 | `lowerReorderableExpression(builder, expr)` | `fn lower_reorderable_expression(builder: &mut HirBuilder, expr: &ast::Expression) -> Place` | |
292 | `isReorderableExpression(builder, expr)` | `fn is_reorderable_expression(builder: &HirBuilder, expr: &ast::Expression) -> bool` | |
293 | `lowerType(node)` | `fn lower_type(node: &ast::TypeAnnotation) -> Type` | |
294 | `gatherCapturedContext(fn, componentScope)` | `fn gather_captured_context(func: &ast::Function, scope_info: &ScopeInfo, parent_scope: ScopeId) -> IndexMap<BindingId, Option<SourceLocation>>` | AST walk replaces Babel traverser |
295 | `captureScopes({from, to})` | `fn capture_scopes(scope_info: &ScopeInfo, from: ScopeId, to: ScopeId) -> IndexSet<ScopeId>` | |
296
297 ### HIRBuilder Methods
298
299 | TypeScript (HIRBuilder.ts) | Rust (hir_builder.rs) | Notes |
300 |---|---|---|
301 | `constructor(env, options?)` | `HirBuilder::new(env, scope_info, function_scope, bindings, context, entry_block_kind)` | |
302 | `push(instruction)` | `builder.push(instruction)` | |
303 | `terminate(terminal, nextBlockKind)` | `builder.terminate(terminal, next_block_kind)` | |
304 | `terminateWithContinuation(terminal, continuation)` | `builder.terminate_with_continuation(terminal, continuation)` | |
305 | `reserve(kind)` | `builder.reserve(kind)` | Returns `WipBlock` |
306 | `complete(block, terminal)` | `builder.complete(block, terminal)` | |
307 | `enter(kind, fn)` | `builder.enter(kind, \|b, id\| { ... })` | Closure takes `&mut Self` |
308 | `enterReserved(wip, fn)` | `builder.enter_reserved(wip, \|b\| { ... })` | |
309 | `enterTryCatch(handler, fn)` | `builder.enter_try_catch(handler, \|b\| { ... })` | |
310 | `loop(label, continue, break, fn)` | `builder.loop_scope(label, continue_block, break_block, \|b\| { ... })` | |
311 | `label(label, break, fn)` | `builder.label_scope(label, break_block, \|b\| { ... })` | |
312 | `switch(label, break, fn)` | `builder.switch_scope(label, break_block, \|b\| { ... })` | |
313 | `lookupBreak(label)` | `builder.lookup_break(label)` | |
314 | `lookupContinue(label)` | `builder.lookup_continue(label)` | |
315 | `resolveIdentifier(path)` | `builder.resolve_identifier(name, start_offset)` | Uses ScopeInfo |
316 | `resolveBinding(node)` | `builder.resolve_binding(name, binding_id)` | Keyed by BindingId |
317 | `isContextIdentifier(path)` | `builder.is_context_identifier(name, start_offset)` | Uses ScopeInfo |
318 | `makeTemporary(loc)` | `builder.make_temporary(loc)` | |
319 | `build()` | `builder.build()` | Returns `(HIR, Vec<Instruction>)` — the HIR plus the flat instruction table |
320 | `recordError(error)` | `builder.record_error(error)` | |
321
322 ### Post-Build Helpers (HIRBuilder.ts)
323
324 These helper functions in HIRBuilder.ts run after `build()` and clean up the CFG:
325
326 | TypeScript | Rust | Notes |
327 |---|---|---|
328 | `getReversePostorderedBlocks(func)` | `get_reverse_postordered_blocks(hir)` | RPO sort + unreachable removal |
329 | `removeUnreachableForUpdates(fn)` | `remove_unreachable_for_updates(hir)` | |
330 | `removeDeadDoWhileStatements(func)` | `remove_dead_do_while_statements(hir)` | |
331 | `removeUnnecessaryTryCatch(fn)` | `remove_unnecessary_try_catch(hir)` | |
332 | `markInstructionIds(func)` | `mark_instruction_ids(hir)` | Assigns EvaluationOrder |
333 | `markPredecessors(func)` | `mark_predecessors(hir)` | Must include fallthrough blocks — verify `each_terminal_successor` matches TS `eachTerminalSuccessor` |
334 | `createTemporaryPlace(env, loc)` | `create_temporary_place(env, loc)` | |
335
336 **Implementation notes for post-build helpers:**
337 - `remove_unnecessary_try_catch` and `remove_dead_do_while_statements`: Verify that the `GotoVariant` used when replacing terminals matches the TS equivalent. Currently uses `GotoVariant::Break` — confirm this is correct.
338 - `mark_predecessors`: The `each_terminal_successor` function must visit fallthrough blocks for terminals like `Try`, not just direct successors. Compare against TS `eachTerminalSuccessor` behavior.
339
340 ---
341
342 ## Statement Lowering: Match Arm Inventory
343
344 The `lowerStatement` function has ~30 match arms. Grouped by complexity:
345
346 ### Tier 1 — Trivial (1-10 lines each)
347 - `EmptyStatement` — no-op
348 - `DebuggerStatement` — single `Debugger` instruction
349 - `ExpressionStatement` — delegate to `lower_expression_to_temporary`
350 - `BreakStatement``builder.lookup_break()` + goto terminal
351 - `ContinueStatement``builder.lookup_continue()` + goto terminal
352 - `ThrowStatement` — lower expression + throw terminal
353
354 ### Tier 2 — Simple control flow (10-30 lines each)
355 - `ReturnStatement` — lower expression + return terminal
356 - `BlockStatement` — iterate body statements
357 - `IfStatement` — reserve blocks, enter consequent/alternate, branch terminal
358 - `WhileStatement` — test block + body block + loop scope
359 - `LabeledStatement` — delegate with label, or create label scope
360
361 ### Tier 3 — Complex control flow (30-100 lines each)
362 - `ForStatement` — init/test/update/body blocks, loop scope
363 - `ForOfStatement` — iterator protocol (GetIterator, IteratorNext, etc.)
364 - `ForInStatement` — similar to ForOf
365 - `DoWhileStatement` — body-first loop
366 - `SwitchStatement` — case discrimination with fall-through
367 - `TryStatement` — try/catch/finally blocks with exception handler stack
368
369 ### Tier 4 — Variable declarations and assignments (30-80 lines)
370 - `VariableDeclaration` — iterate declarators, handle destructuring
371 - `FunctionDeclaration` — hoist function, lower body
372
373 ### Tier 5 — Pass-through / error (1-10 lines each)
374 - TypeScript/Flow declarations — `todo!()` or skip
375 - Import/Export declarations — error (shouldn't appear in function body)
376 - `WithStatement` — error (unsupported)
377 - `ClassDeclaration` — lower class expression
378 - `EnumDeclaration` / `TSEnumDeclaration` — error
379
380 ---
381
382 ## Expression Lowering: Match Arm Inventory
383
384 The `lowerExpression` function has ~40 match arms. Grouped by complexity:
385
386 ### Tier 1 — Literals and simple values (1-10 lines each)
387 - `NullLiteral`, `BooleanLiteral`, `NumericLiteral`, `StringLiteral``Primitive` instruction
388 - `RegExpLiteral``RegExpLiteral` instruction
389 - `Identifier` — delegate to `lower_identifier`
390 - `MetaProperty``LoadGlobal` for `import.meta`
391 - `TSNonNullExpression`, `TSInstantiationExpression` — unwrap inner expression
392 - `TypeCastExpression`, `TSAsExpression`, `TSSatisfiesExpression` — unwrap inner expression
393
394 ### Tier 2 — Operators (10-30 lines each)
395 - `BinaryExpression` — lower operands + `BinaryExpression` instruction
396 - `UnaryExpression` — lower operand + `UnaryExpression` instruction
397 - `UpdateExpression` — read + increment + store (prefix vs postfix)
398 - `SequenceExpression` — lower all expressions, return last
399
400 ### Tier 3 — Object/Array construction (20-50 lines each)
401 - `ObjectExpression` — properties, spread, computed keys
402 - `ArrayExpression` — elements with holes and spreads
403 - `TemplateLiteral` — quasis + expressions
404 - `TaggedTemplateExpression` — tag + template
405
406 ### Tier 4 — Calls and member access (20-50 lines each)
407 - `CallExpression` — callee + arguments + `CallExpression`/`MethodCall` instruction
408 - `NewExpression` — similar to CallExpression
409 - `MemberExpression` — object + property + `PropertyLoad`/`ComputedLoad`
410 - `OptionalCallExpression` — optional chain with test blocks
411 - `OptionalMemberExpression` — optional chain with test blocks
412
413 ### Tier 5 — Control flow expressions (30-80 lines each)
414 - `ConditionalExpression` — if-like CFG with value blocks
415 - `LogicalExpression` — short-circuit evaluation with blocks
416 - `AssignmentExpression` — delegates to `lower_assignment` (destructuring)
417
418 ### Tier 6 — Complex (50-150 lines each)
419 - `JSXElement` — tag + props + children + fbt handling
420 - `JSXFragment` — children only
421 - `ArrowFunctionExpression` / `FunctionExpression` — recursive `lower_function`
422 - `AwaitExpression` — lower value + await instruction
423
424 ---
425
426 ## Assignment Lowering
427
428 `lowerAssignment` (~500 lines in BuildHIR.ts) handles destructuring and is the most complex single function after the statement/expression switches. It processes:
429
430 ### Match arms by target type:
431 - **`Identifier`**`StoreLocal` instruction (with const/let/reassign distinction)
432 - **`MemberExpression`**`PropertyStore` / `ComputedStore` instruction
433 - **`ArrayPattern`** — emit `Destructure` with `ArrayPattern` containing items, holes, rest elements, and default values
434 - **`ObjectPattern`** — emit `Destructure` with `ObjectPattern` containing properties, computed keys, rest elements, and default values
435 - **`AssignmentPattern`** — default value handling: lower the default, emit a conditional assignment
436
437 ### Rust approach:
438 The destructuring patterns map directly — the AST struct fields (`elements`, `properties`, `rest`) correspond to the Babel API calls. The main difference is accessing nested patterns through struct fields instead of `path.get()`.
439
440 ---
441
442 ## Recursive Lowering for Nested Functions
443
444 `lowerFunction()` calls `lower()` recursively for function expressions, arrow functions, and object methods. Key considerations for Rust:
445
446 1. **Shared Environment**: Parent and child share `&mut Environment`. This works because the recursive call completes before the parent continues.
447
448 2. **Shared Bindings**: The parent's `bindings` map is passed to the child so inner functions can resolve references to outer variables. In Rust, this is `&IndexMap<BindingId, IdentifierId>` — the parent's bindings are cloned or borrowed by the child.
449
450 3. **Context gathering**: `gatherCapturedContext()` walks the function's AST to find free variable references. In Rust, this walks the AST structs using `ScopeInfo` to identify references that resolve to bindings in ancestor scopes (between the function's scope and the component scope).
451
452 4. **Function arena storage**: The returned `HirFunction` is stored in `env.functions` (the function arena) and referenced by `FunctionId` in the `FunctionExpression` instruction value.
453
454 ```rust
455 fn lower_function(builder: &mut HirBuilder, func: &ast::Function) -> LoweredFunction {
456 let captured_context = gather_captured_context(func, builder.scope_info, builder.component_scope);
457 let lowered = lower(func, builder.scope_info, builder.env, Some(&builder.bindings), captured_context)?;
458 lowered
459 }
460 ```
461
462 ---
463
464 ## Incremental Implementation Plan
465
466 ### M1: Scaffold + Infrastructure
467
468 **Goal**: Crate structure compiles, `lower()` entry point exists, returns `todo!()`.
469
470 1. Create `compiler/crates/react_compiler_diagnostics/` with `CompilerDiagnostic`, `CompilerError`, `ErrorCategory`, `CompilerErrorDetail`, `CompilerSuggestionOperation`.
471
472 2. Create `compiler/crates/react_compiler_hir/` with core types:
473 - ID newtypes: `BlockId`, `IdentifierId`, `InstructionId` (index into the flat instruction table), `EvaluationOrder` (sequential numbering assigned during `markInstructionIds()` — this was previously called `InstructionId` in the TypeScript compiler), `DeclarationId`, `ScopeId`, `FunctionId`, `TypeId`
474 - `HirFunction`, `HIR`, `BasicBlock`, `WipBlock`, `BlockKind`
475 - `Instruction`, `InstructionValue` (enum with all ~40 variants, each stubbed as `todo!()` for fields)
476 - `Terminal` (enum with all variants)
477 - `Place`, `Identifier`, `MutableRange`, `SourceLocation`
478 - `Effect`, `InstructionKind`, `GotoVariant`, `BindingKind` (enum: `Var`, `Let`, `Const`, `Param`, `Using`, `AwaitUsing`, `CatchParam`, `ImplicitConst`)
479 - `Environment` (counters, arenas, config, errors)
480 - `FloatValue(u64)` — wrapper type for f64 values that need `Eq`/`Hash` (stores raw bits via `f64::to_bits()` for deterministic comparison)
481
482 3. Create `compiler/crates/react_compiler_lowering/` with:
483 - `hir_builder.rs`: `HirBuilder` struct with all methods stubbed
484 - `build_hir.rs`: `lower_statement()` and `lower_expression()` with all arms as `todo!()`
485 - `lib.rs`: `pub fn lower()` that creates a builder and returns `todo!()`
486
487 4. Verify: `cargo check` passes.
488
489 ### M2: HIRBuilder Core
490
491 **Goal**: HIRBuilder methods work — can create blocks, terminate them, build the CFG.
492
493 1. Implement `HirBuilder::new()`, `push()`, `terminate()`, `terminate_with_continuation()`, `reserve()`, `complete()`, `enter_reserved()`, `enter()`.
494
495 2. Implement scope methods: `loop_scope()`, `label_scope()`, `switch_scope()`, `lookup_break()`, `lookup_continue()`.
496
497 3. Implement `enter_try_catch()`, `resolve_throw_handler()`.
498
499 4. Implement `make_temporary()`, `record_error()`.
500
501 5. Implement `build()` including the post-build passes:
502 - `get_reverse_postordered_blocks()`
503 - `remove_unreachable_for_updates()`
504 - `remove_dead_do_while_statements()`
505 - `remove_unnecessary_try_catch()`
506 - `mark_instruction_ids()`
507 - `mark_predecessors()`
508
509 ### M3: Binding Resolution
510
511 **Goal**: `resolve_identifier()` and `resolve_binding()` work with `ScopeInfo`.
512
513 1. Implement `resolve_binding()` — maps `BindingId` to `IdentifierId`, creating new identifiers on first encounter. Uses `IndexMap<BindingId, IdentifierId>` instead of the TypeScript `Map<string, {node, identifier}>`.
514
515 2. Implement `resolve_identifier()` — dispatches to Global, ImportDefault, ImportSpecifier, ImportNamespace, ModuleLocal, or Identifier based on `ScopeInfo` lookups.
516
517 3. Implement `is_context_identifier()` — checks if a reference resolves to a binding in an ancestor scope.
518
519 4. Implement `gather_captured_context()` — walks AST to find free variable references using `ScopeInfo`.
520
521 ### M4: `lower()` Entry Point + Basic Statements
522
523 **Goal**: Can lower simple functions with `ReturnStatement`, `ExpressionStatement`, `BlockStatement`, `VariableDeclaration` (simple, non-destructuring).
524
525 1. Implement the `lower()` function body: parameter processing, body lowering, final return terminal, `builder.build()`.
526
527 2. Implement statement arms:
528 - `ReturnStatement`
529 - `ExpressionStatement`
530 - `BlockStatement`
531 - `EmptyStatement`
532 - `VariableDeclaration` (simple `let x = expr` only, destructuring as `todo!()`)
533
534 3. Implement basic expression arms:
535 - `Identifier` (via `lower_identifier`)
536 - `NullLiteral`, `BooleanLiteral`, `NumericLiteral`, `StringLiteral`
537 - `BinaryExpression`
538 - `UnaryExpression`
539
540 4. Implement helpers: `lower_expression_to_temporary()`, `lower_value_to_temporary()`, `build_temporary_place()`.
541
542 5. **Test**: Run `test-rust-port.sh HIR` on simple fixtures.
543
544 ### M5: Control Flow
545
546 **Goal**: Branches and loops work.
547
548 1. `IfStatement` — consequent/alternate blocks, branch terminal
549 2. `WhileStatement` — test/body blocks, loop scope
550 3. `ForStatement` — init/test/update/body blocks
551 4. `DoWhileStatement` — body-first loop pattern
552 5. `BreakStatement`, `ContinueStatement`
553 6. `LabeledStatement`
554
555 ### M6: Expressions — Calls and Members
556
557 **Goal**: Function calls and property access work.
558
559 1. `CallExpression` — including method calls (callee is MemberExpression)
560 2. `NewExpression`
561 3. `MemberExpression` — PropertyLoad/ComputedLoad
562 4. `lower_arguments()` — spread handling
563 5. `SequenceExpression`
564
565 ### M7: Expressions — Short-circuit and Ternary
566
567 **Goal**: Control-flow expressions produce correct CFG.
568
569 1. `ConditionalExpression` — if-like structure with value blocks
570 2. `LogicalExpression` — short-circuit `&&`, `||`, `??`
571 3. `AssignmentExpression` — simple identifier/member assignment (destructuring deferred)
572
573 ### M8: Expressions — Remaining
574
575 **Goal**: All expression types handled.
576
577 1. `ObjectExpression` — properties, methods, computed, spread
578 2. `ArrayExpression` — elements, holes, spreads
579 3. `TemplateLiteral`, `TaggedTemplateExpression`
580 4. `UpdateExpression` — prefix/postfix increment/decrement
581 5. `RegExpLiteral`
582 6. `AwaitExpression`
583 7. `TypeCastExpression`, `TSAsExpression`, `TSSatisfiesExpression`, `TSNonNullExpression`, `TSInstantiationExpression`
584 8. `MetaProperty`
585
586 ### M9: Function Expressions + Recursive Lowering
587
588 **Goal**: Nested functions work.
589
590 1. `ArrowFunctionExpression`, `FunctionExpression` — call `lower_function()`
591 2. `lower_function()` — recursive `lower()` with captured context
592 3. `gather_captured_context()` — AST walk for free variables
593 4. Function arena storage via `FunctionId`
594 5. `FunctionDeclaration` statement — hoisted function lowering
595
596 ### M10: JSX
597
598 **Goal**: JSX elements and fragments lower correctly.
599
600 1. `JSXElement` — tag, props, children, fbt handling
601 2. `JSXFragment` — children
602 3. `lower_jsx_element_name()` — identifier, member expression, builtin tag dispatch
603 4. `lower_jsx_element()` — child lowering (text, expression, element, spread)
604 5. `lower_jsx_member_expression()`
605 6. `trimJsxText()` — whitespace normalization
606
607 ### M11: Destructuring + Complex Assignments
608
609 **Goal**: Full destructuring support.
610
611 1. `lower_assignment()` for `ArrayPattern` — items, holes, rest, defaults
612 2. `lower_assignment()` for `ObjectPattern` — properties, computed keys, rest, defaults
613 3. `lower_assignment()` for `AssignmentPattern` — default values
614 4. `VariableDeclaration` with destructuring patterns
615 5. Param destructuring in `lower()` entry point
616
617 ### M12: Switch + Try/Catch + Remaining
618
619 **Goal**: All statement types handled, complete coverage.
620
621 1. `SwitchStatement` — case discrimination, fall-through, break
622 2. `TryStatement` — try/catch/finally blocks, exception handler stack
623 3. `ForOfStatement` — iterator protocol
624 4. `ForInStatement` — for-in lowering
625 5. `WithStatement` — error
626 6. `ClassDeclaration` — class expression lowering
627 7. Type declarations — skip/pass-through
628 8. Import/Export declarations — error
629 9. `OptionalCallExpression`, `OptionalMemberExpression` — optional chaining
630 10. `lowerReorderableExpression()`, `isReorderableExpression()`
631
632 ### M13: Polish + Full Test Coverage
633
634 **Goal**: All fixtures pass, no remaining `todo!()` in production paths.
635
636 1. Remove all remaining `todo!()` stubs — replace with proper errors for truly unsupported syntax
637 2. Run `test-rust-port.sh HIR` on all 1714 fixtures
638 3. Debug and fix any divergences from TypeScript output
639 4. Handle edge cases: error recovery, Babel bug workarounds (where applicable), fbt depth tracking
640
641 ---
642
643 ## Key Rust Patterns
644
645 ### Pattern 1: Switch/Case → Match
646
647 Every `switch (stmtPath.type)` and `switch (exprPath.type)` becomes a `match` on the AST enum. Rust's exhaustive matching ensures no cases are missed (unlike TypeScript where the `default` arm might hide bugs).
648
649 ### Pattern 2: `path.get('field')` → Direct Field Access
650
651 ```typescript
652 // TypeScript
653 const test = stmt.get('test');
654 const body = stmt.get('body');
655 ```
656 ```rust
657 // Rust
658 let test = &stmt.test;
659 let body = &stmt.body;
660 ```
661
662 ### Pattern 3: Type Guards → Match Arms
663
664 ```typescript
665 // TypeScript
666 if (param.isIdentifier()) { ... }
667 else if (param.isObjectPattern()) { ... }
668 ```
669 ```rust
670 // Rust
671 match param {
672 ast::PatternLike::Identifier(id) => { ... }
673 ast::PatternLike::ObjectPattern(pat) => { ... }
674 }
675 ```
676
677 ### Pattern 4: `hasNode()` → `Option` Checks
678
679 ```typescript
680 // TypeScript
681 const alternate = stmt.get('alternate');
682 if (hasNode(alternate)) { ... }
683 ```
684 ```rust
685 // Rust
686 if let Some(alternate) = &stmt.alternate { ... }
687 ```
688
689 ### Pattern 5: Instruction Construction
690
691 ```typescript
692 // TypeScript
693 builder.push({
694 id: makeInstructionId(0),
695 lvalue: { ...place },
696 value: { kind: 'LoadGlobal', name, binding, loc },
697 effects: null,
698 loc: exprLoc,
699 });
700 ```
701 ```rust
702 // Rust
703 builder.push(Instruction {
704 id: InstructionId(0), // renumbered by markInstructionIds
705 lvalue: place.clone(),
706 value: InstructionValue::LoadGlobal { name, binding, loc },
707 effects: None,
708 loc: expr_loc,
709 });
710 ```
711
712 ---
713
714 ## Risks and Mitigations
715
716 ### Risk 1: `gatherCapturedContext()` Without Babel Traverser
717 **Impact**: Medium. The TypeScript version uses `fn.traverse()` to find free variable references.
718 **Mitigation**: Write a manual AST walker that visits all `Identifier` nodes in a function body and checks `ScopeInfo.reference_to_binding` for each one. This is simpler than Babel's traverser because we don't need the full visitor infrastructure — just recursive pattern matching over AST node types.
719
720 ### Risk 2: Variable Capture Across `enter()` Closures
721 **Impact**: Low-Medium. ~15-20 places in BuildHIR.ts assign variables inside `enter()` closures that are read outside.
722 **Mitigation**: Case-by-case restructuring. Options include: (a) returning the value from the closure via a tuple, (b) storing it on the builder temporarily, (c) restructuring to compute the value before/after the `enter()` call. Each instance is small and mechanical.
723
724 ### Risk 3: `isReorderableExpression()` Recursive Analysis
725 **Impact**: Low. This function deeply analyzes expressions to determine reorderability.
726 **Mitigation**: Direct recursive pattern matching on AST structs — actually simpler in Rust than TypeScript because there's no NodePath overhead.
727
728 ### Risk 4: Optional Chaining Lowering Complexity
729 **Impact**: Medium. `lowerOptionalCallExpression()` and `lowerOptionalMemberExpression()` (~250 lines combined) generate complex CFG structures with multiple blocks for null checks.
730 **Mitigation**: Port last (M12), after all simpler patterns are verified. The CFG generation logic maps directly — it's just verbose.
731
732 ### Risk 5: fbt/fbs Special Handling
733 **Impact**: Low. The fbt handling in JSXElement lowering uses Babel's `path.traverse()` for counting nested fbt tags.
734 **Mitigation**: Replace with a simple recursive AST walk that counts `JSXNamespacedName` nodes matching the fbt tag name. The fbtDepth counter on the builder is trivial.