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.