Rust Port Step 4: BuildHIR / HIR Lowering

Goal

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.

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.

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.

Known issues to fix:


Crate Layout

compiler/crates/
  react_compiler_lowering/
    Cargo.toml
    src/
      lib.rs              # pub fn lower() entry point
      build_hir.rs        # lowerStatement, lowerExpression, lowerAssignment, etc.
      hir_builder.rs      # HIRBuilder struct
  react_compiler_hir/
    Cargo.toml
    src/
      lib.rs              # HIR types: HirFunction, BasicBlock, Instruction, Terminal, Place, etc.
      environment.rs      # Environment struct (arenas, counters, config)
  react_compiler_diagnostics/
    Cargo.toml
    src/
      lib.rs              # CompilerError, CompilerDiagnostic, ErrorCategory, etc.

Dependencies

# react_compiler_lowering/Cargo.toml
[dependencies]
react_compiler_ast = { path = "../react_compiler_ast" }
react_compiler_hir = { path = "../react_compiler_hir" }
react_compiler_diagnostics = { path = "../react_compiler_diagnostics" }

Key Design Decisions

1. No NodePath — Work Directly with AST Structs + ScopeInfo

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.

TypeScript pattern:

function lowerStatement(builder: HIRBuilder, stmtPath: NodePath<t.Statement>) {
  switch (stmtPath.type) {
    case 'IfStatement': {
      const stmt = stmtPath as NodePath<t.IfStatement>;
      const test = lowerExpressionToTemporary(builder, stmt.get('test'));
      ...
    }
  }
}

Rust equivalent:

fn lower_statement(builder: &mut HirBuilder, stmt: &ast::Statement) {
    match stmt {
        ast::Statement::IfStatement(stmt) => {
            let test = lower_expression_to_temporary(builder, &stmt.test);
            ...
        }
    }
}

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.

2. Binding Resolution via ScopeInfo

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.

TypeScript (HIRBuilder.resolveIdentifier()):

const babelBinding = path.scope.getBinding(originalName);
if (babelBinding === outerBinding) {
  if (path.isImportDefaultSpecifier()) { ... }
}
const resolvedBinding = this.resolveBinding(babelBinding.identifier);

Rust equivalent:

fn resolve_identifier(&mut self, name: &str, start_offset: u32) -> VariableBinding {
    // Look up via ScopeInfo instead of Babel's scope API
    let binding_id = self.scope_info.resolve_reference(start_offset);
    match binding_id {
        None => VariableBinding::Global { name: name.to_string() },
        Some(binding) => {
            if binding.scope == self.scope_info.program_scope {
                // Module-level binding — check import info
                match &binding.import {
                    Some(import) => match import.kind {
                        ImportBindingKind::Default => VariableBinding::ImportDefault { ... },
                        ImportBindingKind::Named => VariableBinding::ImportSpecifier { ... },
                        ImportBindingKind::Namespace => VariableBinding::ImportNamespace { ... },
                    },
                    None => VariableBinding::ModuleLocal { name: name.to_string() },
                }
            } else {
                let identifier = self.resolve_binding(name, binding_id.unwrap());
                VariableBinding::Identifier { identifier, binding_kind: BindingKind::from(&binding.kind) }
            }
        }
    }
}

Key differences:

3. HIRBuilder Struct

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.

pub struct HirBuilder<'a> {
    completed: IndexMap<BlockId, BasicBlock>,
    current: WipBlock,
    entry: BlockId,
    scopes: Vec<Scope>,
    context: IndexMap<BindingId, Option<SourceLocation>>,
    bindings: IndexMap<BindingId, IdentifierId>,
    used_names: IndexMap<String, BindingId>,
    instruction_table: Vec<Instruction>,
    function_scope: ScopeId,
    component_scope: ScopeId,  // outermost component/hook scope, for gather_captured_context
    env: &'a mut Environment,
    scope_info: &'a ScopeInfo,
    exception_handler_stack: Vec<BlockId>,
    fbt_depth: u32,
}

Closure patterns: The TypeScript enter() method creates a new block, sets it as current, runs a closure, then restores the previous block. In Rust:

