1# Rust Port Step 4: BuildHIR / HIR Lowering23## Goal45Port `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.67The 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.89**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.1011**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`.1920---2122## Crate Layout2324```25compiler/crates/26 react_compiler_lowering/27 Cargo.toml28 src/29 lib.rs # pub fn lower() entry point30 build_hir.rs # lowerStatement, lowerExpression, lowerAssignment, etc.31 hir_builder.rs # HIRBuilder struct32 react_compiler_hir/33 Cargo.toml34 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.toml39 src/40 lib.rs # CompilerError, CompilerDiagnostic, ErrorCategory, etc.41```4243### Dependencies4445```toml46# react_compiler_lowering/Cargo.toml47[dependencies]48react_compiler_ast = { path = "../react_compiler_ast" }49react_compiler_hir = { path = "../react_compiler_hir" }50react_compiler_diagnostics = { path = "../react_compiler_diagnostics" }51```5253---5455## Key Design Decisions5657### 1. No NodePath — Work Directly with AST Structs + ScopeInfo5859The 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.6061**TypeScript pattern:**62```typescript63function 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```7374**Rust equivalent:**75```rust76fn 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```8586The 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.8788### 2. Binding Resolution via ScopeInfo8990The 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.9192**TypeScript** (`HIRBuilder.resolveIdentifier()`):93```typescript94const babelBinding = path.scope.getBinding(originalName);95if (babelBinding === outerBinding) {96 if (path.isImportDefaultSpecifier()) { ... }97}98const resolvedBinding = this.resolveBinding(babelBinding.identifier);99```100101**Rust equivalent:**102```rust103fn resolve_identifier(&mut self, name: &str, start_offset: u32) -> VariableBinding {104 // Look up via ScopeInfo instead of Babel's scope API105 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 info111 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```127128Key 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.132133### 3. HIRBuilder Struct134135The `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`.136137```rust138pub 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_context149 env: &'a mut Environment,150 scope_info: &'a ScopeInfo,151 exception_handler_stack: Vec<BlockId>,152 fbt_depth: u32,153}154```155156**Closure patterns**: The TypeScript `enter()` method creates a new block, sets it as current, runs a closure, then restores the previous block. In Rust:157158```rust159impl<'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_id165 }166167 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 }180181 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 value192 }193}194```195196**Variable capture across closures**: TypeScript frequently assigns variables inside `enter()` closures that are read after:197```typescript198let callee: Place | null = null;199builder.enter('block', () => {200 callee = lowerExpressionToTemporary(builder, ...);201 return { kind: 'goto', ... };202});203// callee is used here204```205206In Rust, this pattern is handled by returning values from the closure:207```rust208let (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 Terminal211 // Instead, compute callee before/after enter(), or restructure212 ...213 });214 // Alternative: compute the value and store it on builder temporarily215};216```217218For 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.219220### 4. Source Locations221222TypeScript accesses `node.loc` directly. Rust accesses `node.base.loc` (through the `BaseNode` flattened into each AST struct). Helper:223224```rust225fn loc_from_node(base: &BaseNode) -> SourceLocation {226 base.loc.as_ref().map(|l| hir::SourceLocation::from(l)).unwrap_or(GENERATED_SOURCE)227}228```229230### 5. Error Handling231232Following the port notes:233- `CompilerError.invariant(cond, ...)` → `if !cond { panic!(...) }` or dedicated `compiler_invariant!` macro234- `CompilerError.throwTodo(...)` → `return Err(CompilerDiagnostic::todo(...))`235- `builder.recordError(...)` → `builder.record_error(...)` (accumulates on Environment)236- Non-null assertions (`!`) → `.unwrap()` or `.expect("...")`237238The `lower()` function returns `Result<HirFunction, CompilerError>` for invariant/thrown errors, while accumulated errors go to `env.errors`.239240### 6. `todo!()` Strategy for Incremental Implementation241242BuildHIR is too large (4555 lines) for a single implementation pass. Use Rust's `todo!()` macro to stub unimplemented branches:243244```rust245fn 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 milestones251 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 // ... etc256 }257}258```259260This "fog of war" approach allows:2611. The code to compile at every step2622. Tests to run for fixtures that only use implemented features2633. Clear visibility into what remains2644. Agents to pick up individual `todo!()` arms and implement them265266---267268## Structural Mapping: TypeScript → Rust269270### Top-Level Functions271272| 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>` | |296297### HIRBuilder Methods298299| 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)` | |321322### Post-Build Helpers (HIRBuilder.ts)323324These helper functions in HIRBuilder.ts run after `build()` and clean up the CFG:325326| 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)` | |335336**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.339340---341342## Statement Lowering: Match Arm Inventory343344The `lowerStatement` function has ~30 match arms. Grouped by complexity:345346### Tier 1 — Trivial (1-10 lines each)347- `EmptyStatement` — no-op348- `DebuggerStatement` — single `Debugger` instruction349- `ExpressionStatement` — delegate to `lower_expression_to_temporary`350- `BreakStatement` — `builder.lookup_break()` + goto terminal351- `ContinueStatement` — `builder.lookup_continue()` + goto terminal352- `ThrowStatement` — lower expression + throw terminal353354### Tier 2 — Simple control flow (10-30 lines each)355- `ReturnStatement` — lower expression + return terminal356- `BlockStatement` — iterate body statements357- `IfStatement` — reserve blocks, enter consequent/alternate, branch terminal358- `WhileStatement` — test block + body block + loop scope359- `LabeledStatement` — delegate with label, or create label scope360361### Tier 3 — Complex control flow (30-100 lines each)362- `ForStatement` — init/test/update/body blocks, loop scope363- `ForOfStatement` — iterator protocol (GetIterator, IteratorNext, etc.)364- `ForInStatement` — similar to ForOf365- `DoWhileStatement` — body-first loop366- `SwitchStatement` — case discrimination with fall-through367- `TryStatement` — try/catch/finally blocks with exception handler stack368369### Tier 4 — Variable declarations and assignments (30-80 lines)370- `VariableDeclaration` — iterate declarators, handle destructuring371- `FunctionDeclaration` — hoist function, lower body372373### Tier 5 — Pass-through / error (1-10 lines each)374- TypeScript/Flow declarations — `todo!()` or skip375- Import/Export declarations — error (shouldn't appear in function body)376- `WithStatement` — error (unsupported)377- `ClassDeclaration` — lower class expression378- `EnumDeclaration` / `TSEnumDeclaration` — error379380---381382## Expression Lowering: Match Arm Inventory383384The `lowerExpression` function has ~40 match arms. Grouped by complexity:385386### Tier 1 — Literals and simple values (1-10 lines each)387- `NullLiteral`, `BooleanLiteral`, `NumericLiteral`, `StringLiteral` — `Primitive` instruction388- `RegExpLiteral` — `RegExpLiteral` instruction389- `Identifier` — delegate to `lower_identifier`390- `MetaProperty` — `LoadGlobal` for `import.meta`391- `TSNonNullExpression`, `TSInstantiationExpression` — unwrap inner expression392- `TypeCastExpression`, `TSAsExpression`, `TSSatisfiesExpression` — unwrap inner expression393394### Tier 2 — Operators (10-30 lines each)395- `BinaryExpression` — lower operands + `BinaryExpression` instruction396- `UnaryExpression` — lower operand + `UnaryExpression` instruction397- `UpdateExpression` — read + increment + store (prefix vs postfix)398- `SequenceExpression` — lower all expressions, return last399400### Tier 3 — Object/Array construction (20-50 lines each)401- `ObjectExpression` — properties, spread, computed keys402- `ArrayExpression` — elements with holes and spreads403- `TemplateLiteral` — quasis + expressions404- `TaggedTemplateExpression` — tag + template405406### Tier 4 — Calls and member access (20-50 lines each)407- `CallExpression` — callee + arguments + `CallExpression`/`MethodCall` instruction408- `NewExpression` — similar to CallExpression409- `MemberExpression` — object + property + `PropertyLoad`/`ComputedLoad`410- `OptionalCallExpression` — optional chain with test blocks411- `OptionalMemberExpression` — optional chain with test blocks412413### Tier 5 — Control flow expressions (30-80 lines each)414- `ConditionalExpression` — if-like CFG with value blocks415- `LogicalExpression` — short-circuit evaluation with blocks416- `AssignmentExpression` — delegates to `lower_assignment` (destructuring)417418### Tier 6 — Complex (50-150 lines each)419- `JSXElement` — tag + props + children + fbt handling420- `JSXFragment` — children only421- `ArrowFunctionExpression` / `FunctionExpression` — recursive `lower_function`422- `AwaitExpression` — lower value + await instruction423424---425426## Assignment Lowering427428`lowerAssignment` (~500 lines in BuildHIR.ts) handles destructuring and is the most complex single function after the statement/expression switches. It processes:429430### Match arms by target type:431- **`Identifier`** — `StoreLocal` instruction (with const/let/reassign distinction)432- **`MemberExpression`** — `PropertyStore` / `ComputedStore` instruction433- **`ArrayPattern`** — emit `Destructure` with `ArrayPattern` containing items, holes, rest elements, and default values434- **`ObjectPattern`** — emit `Destructure` with `ObjectPattern` containing properties, computed keys, rest elements, and default values435- **`AssignmentPattern`** — default value handling: lower the default, emit a conditional assignment436437### Rust approach:438The 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()`.439440---441442## Recursive Lowering for Nested Functions443444`lowerFunction()` calls `lower()` recursively for function expressions, arrow functions, and object methods. Key considerations for Rust:4454461. **Shared Environment**: Parent and child share `&mut Environment`. This works because the recursive call completes before the parent continues.4474482. **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.4494503. **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).4514524. **Function arena storage**: The returned `HirFunction` is stored in `env.functions` (the function arena) and referenced by `FunctionId` in the `FunctionExpression` instruction value.453454```rust455fn 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 lowered459}460```461462---463464## Incremental Implementation Plan465466### M1: Scaffold + Infrastructure467468**Goal**: Crate structure compiles, `lower()` entry point exists, returns `todo!()`.4694701. Create `compiler/crates/react_compiler_diagnostics/` with `CompilerDiagnostic`, `CompilerError`, `ErrorCategory`, `CompilerErrorDetail`, `CompilerSuggestionOperation`.4714722. 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)4814823. Create `compiler/crates/react_compiler_lowering/` with:483 - `hir_builder.rs`: `HirBuilder` struct with all methods stubbed484 - `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!()`4864874. Verify: `cargo check` passes.488489### M2: HIRBuilder Core490491**Goal**: HIRBuilder methods work — can create blocks, terminate them, build the CFG.4924931. Implement `HirBuilder::new()`, `push()`, `terminate()`, `terminate_with_continuation()`, `reserve()`, `complete()`, `enter_reserved()`, `enter()`.4944952. Implement scope methods: `loop_scope()`, `label_scope()`, `switch_scope()`, `lookup_break()`, `lookup_continue()`.4964973. Implement `enter_try_catch()`, `resolve_throw_handler()`.4984994. Implement `make_temporary()`, `record_error()`.5005015. 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()`508509### M3: Binding Resolution510511**Goal**: `resolve_identifier()` and `resolve_binding()` work with `ScopeInfo`.5125131. 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}>`.5145152. Implement `resolve_identifier()` — dispatches to Global, ImportDefault, ImportSpecifier, ImportNamespace, ModuleLocal, or Identifier based on `ScopeInfo` lookups.5165173. Implement `is_context_identifier()` — checks if a reference resolves to a binding in an ancestor scope.5185194. Implement `gather_captured_context()` — walks AST to find free variable references using `ScopeInfo`.520521### M4: `lower()` Entry Point + Basic Statements522523**Goal**: Can lower simple functions with `ReturnStatement`, `ExpressionStatement`, `BlockStatement`, `VariableDeclaration` (simple, non-destructuring).5245251. Implement the `lower()` function body: parameter processing, body lowering, final return terminal, `builder.build()`.5265272. Implement statement arms:528 - `ReturnStatement`529 - `ExpressionStatement`530 - `BlockStatement`531 - `EmptyStatement`532 - `VariableDeclaration` (simple `let x = expr` only, destructuring as `todo!()`)5335343. Implement basic expression arms:535 - `Identifier` (via `lower_identifier`)536 - `NullLiteral`, `BooleanLiteral`, `NumericLiteral`, `StringLiteral`537 - `BinaryExpression`538 - `UnaryExpression`5395404. Implement helpers: `lower_expression_to_temporary()`, `lower_value_to_temporary()`, `build_temporary_place()`.5415425. **Test**: Run `test-rust-port.sh HIR` on simple fixtures.543544### M5: Control Flow545546**Goal**: Branches and loops work.5475481. `IfStatement` — consequent/alternate blocks, branch terminal5492. `WhileStatement` — test/body blocks, loop scope5503. `ForStatement` — init/test/update/body blocks5514. `DoWhileStatement` — body-first loop pattern5525. `BreakStatement`, `ContinueStatement`5536. `LabeledStatement`554555### M6: Expressions — Calls and Members556557**Goal**: Function calls and property access work.5585591. `CallExpression` — including method calls (callee is MemberExpression)5602. `NewExpression`5613. `MemberExpression` — PropertyLoad/ComputedLoad5624. `lower_arguments()` — spread handling5635. `SequenceExpression`564565### M7: Expressions — Short-circuit and Ternary566567**Goal**: Control-flow expressions produce correct CFG.5685691. `ConditionalExpression` — if-like structure with value blocks5702. `LogicalExpression` — short-circuit `&&`, `||`, `??`5713. `AssignmentExpression` — simple identifier/member assignment (destructuring deferred)572573### M8: Expressions — Remaining574575**Goal**: All expression types handled.5765771. `ObjectExpression` — properties, methods, computed, spread5782. `ArrayExpression` — elements, holes, spreads5793. `TemplateLiteral`, `TaggedTemplateExpression`5804. `UpdateExpression` — prefix/postfix increment/decrement5815. `RegExpLiteral`5826. `AwaitExpression`5837. `TypeCastExpression`, `TSAsExpression`, `TSSatisfiesExpression`, `TSNonNullExpression`, `TSInstantiationExpression`5848. `MetaProperty`585586### M9: Function Expressions + Recursive Lowering587588**Goal**: Nested functions work.5895901. `ArrowFunctionExpression`, `FunctionExpression` — call `lower_function()`5912. `lower_function()` — recursive `lower()` with captured context5923. `gather_captured_context()` — AST walk for free variables5934. Function arena storage via `FunctionId`5945. `FunctionDeclaration` statement — hoisted function lowering595596### M10: JSX597598**Goal**: JSX elements and fragments lower correctly.5996001. `JSXElement` — tag, props, children, fbt handling6012. `JSXFragment` — children6023. `lower_jsx_element_name()` — identifier, member expression, builtin tag dispatch6034. `lower_jsx_element()` — child lowering (text, expression, element, spread)6045. `lower_jsx_member_expression()`6056. `trimJsxText()` — whitespace normalization606607### M11: Destructuring + Complex Assignments608609**Goal**: Full destructuring support.6106111. `lower_assignment()` for `ArrayPattern` — items, holes, rest, defaults6122. `lower_assignment()` for `ObjectPattern` — properties, computed keys, rest, defaults6133. `lower_assignment()` for `AssignmentPattern` — default values6144. `VariableDeclaration` with destructuring patterns6155. Param destructuring in `lower()` entry point616617### M12: Switch + Try/Catch + Remaining618619**Goal**: All statement types handled, complete coverage.6206211. `SwitchStatement` — case discrimination, fall-through, break6222. `TryStatement` — try/catch/finally blocks, exception handler stack6233. `ForOfStatement` — iterator protocol6244. `ForInStatement` — for-in lowering6255. `WithStatement` — error6266. `ClassDeclaration` — class expression lowering6277. Type declarations — skip/pass-through6288. Import/Export declarations — error6299. `OptionalCallExpression`, `OptionalMemberExpression` — optional chaining63010. `lowerReorderableExpression()`, `isReorderableExpression()`631632### M13: Polish + Full Test Coverage633634**Goal**: All fixtures pass, no remaining `todo!()` in production paths.6356361. Remove all remaining `todo!()` stubs — replace with proper errors for truly unsupported syntax6372. Run `test-rust-port.sh HIR` on all 1714 fixtures6383. Debug and fix any divergences from TypeScript output6394. Handle edge cases: error recovery, Babel bug workarounds (where applicable), fbt depth tracking640641---642643## Key Rust Patterns644645### Pattern 1: Switch/Case → Match646647Every `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).648649### Pattern 2: `path.get('field')` → Direct Field Access650651```typescript652// TypeScript653const test = stmt.get('test');654const body = stmt.get('body');655```656```rust657// Rust658let test = &stmt.test;659let body = &stmt.body;660```661662### Pattern 3: Type Guards → Match Arms663664```typescript665// TypeScript666if (param.isIdentifier()) { ... }667else if (param.isObjectPattern()) { ... }668```669```rust670// Rust671match param {672 ast::PatternLike::Identifier(id) => { ... }673 ast::PatternLike::ObjectPattern(pat) => { ... }674}675```676677### Pattern 4: `hasNode()` → `Option` Checks678679```typescript680// TypeScript681const alternate = stmt.get('alternate');682if (hasNode(alternate)) { ... }683```684```rust685// Rust686if let Some(alternate) = &stmt.alternate { ... }687```688689### Pattern 5: Instruction Construction690691```typescript692// TypeScript693builder.push({694 id: makeInstructionId(0),695 lvalue: { ...place },696 value: { kind: 'LoadGlobal', name, binding, loc },697 effects: null,698 loc: exprLoc,699});700```701```rust702// Rust703builder.push(Instruction {704 id: InstructionId(0), // renumbered by markInstructionIds705 lvalue: place.clone(),706 value: InstructionValue::LoadGlobal { name, binding, loc },707 effects: None,708 loc: expr_loc,709});710```711712---713714## Risks and Mitigations715716### Risk 1: `gatherCapturedContext()` Without Babel Traverser717**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.719720### Risk 2: Variable Capture Across `enter()` Closures721**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.723724### Risk 3: `isReorderableExpression()` Recursive Analysis725**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.727728### Risk 4: Optional Chaining Lowering Complexity729**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.731732### Risk 5: fbt/fbs Special Handling733**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.
Findings
✓ No findings reported for this file.