| 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. |