impl<'a> HirBuilder<'a> {
    fn enter(&mut self, kind: BlockKind, f: impl FnOnce(&mut Self, BlockId) -> Terminal) -> BlockId {
        let wip = self.reserve(kind);
        let wip_id = wip.id;
        self.enter_reserved(wip, |this| f(this, wip_id));
        wip_id
    }

    fn enter_reserved(&mut self, wip: WipBlock, f: impl FnOnce(&mut Self) -> Terminal) {
        let prev = std::mem::replace(&mut self.current, wip);
        let terminal = f(self);
        let completed = std::mem::replace(&mut self.current, prev);
        self.completed.insert(completed.id, BasicBlock {
            kind: completed.kind,
            id: completed.id,
            instructions: completed.instructions,
            terminal,
            preds: IndexSet::new(),
            phis: Vec::new(),
        });
    }

    fn loop_scope<T>(
        &mut self,
        label: Option<String>,
        continue_block: BlockId,
        break_block: BlockId,
        f: impl FnOnce(&mut Self) -> T,
    ) -> T {
        self.scopes.push(Scope::Loop { label, continue_block, break_block });
        let value = f(self);
        self.scopes.pop();
        value
    }
}

Variable capture across closures: TypeScript frequently assigns variables inside enter() closures that are read after:

let callee: Place | null = null;
builder.enter('block', () => {
  callee = lowerExpressionToTemporary(builder, ...);
  return { kind: 'goto', ... };
});
// callee is used here

In Rust, this pattern is handled by returning values from the closure:

let (block_id, callee) = {
    let block_id = builder.enter('block', |builder, _block_id| {
        // We can't easily return extra values from enter() since it expects Terminal
        // Instead, compute callee before/after enter(), or restructure
        ...
    });
    // Alternative: compute the value and store it on builder temporarily
};

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.

4. Source Locations

TypeScript accesses node.loc directly. Rust accesses node.base.loc (through the BaseNode flattened into each AST struct). Helper:

fn loc_from_node(base: &BaseNode) -> SourceLocation {
    base.loc.as_ref().map(|l| hir::SourceLocation::from(l)).unwrap_or(GENERATED_SOURCE)
}

5. Error Handling

Following the port notes:

The lower() function returns Result<HirFunction, CompilerError> for invariant/thrown errors, while accumulated errors go to env.errors.

6. todo!() Strategy for Incremental Implementation

BuildHIR is too large (4555 lines) for a single implementation pass. Use Rust's todo!() macro to stub unimplemented branches:

fn lower_statement(builder: &mut HirBuilder, stmt: &ast::Statement) {
    match stmt {
        ast::Statement::IfStatement(s) => lower_if_statement(builder, s),
        ast::Statement::ReturnStatement(s) => lower_return_statement(builder, s),
        ast::Statement::BlockStatement(s) => lower_block_statement(builder, s),
        // Stubbed — will be filled in later milestones
        ast::Statement::ForStatement(_) => todo!("lower ForStatement"),
        ast::Statement::WhileStatement(_) => todo!("lower WhileStatement"),
        ast::Statement::SwitchStatement(_) => todo!("lower SwitchStatement"),
        ast::Statement::TryStatement(_) => todo!("lower TryStatement"),
        // ... etc
    }
}

This "fog of war" approach allows:

  1. The code to compile at every step
  2. Tests to run for fixtures that only use implemented features
  3. Clear visibility into what remains
  4. Agents to pick up individual todo!() arms and implement them

Structural Mapping: TypeScript → Rust

Top-Level Functions

