compiler/docs/rust-port/rust-port-0003-testing-infrastructure.md MARKDOWN 635 lines View on github.com → Search inside
1# Rust Port Step 2: Testing Infrastructure23## Goal45Create a testing infrastructure that validates the Rust port produces identical results to the TypeScript compiler at every stage of the pipeline. The port proceeds incrementally  one pass at a time  so the test infrastructure must support running the pipeline up to any specified pass and comparing the intermediate state between TS and Rust.67**Current status**: M1, M2, M3 implemented. All Rust tests expected to fail (todo!() stubs). Next step: port lower() (M4).89**Known issues  resolved:**10- TS binary rewritten to call `compile()` directly (bypasses `transformFromAstSync` + `BabelPluginReactCompiler`). Individual pass functions aren't exported from dist, so logger-based capture is still used, but the Babel plugin orchestration layer is bypassed. (done)11- `debug_error` renamed to `format_errors` (done). `CompilerError` type name kept as-is since `CompilerDiagnostic` already exists as a different type in the diagnostics crate.12- Both TS and Rust now print `returnTypeAnnotation` in debug output. (done)13- `mark_predecessors` fallthrough handling: VERIFIED  matches TS `eachTerminalSuccessor` (does not include fallthroughs, correct).14- `GotoVariant::Break` usage in `remove_unnecessary_try_catch` and `remove_dead_do_while_statements`: VERIFIED  matches TS.15- All collection types migrated to `IndexMap`/`IndexSet` (done).1617**Known issues  remaining:**18- Debug output format: TS and Rust debug printers produce different output formats. Both need to converge on Rust `Debug`-style nested format. This will be addressed when the Rust lowering is implemented and output comparison becomes possible.19- TS debug printer collects identifiers/functions per-function; should print all from environment (matching Rust). Requires access to the Environment from TS, which is not currently exposed through the logger API.20- Rust binary config: `Environment::new()` needs matching config (`compilationMode: "all"`, `target: "19"`, etc.)  requires adding config support to the Rust Environment type.21- Error format output between TS and Rust has not been validated for byte-identical output. Will be validated when lowering produces real output.2223---2425## Overview2627```28                                fixture.js29                                    30                 ┌──────────────────┴──────────────────┐31                                                       32        TS test binary                     @babel/parser ──> AST JSON33        (parse with Babel,                                 + Scope JSON34         compile up to                                        35         target pass)                                         36                                                     Rust test binary37                                                     (compile up to38                                                      target pass)39                                                          40           TS debug output                          Rust debug output41                                                          42                 └──────────────── diff ───────────────────┘43```4445A single entrypoint script discovers fixtures, runs both the TS and Rust binaries on each fixture, and diffs their output. The inputs differ slightly: the TS binary takes the original fixture path (parsing with Babel internally, since the TS compiler expects a Babel `NodePath`), while the Rust binary takes pre-parsed AST JSON + Scope JSON. Both produce the same detailed debug representation of the compiler state after the target pass.4647---4849## Entrypoint5051### `compiler/scripts/test-rust-port.sh <pass> [<dir>]`5253```bash54#!/bin/bash55set -e5657PASS="$1"        # Required: name of the compiler pass to run up to58DIR="$2"         # Optional: fixture root directory (default: compiler fixtures)5960# 1. Parse fixtures into AST JSON + Scope JSON (reuses existing scripts)61# 2. Build TS test binary (if needed)62# 3. Build Rust test binary (cargo build)63# 4. For each fixture:64#    a. Run TS binary:   node compiler/scripts/ts-compile-fixture.mjs <pass> <fixture.js>65#    b. Run Rust binary:  compiler/target/debug/test-rust-port <pass> <ast.json> <scope.json>66#    c. Diff the outputs67# 5. Report results (pass/fail counts, first N diffs)68```6970**Arguments:**71- `<pass>`  The name of the compiler pass to run up to. Uses the same names as the `log()` calls in Pipeline.ts (e.g., `HIR`, `SSA`, `InferTypes`, `InferMutationAliasingEffects`). See [Pass Names](#pass-names) below.72- `[<dir>]`  Optional root directory of fixtures. Scans for `**/*.{js,jsx,ts,tsx}` files. Defaults to `compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures`.7374**Output format:** Same style as `test-babel-ast.sh`  show the first 5 failures with colored unified diffs (using `diff` or the `similar` crate pattern), then a summary count. Example:7576```77Testing 1714 fixtures up to pass: InferTypes7879FAIL compiler/simple.js80--- TypeScript81+++ Rust82@@ -3,7 +3,7 @@83   bb0 (block):84     [1] $0:T = LoadGlobal global:console85-    [2] $1:TFunction<BuiltInConsoleLog> = PropertyLoad $0.log86+    [2] $1:T = PropertyLoad $0.log8788... (first 50 lines of diff)8990Results: 1710 passed, 4 failed (1714 total)91```9293---9495## Pass Names9697These are the valid `<pass>` arguments, matching the `log()` name strings in Pipeline.ts. The test binaries run all passes up to and including the named pass.9899### HIR Phase100101| Pass Name | Pipeline.ts Function |102|-----------|---------------------|103| `HIR` | `lower()` |104| `PruneMaybeThrows` | `pruneMaybeThrows()` (first call) |105| `DropManualMemoization` | `dropManualMemoization()` |106| `InlineIIFEs` | `inlineImmediatelyInvokedFunctionExpressions()` |107| `MergeConsecutiveBlocks` | `mergeConsecutiveBlocks()` |108| `SSA` | `enterSSA()` |109| `EliminateRedundantPhi` | `eliminateRedundantPhi()` |110| `ConstantPropagation` | `constantPropagation()` |111| `InferTypes` | `inferTypes()` |112| `OptimizePropsMethodCalls` | `optimizePropsMethodCalls()` |113| `AnalyseFunctions` | `analyseFunctions()` |114| `InferMutationAliasingEffects` | `inferMutationAliasingEffects()` |115| `OptimizeForSSR` | `optimizeForSSR()` |116| `DeadCodeElimination` | `deadCodeElimination()` |117| `PruneMaybeThrows2` | `pruneMaybeThrows()` (second call) |118| `InferMutationAliasingRanges` | `inferMutationAliasingRanges()` |119| `InferReactivePlaces` | `inferReactivePlaces()` |120| `RewriteInstructionKinds` | `rewriteInstructionKindsBasedOnReassignment()` |121| `InferReactiveScopeVariables` | `inferReactiveScopeVariables()` |122| `MemoizeFbtOperands` | `memoizeFbtAndMacroOperandsInSameScope()` |123| `NameAnonymousFunctions` | `nameAnonymousFunctions()` |124| `OutlineFunctions` | `outlineFunctions()` |125| `AlignMethodCallScopes` | `alignMethodCallScopes()` |126| `AlignObjectMethodScopes` | `alignObjectMethodScopes()` |127| `PruneUnusedLabelsHIR` | `pruneUnusedLabelsHIR()` |128| `AlignReactiveScopesToBlockScopes` | `alignReactiveScopesToBlockScopesHIR()` |129| `MergeOverlappingReactiveScopes` | `mergeOverlappingReactiveScopesHIR()` |130| `BuildReactiveScopeTerminals` | `buildReactiveScopeTerminalsHIR()` |131| `FlattenReactiveLoops` | `flattenReactiveLoopsHIR()` |132| `FlattenScopesWithHooksOrUse` | `flattenScopesWithHooksOrUseHIR()` |133| `PropagateScopeDependencies` | `propagateScopeDependenciesHIR()` |134135### Reactive Phase136137| Pass Name | Pipeline.ts Function |138|-----------|---------------------|139| `BuildReactiveFunction` | `buildReactiveFunction()` |140| `PruneUnusedLabels` | `pruneUnusedLabels()` |141| `PruneNonEscapingScopes` | `pruneNonEscapingScopes()` |142| `PruneNonReactiveDependencies` | `pruneNonReactiveDependencies()` |143| `PruneUnusedScopes` | `pruneUnusedScopes()` |144| `MergeReactiveScopesThatInvalidateTogether` | `mergeReactiveScopesThatInvalidateTogether()` |145| `PruneAlwaysInvalidatingScopes` | `pruneAlwaysInvalidatingScopes()` |146| `PropagateEarlyReturns` | `propagateEarlyReturns()` |147| `PruneUnusedLValues` | `pruneUnusedLValues()` |148| `PromoteUsedTemporaries` | `promoteUsedTemporaries()` |149| `ExtractScopeDeclarationsFromDestructuring` | `extractScopeDeclarationsFromDestructuring()` |150| `StabilizeBlockIds` | `stabilizeBlockIds()` |151| `RenameVariables` | `renameVariables()` |152| `PruneHoistedContexts` | `pruneHoistedContexts()` |153| `Codegen` | `codegenFunction()` |154155---156157## TS Test Binary158159### `compiler/scripts/ts-compile-fixture.mjs`160161A Node.js script that takes the original fixture path, parses it with Babel, and runs the compiler pipeline up to the target pass. It uses the real Babel `NodePath` and the existing `lower()` function directly  no JSON intermediary on the TS side.162163**Interface:**164```165node compiler/scripts/ts-compile-fixture.mjs <pass> <fixture-path>166```167168**Outputs to stdout:**169- On success: detailed debug representation of the HIR or ReactiveFunction, including outlined functions (see [Debug Output Format](#debug-output-format))170- On error (thrown CompilerError): formatted error with full diagnostic details171- On accumulated errors (env has errors at the target pass): formatted accumulated errors  these take priority over the debug HIR output172173**Implementation approach:**174175```typescript176import { parse } from '@babel/parser';177import traverse from '@babel/traverse';178import { lower } from '../packages/babel-plugin-react-compiler/src/HIR/BuildHIR';179// ... import all passes180181function main() {182  const [pass, fixturePath] = process.argv.slice(2);183  const source = fs.readFileSync(fixturePath, 'utf8');184185  // Parse with Babel to get a real NodePath (same as production compiler)186  const ast = parse(source, { sourceType: 'module', plugins: [...], errorRecovery: true });187  let functionPath;188  traverse(ast, {189    'FunctionDeclaration|ArrowFunctionExpression|FunctionExpression'(path) {190      functionPath = path;191      path.stop();192    }193  });194195  const env = createEnvironment(/* default config, with pragma overrides from source */);196197  try {198    const hir = lower(functionPath, env);199    if (pass === 'HIR') {200      if (env.hasErrors()) {201        return printFormattedErrors(env.errors());202      }203      return printDebugHIR(hir, env); // includes outlined functions204    }205206    pruneMaybeThrows(hir);207    if (pass === 'PruneMaybeThrows') {208      if (env.hasErrors()) {209        return printFormattedErrors(env.errors());210      }211      return printDebugHIR(hir, env);212    }213214    // ... each pass in order, with the same pattern:215    //   somePass(hir);216    //   if (pass === 'PassName') {217    //     if (env.hasErrors()) {218    //       return printFormattedErrors(env.errors());219    //     }220    //     return printDebugHIR(hir, env);221    //   }222223  } catch (e) {224    if (e instanceof CompilerError) {225      return printFormattedError(e);226    }227    throw e; // re-throw non-compiler errors228  }229}230```231232**Key design decisions:**2332341. **Independent pipeline**: Does NOT call `runWithEnvironment()`. Implements the pass sequence independently, exactly mirroring the Rust binary. This ensures we're testing the pass behavior, not the pipeline orchestration.2352362. **Fixture path input, real Babel parse**: The TS binary takes the original fixture path and parses it with `@babel/parser` + `@babel/traverse` to get a real `NodePath`  reusing the existing `lower()` directly. This means the TS and Rust sides have slightly different inputs (fixture path vs. AST JSON + Scope JSON), but that's fine: the AST JSON is validated by the step 1 round-trip test, and the shared contract is the debug output format, not the input format.2372383. **Validation passes**: Validation passes that run between transform passes (e.g., `validateContextVariableLValues`, `validateHooksUsage`) are included in the pipeline. If a validation pass records errors or throws, that affects the output. The test compares the full behavior including validation.2392404. **Conditional passes**: Passes behind feature flags (e.g., `enableDropManualMemoization`, `enableJsxOutlining`) use the same default config in both TS and Rust. The config is fixed for testing  not configurable per-fixture (initially). If we later need per-fixture config, the fixture's pragma comment can be parsed.2412425. **Config pragmas**: Parse the first line of the original fixture source for config pragmas (e.g., `// @enableJsxOutlining`), same as the snap test runner does. Apply these to the environment config before running passes. This ensures feature-flag-gated passes are tested correctly.243244---245246## Rust Test Binary247248### `compiler/crates/react_compiler/src/bin/test_rust_port.rs`249250A Rust binary in the main compiler crate that mirrors the TS test binary exactly.251252**Interface:**253```254compiler/target/debug/test-rust-port <pass> <ast.json> <scope.json>255```256257**Same output contract as the TS binary**  identical debug format on stdout.258259**Implementation:**260261```rust262fn main() -> Result<(), Box<dyn Error>> {263    let args: Vec<String> = std::env::args().collect();264    let pass = &args[1];265    let ast_json = fs::read_to_string(&args[2])?;266    let scope_json = fs::read_to_string(&args[3])?;267268    let ast: react_compiler_ast::File = serde_json::from_str(&ast_json)?;269    let scope: react_compiler_ast::ScopeInfo = serde_json::from_str(&scope_json)?;270271    let mut env = Environment::new(/* config matching TS binary: compilationMode="all", target="19", etc. */);272273    match run_pipeline(pass, &ast, &scope, &mut env) {274        Ok(output) => {275            print!("{}", output);276        }277        Err(error) => {278            print!("{}", format_errors(&error));279        }280    }281282    Ok(())283}284285fn run_pipeline(286    target_pass: &str,287    ast: &File,288    scope: &ScopeInfo,289    env: &mut Environment,290) -> Result<String, CompilerError> {291    let mut hir = lower(ast, scope, env)?;292    if target_pass == "HIR" {293        if env.has_errors() {294            return Ok(format_errors(env.errors()));295        }296        return Ok(debug_hir(&hir, env)); // includes outlined functions297    }298299    prune_maybe_throws(&mut hir);300    if target_pass == "PruneMaybeThrows" {301        if env.has_errors() {302            return Ok(format_errors(env.errors()));303        }304        return Ok(debug_hir(&hir, env));305    }306307    // ... each pass in order, with the same pattern:308    //   some_pass(&mut hir, env)?;309    //   if target_pass == "PassName" {310    //       if env.has_errors() {311    //           return Ok(format_errors(env.errors()));312    //       }313    //       return Ok(debug_hir(&hir, env));314    //   }315}316```317318**Crate structure**: The test binary lives in whatever crate contains the compiler pipeline (likely `react_compiler` or similar  to be created as passes are ported). It depends on `react_compiler_ast` for the input types.319320---321322## Debug Output Format323324### Why Not PrintHIR325326The existing `PrintHIR.ts` omits important details:327- Mutable ranges hidden when `end <= start + 1`328- `DEBUG_MUTABLE_RANGES` flag defaults to `false`329- Type information omitted for unresolved types330- Source locations not printed331- UnaryExpression doesn't print operator332- Scope details minimal (just `_@scopeId` suffix)333- DeclarationId not printed334- Identifier's full type structure not shown335336For port validation, we need a representation that prints **everything**  similar to Rust's `#[derive(Debug)]` output. Every field of every identifier, every scope, every instruction must be visible so any divergence between TS and Rust is immediately caught.337338### Debug HIR Format339340A structured text format that prints every field of the HIR, **including outlined functions**. Both TS and Rust must produce byte-identical output for the same HIR state. The format uses **Rust `Debug` trait style**  nested struct/enum formatting with curly braces and named fields.341342**Design principles:**343- **Rust `Debug`-style format**: Output looks like Rust's `#[derive(Debug)]` output — `StructName { field: value, ... }` for structs, `EnumVariant { ... }` for enum variants344- Print every field, even defaults/empty values (no elision)345- Deterministic ordering (blocks in RPO, instructions in order, maps by sorted key)346- Stable identifiers (use numeric IDs, not memory addresses)347- Indent with 2 spaces for nesting348- Include all identifiers from the environment (not just those referenced in the function)349- Include all outlined functions from the environment (not just those referenced in the function), each printed with the same format, numbered sequentially (`Function #0`, `Function #1`, etc.)350351**Example output after `InferTypes`:**352353```354Function #0:355  HirFunction {356    id: "example",357    params: [358      Place { identifier: $3, effect: Read, reactive: false, loc: 1:20-1:21 },359    ],360    returns: Place { identifier: $0, effect: Read, reactive: false, loc: 0:0-0:0 },361    returnTypeAnnotation: None,362    context: [],363    aliasing_effects: None,364  }365366  Identifiers:367    $0: Identifier { id: 0, declaration_id: None, name: None, mutable_range: [0, 0], scope: None, type: Type, loc: 0:0-0:0 }368    $1: Identifier { id: 1, declaration_id: 0, name: Some("x"), mutable_range: [1, 5], scope: None, type: TFunction(BuiltInArray), loc: 1:20-1:21 }369    ...370371  Blocks:372    bb0 (block):373      preds: []374      phis: []375      instructions:376        Instruction { id: EvaluationOrder(1), lvalue: Place { identifier: $1, effect: Mutate, reactive: false, loc: 1:0-1:10 }, value: LoadGlobal { name: "console" }, effects: None, loc: 1:0-1:10 }377        ...378      terminal: Return { value: Place { identifier: $2, effect: Read, reactive: false, loc: 5:2-5:10 }, loc: 5:2-5:10 }379```380381Note: This is Rust `Debug`-style formatting. Field names use `snake_case`. Optional values use `None`/`Some(...)`. Enum variants use `VariantName { ... }` or `VariantName(...)` syntax.382383### Debug Reactive Function Format384385Same approach for `ReactiveFunction`  print the full tree structure with all fields visible.386387### Debug Error Format388389When compilation produces errors (thrown or accumulated), output a structured error representation:390391```392Error:393  category: InvalidReact394  severity: InvalidReact395  reason: "Hooks must be called unconditionally"396  description: "Cannot call a hook (useState) conditionally"397  loc: 3:4-3:20398  suggestions: []399  details:400    - severity: InvalidReact401      reason: "This is a conditional"402      loc: 2:2-5:3403```404405All fields of `CompilerDiagnostic` are included  reason, description, loc, severity, category, suggestions (with text + loc), and any nested detail diagnostics.406407### Implementation Strategy408409**TS side**: Create a `debugHIR(hir: HIRFunction, env: Environment): string` function in the test script that walks the HIR and prints everything using Rust `Debug`-style formatting (`StructName { field: value, ... }`). Prints all identifiers and outlined functions from the environment (not just those referenced by the function). This is NOT a modification to the existing `PrintHIR.ts`  it's a separate debug printer in the test infrastructure. Must also print `returnTypeAnnotation`.410411**Rust side**: Implement a custom `debug_hir()` function that produces Rust `Debug`-style output. While this is similar to `#[derive(Debug)]`, a custom implementation is needed for consistent field ordering and formatting. Prints all identifiers and functions from the environment.412413**Shared format specification**: The format is defined once (in this document) and both sides implement it. The round-trip test validates they produce identical output. Both sides must print `returnTypeAnnotation`.414415---416417## Error Handling in Test Binaries418419Both test binaries handle errors uniformly: every pass checkpoint (each `if (pass === ...)` check) first inspects the environment for accumulated errors. If errors are present, the formatted errors are returned **instead of** the debug HIR. This ensures that error output is always comparable between TS and Rust.420421### Thrown Errors (try/catch in TS, Result::Err in Rust)422423- `CompilerError.invariant()`  truly unexpected state424- `CompilerError.throwTodo()`  unsupported but known pattern425- `CompilerError.throw*()`  other throwing methods426427In TS, the entire pipeline is wrapped in a `try/catch`. When a `CompilerError` is caught, the test binary prints the formatted error. Non-`CompilerError` exceptions re-throw (test binary crashes with non-zero exit code, treated as a test failure).428429In Rust, passes return `Result<_, CompilerDiagnostic>`. The `Err` case is handled at the top level by printing the formatted error. Panics (e.g., from `.unwrap()`) crash the binary with a non-zero exit code, treated as a test failure.430431### Accumulated Errors (env.hasErrors())432433Errors recorded via `env.recordError()` / `env.logErrors()` accumulate on the environment. At every pass checkpoint, the test binary checks `env.hasErrors()` **before** printing the debug HIR. If errors are present, the formatted error list is printed instead of the HIR  the pipeline does not continue past the target pass when errors exist.434435This means each pass checkpoint follows the same pattern:436437```438run_pass(hir);439if target_pass == "PassName":440    if env.has_errors():441        return format_errors(env.errors())   // errors take priority442    return debug_hir(hir, env)               // no errors → print HIR443```444445### Comparison Rules4464471. If TS throws and Rust returns Err: compare the formatted error output4482. If TS succeeds and Rust succeeds: compare the debug HIR/reactive output (including outlined functions)4493. If TS throws and Rust succeeds (or vice versa): test fails (mismatch)4504. If TS has accumulated errors and Rust doesn't (or vice versa): test fails4515. If both have accumulated errors at the same pass: compare the formatted error lists452453---454455## Fixture Discovery456457The test script scans the fixture directory for `**/*.{js,jsx,ts,tsx}` files, matching the pattern used by `test-babel-ast.sh`. For each fixture:4584591. Parse with Babel to produce AST JSON + Scope JSON (reusing `babel-ast-to-json.mjs` and `babel-scope-to-json.mjs`)4602. Skip fixtures that fail to parse (`.parse-error` marker)4613. Run both TS and Rust binaries4624. Diff outputs463464**Fixture paths**: The test script passes the original fixture path to the TS binary (which handles its own parsing) and the pre-parsed AST/Scope JSON paths to the Rust binary.465466---467468## Input Asymmetry: Fixture Path vs. AST JSON469470The TS and Rust test binaries take different inputs:471472- **TS binary**: Takes the original fixture path. Parses with `@babel/parser`, runs `@babel/traverse` to build scope info, and calls the existing `lower()` with a real Babel `NodePath`. This is the simplest approach  `lower()` is deeply entangled with Babel's `NodePath` API (`path.get()`, `path.scope.getBinding()`, etc.), so reusing it directly avoids reimplementing those dependencies.473474- **Rust binary**: Takes pre-parsed AST JSON + Scope JSON (produced by the step 1 infrastructure). Deserializes into `react_compiler_ast::File` and `ScopeInfo`, then calls a Rust `lower()` that works with these types directly  no Babel dependency.475476This asymmetry is intentional and acceptable:4771. The AST JSON round-trip is already validated by step 1 (1714/1714 fixtures pass), so the Rust side sees the same AST data that Babel produced.4782. The shared contract between the two sides is the **debug output format**, not the input format.4793. Keeping the TS side on real Babel `NodePath`s means we're comparing against the production compiler's actual behavior, not a reimplementation of its input handling.480481---482483## Implementation Plan484485### M1: Debug Output Format + TS Test Binary486487**Goal**: Get the TS side working end-to-end so we have a reference output for every fixture at every pass.4884891. **Define the debug output format**  Write a precise specification for the text format. Create a `DebugPrintHIR.ts` module in `compiler/scripts/` (test infrastructure, not compiler source) that implements the format.4904912. **Define the debug error format**  Specify exact formatting for `CompilerDiagnostic` objects, including all fields.4924933. **Create `compiler/scripts/ts-compile-fixture.mjs`**  The TS test binary. Takes `<pass> <fixture-path>` and produces debug output. Parses the fixture source with Babel to get a real `NodePath`, runs passes up to the target, prints debug output.4944954. **Validate the TS binary**  Run it on all fixtures at several pass points (`HIR`, `SSA`, `InferTypes`, `InferMutationAliasingEffects`, `InferMutationAliasingRanges`) and verify the output is sensible and deterministic (running twice produces identical output).496497### M2: Shell Script + Diff Infrastructure498499**Goal**: The test script runs the TS binary on all fixtures and produces output files. Later, when Rust passes are implemented, it will also run the Rust binary and diff.5005011. **Create `compiler/scripts/test-rust-port.sh`**  The entrypoint script. Initially only runs the TS side (Rust passes don't exist yet). Supports `<pass>` and `[<dir>]` arguments.5025032. **Diff formatting**  Implement colored unified diff output, similar to `test-babel-ast.sh`. Show first 5 failures with diffs, then summary counts.5045053. **Exit codes**  Exit 0 on all pass, non-zero on any failure. Useful for CI integration.506507### M3: Rust Test Binary Scaffold508509**Goal**: Scaffold the Rust binary and a `todo!`-only stub for `lower()` so the end-to-end test loop works immediately  even though every test will fail. This validates the full test infrastructure (fixture discovery, Rust binary invocation, diff output) before any real porting begins.5105111. **Create the Rust compiler crate**  `compiler/crates/react_compiler/` with the binary target `test-rust-port`. Depends on `react_compiler_ast` for input types.5125132. **Stub `lower()`**  Create a `lower()` function with the correct signature that immediately calls `todo!("lower not yet implemented")`. This means the Rust binary will panic for every fixture, producing a non-zero exit code. The test script treats this as a test failure (expected at this stage).5145153. **Stub pipeline**  The `run_pipeline()` function calls the stubbed `lower()` and has placeholder match arms for all other pass names. Every pass beyond `lower()` also hits `todo!()`.5165174. **Implement `debug_hir()`**  Rust debug printer matching the TS format exactly. This won't be exercised until `lower()` is real, but having it in place means the first real pass port immediately produces diffable output.5185195. **Implement `debug_error()`**  Rust error printer matching the TS format.5205216. **Integrate into `test-rust-port.sh`**  Run both TS and Rust binaries, diff outputs. At this stage, **all tests are expected to fail** (Rust panics on `todo!()`). The test script should report the failure count and distinguish between "Rust panicked" vs "output mismatch" failures:522523   ```524   Testing 1714 fixtures up to pass: HIR525526   Results: 0 passed, 1714 failed (1714 total)527     1714 rust panicked (todo!), 0 output mismatch528   ```529530   This confirms the infrastructure works end-to-end. As `lower()` and subsequent passes are implemented, the "rust panicked" count drops and "passed" / "output mismatch" counts rise.531532**Why stub with `todo!()` now**: The goal of this phase is to validate the test infrastructure itself, not the compiler port. By having a Rust binary that compiles and runs (but panics), we prove that fixture discovery, AST JSON passing, Rust binary invocation, and diff reporting all work correctly. When the real `lower()` port begins (step 4+), the developer can immediately see their progress reflected in the test results without any infrastructure work.533534### M4: Ongoing  Per-Pass Validation535536As each pass is ported to Rust, replace the `todo!()` stub with a real implementation:5375381. Replace the `todo!()` in the pass with a real implementation5392. Run `test-rust-port.sh <pass>` to compare TS and Rust output5403. Fix any differences until all (or nearly all) fixtures pass5414. Move to the next pass542543The first pass to port is `lower()`. Once it's real, fixtures at the `HIR` pass will transition from "rust panicked" to either "passed" or "output mismatch". The test infrastructure is complete after M3 — M4 is the ongoing usage pattern.544545---546547## File Layout548549```550compiler/551  scripts/552    test-rust-port.sh              # Entrypoint script553    ts-compile-fixture.mjs         # TS test binary554    debug-print-hir.mjs            # Debug HIR printer (TS)555    debug-print-reactive.mjs       # Debug ReactiveFunction printer (TS)556    debug-print-error.mjs          # Debug error printer (TS)557  crates/558    react_compiler/559      Cargo.toml560      src/561        bin/562          test_rust_port.rs        # Rust test binary563        lib.rs564        debug_print.rs             # Debug HIR/Reactive/Error printer (Rust)565        pipeline.rs                # Pipeline runner (pass-by-pass)566    react_compiler_hir/567      Cargo.toml568      src/569        lib.rs                     # HIR types570        environment.rs             # Environment type571    react_compiler_lowering/572      Cargo.toml573      src/574        lib.rs                     # pub fn lower() entry point575        build_hir.rs               # Lowering functions576        hir_builder.rs             # HIRBuilder struct577    react_compiler_diagnostics/578      Cargo.toml579      src/580        lib.rs                     # CompilerError, CompilerDiagnostic, etc.581    react_compiler_ast/            # Existing AST crate (from step 1)582      ...583```584585---586587## TS Binary: Parsing Strategy588589The TS test binary parses the original fixture source with `@babel/parser` and `@babel/traverse`, then calls the existing `lower()` with the real `NodePath`. This ensures the TS reference output is 100% faithful to what the production compiler would produce. Any differences in the Rust side's HIR output reveal bugs in the Rust lowering — not artifacts of a reimplemented TS input layer.590591---592593## Configuration594595Both test binaries use the **same configuration**. This includes `compilationMode: "all"`, `target: "19"`, and other settings that ensure both sides produce comparable output, plus any overrides from pragma comments in the fixture source.596597**Pragma parsing**: The first line of each fixture may contain config pragmas like `// @enableJsxOutlining @enableNameAnonymousFunctions:false`. Both test binaries parse this line and apply the overrides before running passes.598599**TS side**: Reuse the existing pragma parser from the snap test runner.600601**Rust side**: Implement a simple pragma parser that produces the same config. Initially, before the Rust pragma parser is built, use a fixed default config and skip fixtures with non-default pragmas (or have the TS binary output the resolved config as a JSON header that the Rust binary can consume).602603---604605## Determinism Requirements606607For the diff to be meaningful, both test binaries must be fully deterministic:6086091. **Map/Set iteration order**: TS uses insertion-order Maps and Sets. Rust should use `IndexMap`/`IndexSet` (from the `indexmap` crate) for insertion-order maps and sets, matching TS's insertion-order `Map` and `Set`. The debug printer must sort by key (block IDs, identifier IDs, scope IDs) before printing.6106112. **ID assignment**: Both sides must assign the same IDs (IdentifierId, BlockId, ScopeId) in the same order. This is ensured by following the same pipeline logic.6126133. **Floating point**: Avoid floating point in debug output. All numeric values are integers (IDs, ranges, line/column numbers).6146154. **Source locations**: Print locations as `line:column-line:column`. Both sides read the same source locations from the AST JSON.616617---618619## Scope and Non-Goals620621### In Scope622- Testing every pass from `lower` through `codegen`623- HIR debug output comparison624- ReactiveFunction debug output comparison625- Error output comparison (thrown and accumulated)626- Support for custom fixture directories627- Config pragma support628629### Not In Scope (Initially)630- Performance benchmarking (separate effort)631- Testing the Babel plugin integration (the Rust compiler is a standalone binary)632- Testing codegen output (the `Codegen` pass produces a Babel AST, which is tested by comparing its debug representation  not by running the generated code)633- Parallel test execution (run fixtures sequentially initially; parallelize later if needed)634- Watch mode

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.