TypeScript (BuildHIR.ts) Rust (build_hir.rs) Notes
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)
lowerStatement(builder, stmtPath, label) fn lower_statement(builder: &mut HirBuilder, stmt: &ast::Statement, label: Option<&str>) ~30 match arms
lowerExpression(builder, exprPath) fn lower_expression(builder: &mut HirBuilder, expr: &ast::Expression) -> InstructionValue ~40 match arms
lowerExpressionToTemporary(builder, exprPath) fn lower_expression_to_temporary(builder: &mut HirBuilder, expr: &ast::Expression) -> Place
lowerValueToTemporary(builder, value) fn lower_value_to_temporary(builder: &mut HirBuilder, value: InstructionValue) -> Place
lowerAssignment(builder, loc, kind, target, value, assignmentStyle) fn lower_assignment(builder: &mut HirBuilder, ...) Handles destructuring patterns
lowerIdentifier(builder, exprPath) fn lower_identifier(builder: &mut HirBuilder, name: &str, start: u32, loc: SourceLocation) -> Place
lowerMemberExpression(builder, exprPath) fn lower_member_expression(builder: &mut HirBuilder, expr: &ast::MemberExpression) -> InstructionValue
lowerOptionalMemberExpression(builder, exprPath) fn lower_optional_member_expression(builder: &mut HirBuilder, expr: &ast::OptionalMemberExpression) -> InstructionValue
lowerOptionalCallExpression(builder, exprPath) fn lower_optional_call_expression(builder: &mut HirBuilder, expr: &ast::OptionalCallExpression) -> InstructionValue
lowerArguments(builder, args, isDev) fn lower_arguments(builder: &mut HirBuilder, args: &[ast::Expression], is_dev: bool) -> Vec<PlaceOrSpread>
lowerFunctionToValue(builder, expr) fn lower_function_to_value(builder: &mut HirBuilder, expr: &ast::Function) -> InstructionValue
lowerFunction(builder, expr) fn lower_function(builder: &mut HirBuilder, expr: &ast::Function) -> LoweredFunction Recursive lower() call. Returns LoweredFunction (not FunctionId)
lowerJsxElementName(builder, name) fn lower_jsx_element_name(builder: &mut HirBuilder, name: &ast::JSXElementName) -> JsxTag
lowerJsxElement(builder, child) fn lower_jsx_element(builder: &mut HirBuilder, child: &ast::JSXChild) -> Option<Place>
lowerObjectMethod(builder, property) fn lower_object_method(builder: &mut HirBuilder, method: &ast::ObjectMethod) -> ObjectProperty
lowerObjectPropertyKey(builder, key) fn lower_object_property_key(builder: &mut HirBuilder, key: &ast::ObjectPropertyKey) -> ObjectPropertyKey
lowerReorderableExpression(builder, expr) fn lower_reorderable_expression(builder: &mut HirBuilder, expr: &ast::Expression) -> Place
isReorderableExpression(builder, expr) fn is_reorderable_expression(builder: &HirBuilder, expr: &ast::Expression) -> bool
lowerType(node) fn lower_type(node: &ast::TypeAnnotation) -> Type
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
captureScopes({from, to}) fn capture_scopes(scope_info: &ScopeInfo, from: ScopeId, to: ScopeId) -> IndexSet<ScopeId>

HIRBuilder Methods

TypeScript (HIRBuilder.ts) Rust (hir_builder.rs) Notes
constructor(env, options?) HirBuilder::new(env, scope_info, function_scope, bindings, context, entry_block_kind)
push(instruction) builder.push(instruction)
terminate(terminal, nextBlockKind) builder.terminate(terminal, next_block_kind)
terminateWithContinuation(terminal, continuation) builder.terminate_with_continuation(terminal, continuation)
reserve(kind) builder.reserve(kind) Returns WipBlock
complete(block, terminal) builder.complete(block, terminal)
enter(kind, fn) builder.enter(kind, |b, id| { ... }) Closure takes &mut Self
enterReserved(wip, fn) builder.enter_reserved(wip, |b| { ... })
enterTryCatch(handler, fn) builder.enter_try_catch(handler, |b| { ... })
loop(label, continue, break, fn) builder.loop_scope(label, continue_block, break_block, |b| { ... })
label(label, break, fn) builder.label_scope(label, break_block, |b| { ... })
switch(label, break, fn) builder.switch_scope(label, break_block, |b| { ... })
lookupBreak(label) builder.lookup_break(label)
lookupContinue(label) builder.lookup_continue(label)
resolveIdentifier(path) builder.resolve_identifier(name, start_offset) Uses ScopeInfo
resolveBinding(node) builder.resolve_binding(name, binding_id) Keyed by BindingId
isContextIdentifier(path) builder.is_context_identifier(name, start_offset) Uses ScopeInfo
makeTemporary(loc) builder.make_temporary(loc)
build() builder.build() Returns (HIR, Vec<Instruction>) — the HIR plus the flat instruction table
recordError(error) builder.record_error(error)

Post-Build Helpers (HIRBuilder.ts)

These helper functions in HIRBuilder.ts run after build() and clean up the CFG:

TypeScript Rust Notes
getReversePostorderedBlocks(func) get_reverse_postordered_blocks(hir) RPO sort + unreachable removal
removeUnreachableForUpdates(fn) remove_unreachable_for_updates(hir)
removeDeadDoWhileStatements(func) remove_dead_do_while_statements(hir)
removeUnnecessaryTryCatch(fn) remove_unnecessary_try_catch(hir)
markInstructionIds(func) mark_instruction_ids(hir) Assigns EvaluationOrder
markPredecessors(func) mark_predecessors(hir) Must include fallthrough blocks — verify each_terminal_successor matches TS eachTerminalSuccessor
createTemporaryPlace(env, loc) create_temporary_place(env, loc)

Implementation notes for post-build helpers:


Statement Lowering: Match Arm Inventory

The lowerStatement function has ~30 match arms. Grouped by complexity:

Tier 1 — Trivial (1-10 lines each)

Tier 2 — Simple control flow (10-30 lines each)

Tier 3 — Complex control flow (30-100 lines each)

Tier 4 — Variable declarations and assignments (30-80 lines)

Tier 5 — Pass-through / error (1-10 lines each)


Expression Lowering: Match Arm Inventory

The lowerExpression function has ~40 match arms. Grouped by complexity:

Tier 1 — Literals and simple values (1-10 lines each)

Tier 2 — Operators (10-30 lines each)

Tier 3 — Object/Array construction (20-50 lines each)

Tier 4 — Calls and member access (20-50 lines each)

Tier 5 — Control flow expressions (30-80 lines each)

Tier 6 — Complex (50-150 lines each)


Assignment Lowering

lowerAssignment (~500 lines in BuildHIR.ts) handles destructuring and is the most complex single function after the statement/expression switches. It processes:

Match arms by target type:

Rust approach:

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


Recursive Lowering for Nested Functions

lowerFunction() calls lower() recursively for function expressions, arrow functions, and object methods. Key considerations for Rust:

  1. Shared Environment: Parent and child share &mut Environment. This works because the recursive call completes before the parent continues.

  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.

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

  4. Function arena storage: The returned HirFunction is stored in env.functions (the function arena) and referenced by FunctionId in the FunctionExpression instruction value.

fn lower_function(builder: &mut HirBuilder, func: &ast::Function) -> LoweredFunction {
    let captured_context = gather_captured_context(func, builder.scope_info, builder.component_scope);
    let lowered = lower(func, builder.scope_info, builder.env, Some(&builder.bindings), captured_context)?;
    lowered
}

Incremental Implementation Plan

M1: Scaffold + Infrastructure

Goal: Crate structure compiles, lower() entry point exists, returns todo!().

  1. Create compiler/crates/react_compiler_diagnostics/ with CompilerDiagnostic, CompilerError, ErrorCategory, CompilerErrorDetail, CompilerSuggestionOperation.

  2. Create compiler/crates/react_compiler_hir/ with core types:

    • 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
    • HirFunction, HIR, BasicBlock, WipBlock, BlockKind
    • Instruction, InstructionValue (enum with all ~40 variants, each stubbed as todo!() for fields)
    • Terminal (enum with all variants)
    • Place, Identifier, MutableRange, SourceLocation
    • Effect, InstructionKind, GotoVariant, BindingKind (enum: Var, Let, Const, Param, Using, AwaitUsing, CatchParam, ImplicitConst)
    • Environment (counters, arenas, config, errors)
    • FloatValue(u64) — wrapper type for f64 values that need Eq/Hash (stores raw bits via f64::to_bits() for deterministic comparison)
  3. Create compiler/crates/react_compiler_lowering/ with:

    • hir_builder.rs: HirBuilder struct with all methods stubbed
    • build_hir.rs: lower_statement() and lower_expression() with all arms as todo!()
    • lib.rs: pub fn lower() that creates a builder and returns todo!()
  4. Verify: cargo check passes.

M2: HIRBuilder Core

Goal: HIRBuilder methods work — can create blocks, terminate them, build the CFG.

  1. Implement HirBuilder::new(), push(), terminate(), terminate_with_continuation(), reserve(), complete(), enter_reserved(), enter().

  2. Implement scope methods: loop_scope(), label_scope(), switch_scope(), lookup_break(), lookup_continue().

  3. Implement enter_try_catch(), resolve_throw_handler().

  4. Implement make_temporary(), record_error().

  5. Implement build() including the post-build passes:

    • get_reverse_postordered_blocks()
    • remove_unreachable_for_updates()
    • remove_dead_do_while_statements()
    • remove_unnecessary_try_catch()
    • mark_instruction_ids()
    • mark_predecessors()

M3: Binding Resolution

Goal: resolve_identifier() and resolve_binding() work with ScopeInfo.

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

  2. Implement resolve_identifier() — dispatches to Global, ImportDefault, ImportSpecifier, ImportNamespace, ModuleLocal, or Identifier based on ScopeInfo lookups.

  3. Implement is_context_identifier() — checks if a reference resolves to a binding in an ancestor scope.

  4. Implement gather_captured_context() — walks AST to find free variable references using ScopeInfo.

M4: lower() Entry Point + Basic Statements

Goal: Can lower simple functions with ReturnStatement, ExpressionStatement, BlockStatement, VariableDeclaration (simple, non-destructuring).

  1. Implement the lower() function body: parameter processing, body lowering, final return terminal, builder.build().

  2. Implement statement arms:

    • ReturnStatement
    • ExpressionStatement
    • BlockStatement
    • EmptyStatement
    • VariableDeclaration (simple let x = expr only, destructuring as todo!())
  3. Implement basic expression arms:

    • Identifier (via lower_identifier)
    • NullLiteral, BooleanLiteral, NumericLiteral, StringLiteral
    • BinaryExpression
    • UnaryExpression
  4. Implement helpers: lower_expression_to_temporary(), lower_value_to_temporary(), build_temporary_place().

  5. Test: Run test-rust-port.sh HIR on simple fixtures.

M5: Control Flow

Goal: Branches and loops work.

  1. IfStatement — consequent/alternate blocks, branch terminal
  2. WhileStatement — test/body blocks, loop scope
  3. ForStatement — init/test/update/body blocks
  4. DoWhileStatement — body-first loop pattern
  5. BreakStatement, ContinueStatement
  6. LabeledStatement

M6: Expressions — Calls and Members

Goal: Function calls and property access work.

  1. CallExpression — including method calls (callee is MemberExpression)
  2. NewExpression
  3. MemberExpression — PropertyLoad/ComputedLoad
  4. lower_arguments() — spread handling
  5. SequenceExpression

M7: Expressions — Short-circuit and Ternary

Goal: Control-flow expressions produce correct CFG.

  1. ConditionalExpression — if-like structure with value blocks
  2. LogicalExpression — short-circuit &&, ||, ??
  3. AssignmentExpression — simple identifier/member assignment (destructuring deferred)

M8: Expressions — Remaining

Goal: All expression types handled.

  1. ObjectExpression — properties, methods, computed, spread
  2. ArrayExpression — elements, holes, spreads
  3. TemplateLiteral, TaggedTemplateExpression
  4. UpdateExpression — prefix/postfix increment/decrement
  5. RegExpLiteral
  6. AwaitExpression
  7. TypeCastExpression, TSAsExpression, TSSatisfiesExpression, TSNonNullExpression, TSInstantiationExpression
  8. MetaProperty

M9: Function Expressions + Recursive Lowering

Goal: Nested functions work.

  1. ArrowFunctionExpression, FunctionExpression — call lower_function()
  2. lower_function() — recursive lower() with captured context
  3. gather_captured_context() — AST walk for free variables
  4. Function arena storage via FunctionId
  5. FunctionDeclaration statement — hoisted function lowering

M10: JSX

Goal: JSX elements and fragments lower correctly.

  1. JSXElement — tag, props, children, fbt handling
  2. JSXFragment — children
  3. lower_jsx_element_name() — identifier, member expression, builtin tag dispatch
  4. lower_jsx_element() — child lowering (text, expression, element, spread)
  5. lower_jsx_member_expression()
  6. trimJsxText() — whitespace normalization

M11: Destructuring + Complex Assignments

Goal: Full destructuring support.

  1. lower_assignment() for ArrayPattern — items, holes, rest, defaults
  2. lower_assignment() for ObjectPattern — properties, computed keys, rest, defaults
  3. lower_assignment() for AssignmentPattern — default values
  4. VariableDeclaration with destructuring patterns
  5. Param destructuring in lower() entry point

M12: Switch + Try/Catch + Remaining

Goal: All statement types handled, complete coverage.

  1. SwitchStatement — case discrimination, fall-through, break
  2. TryStatement — try/catch/finally blocks, exception handler stack
  3. ForOfStatement — iterator protocol
  4. ForInStatement — for-in lowering
  5. WithStatement — error
  6. ClassDeclaration — class expression lowering
  7. Type declarations — skip/pass-through
  8. Import/Export declarations — error
  9. OptionalCallExpression, OptionalMemberExpression — optional chaining
  10. lowerReorderableExpression(), isReorderableExpression()

M13: Polish + Full Test Coverage

Goal: All fixtures pass, no remaining todo!() in production paths.

  1. Remove all remaining todo!() stubs — replace with proper errors for truly unsupported syntax
  2. Run test-rust-port.sh HIR on all 1714 fixtures
  3. Debug and fix any divergences from TypeScript output
  4. Handle edge cases: error recovery, Babel bug workarounds (where applicable), fbt depth tracking

Key Rust Patterns

Pattern 1: Switch/Case → Match

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

Pattern 2: path.get('field') → Direct Field Access

// TypeScript
const test = stmt.get('test');
const body = stmt.get('body');
// Rust
let test = &stmt.test;
let body = &stmt.body;

Pattern 3: Type Guards → Match Arms

// TypeScript
if (param.isIdentifier()) { ... }
else if (param.isObjectPattern()) { ... }
// Rust
match param {
    ast::PatternLike::Identifier(id) => { ... }
    ast::PatternLike::ObjectPattern(pat) => { ... }
}

Pattern 4: hasNode()Option Checks

// TypeScript
const alternate = stmt.get('alternate');
if (hasNode(alternate)) { ... }
// Rust
if let Some(alternate) = &stmt.alternate { ... }

Pattern 5: Instruction Construction

// TypeScript
builder.push({
    id: makeInstructionId(0),
    lvalue: { ...place },
    value: { kind: 'LoadGlobal', name, binding, loc },
    effects: null,
    loc: exprLoc,
});
// Rust
builder.push(Instruction {
    id: InstructionId(0), // renumbered by markInstructionIds
    lvalue: place.clone(),
    value: InstructionValue::LoadGlobal { name, binding, loc },
    effects: None,
    loc: expr_loc,
});

Risks and Mitigations

Risk 1: gatherCapturedContext() Without Babel Traverser

Impact: Medium. The TypeScript version uses fn.traverse() to find free variable references. 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.

Risk 2: Variable Capture Across enter() Closures

Impact: Low-Medium. ~15-20 places in BuildHIR.ts assign variables inside enter() closures that are read outside. 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.

Risk 3: isReorderableExpression() Recursive Analysis

Impact: Low. This function deeply analyzes expressions to determine reorderability. Mitigation: Direct recursive pattern matching on AST structs — actually simpler in Rust than TypeScript because there's no NodePath overhead.

Risk 4: Optional Chaining Lowering Complexity

Impact: Medium. lowerOptionalCallExpression() and lowerOptionalMemberExpression() (~250 lines combined) generate complex CFG structures with multiple blocks for null checks. Mitigation: Port last (M12), after all simpler patterns are verified. The CFG generation logic maps directly — it's just verbose.

Risk 5: fbt/fbs Special Handling

Impact: Low. The fbt handling in JSXElement lowering uses Babel's path.traverse() for counting nested fbt tags. 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.