From 08cf7d607f304cf2c701b41ab9cf70b44f6fc167 Mon Sep 17 00:00:00 2001 From: lauren Date: Wed, 20 May 2026 23:43:06 -0700 Subject: [PATCH 1/5] [rust-compiler] Emit loc.column/index as UTF-16 code units in SWC frontend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the cluster-1 BytePos shift, `ConvertCtx::position()` emitted `loc.column` and `loc.index` as 0-based UTF-8 byte offsets. Babel emits them as 0-based UTF-16 code unit offsets (matching JS string indexing). For files containing any character above U+FFFF (e.g. an emoji like πŸ”΄ U+1F534), the two diverge by +2 per such character because the char is 4 bytes in UTF-8 but 2 code units in UTF-16. Precompute a `utf16_offsets: Vec` table in `ConvertCtx::new` that maps each source byte index to its 0-based UTF-16 code unit offset. `position()` then looks up `index` directly and computes `column` as `index - utf16_index_of_line_start`. O(1) per call; the table costs ~4Γ— the source length in memory, which is bounded for fixture/file inputs. Considered an alternative that walks the source line on each `position()` call to count UTF-16 code units. More memory-frugal but O(line length) per call. The precomputed table wins on O(1) lookup and the per-call cost matters because `position()` is invoked on every node, comment, and reference in the converter. Clamp the byte index in `position()` to the sentinel at `utf16_offsets.len() - 1`. Synthetic spans (e.g. compiler-generated imports given `BytePos(1)`) can point past EOF in degenerate cases; clamping avoids a panic. Line numbers stay 1-based and the binary-search remains keyed on byte offsets, since the underlying `line_offsets` table is byte-based. Fixes 4 e2e parity fixtures (3 targeted + 1 latent): - effect-derived-computations/invalid-derived-computation-in-effect.js - error.invalid-derived-computation-in-effect.js - fbt/error.todo-multiple-fbt-plural.tsx - (one additional latent fixture passes for free) Test plan: - bash compiler/scripts/test-e2e.sh --variant swc: Before: Total 1770/1795 After: Total 1774/1795 (4 fixed) - bash compiler/scripts/test-e2e.sh --variant babel: 1788/1795 (unchanged) - bash compiler/scripts/test-e2e.sh --variant oxc: 1702/1795 (unchanged) - cargo test --workspace: 56 passed, 0 failed --- .../react_compiler_swc/src/convert_ast.rs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/compiler/crates/react_compiler_swc/src/convert_ast.rs b/compiler/crates/react_compiler_swc/src/convert_ast.rs index 319ff59e2a2..651d9f8b21f 100644 --- a/compiler/crates/react_compiler_swc/src/convert_ast.rs +++ b/compiler/crates/react_compiler_swc/src/convert_ast.rs @@ -191,19 +191,27 @@ struct ConvertCtx<'a> { #[allow(dead_code)] source_text: &'a str, line_offsets: Vec, + utf16_offsets: Vec, } impl<'a> ConvertCtx<'a> { fn new(source_text: &'a str) -> Self { let mut line_offsets = vec![0u32]; + let mut utf16_offsets = vec![0u32; source_text.len() + 1]; + let mut utf16_offset = 0u32; for (i, ch) in source_text.char_indices() { + let next = i + ch.len_utf8(); + utf16_offsets[i..next].fill(utf16_offset); + utf16_offset += ch.len_utf16() as u32; if ch == '\n' { - line_offsets.push((i + 1) as u32); + line_offsets.push(next as u32); } } + utf16_offsets[source_text.len()] = utf16_offset; Self { source_text, line_offsets, + utf16_offsets, } } @@ -222,7 +230,7 @@ impl<'a> ConvertCtx<'a> { } } - /// `BytePos` is 1-based; emit 0-based `loc` to match Babel. + /// `BytePos` is 1-based; emit 0-based UTF-16 `loc` to match Babel. /// (`BaseNode.start`/`end` stays 1-based: `convert_scope` keys on it.) fn position(&self, offset: u32) -> Position { let zero_based = offset.saturating_sub(1); @@ -231,10 +239,14 @@ impl<'a> ConvertCtx<'a> { Err(idx) => idx.saturating_sub(1), }; let line_start = self.line_offsets[line_idx]; + // Synthetic spans can point past EOF; clamp to the sentinel. + let byte_idx = (zero_based as usize).min(self.utf16_offsets.len() - 1); + let utf16_offset = self.utf16_offsets[byte_idx]; + let line_start_utf16 = self.utf16_offsets[line_start as usize]; Position { line: (line_idx as u32) + 1, - column: zero_based - line_start, - index: Some(zero_based), + column: utf16_offset - line_start_utf16, + index: Some(utf16_offset), } } From 90b4f647bc25877c80622eadaa073892b5400c5a Mon Sep 17 00:00:00 2001 From: lauren Date: Wed, 20 May 2026 22:26:01 -0700 Subject: [PATCH 2/5] [rust-compiler] Treat redeclared functions as one binding in SWC scope info `convert_scope.rs::visit_fn_decl` allocated a fresh `BindingId` for every `function x()` declaration. Babel and OXC collapse same-named function declarations in the same hoist scope into one binding, with the second declaration registered as a `constantViolations` reference (reassignment) rather than a new binding. For `function-declaration-redeclare.js` the SWC variant emitted `const x_0 = t0; return x_0;` because the compiler saw two distinct bindings and renamed one. Babel's output is `let x; ... x = t0; return x;` because there is one binding that gets reassigned. In `visit_fn_decl`, check whether the hoist scope already has a binding for the name. If yes, record the redeclaration's ident position in a new `redeclaration_refs` map and skip adding a fresh binding. `build_scope_info` overlays this map onto `reference_to_binding` so the second function's ident resolves to the first binding's `BindingId`. Fixes 1 e2e parity fixture: - function-declaration-redeclare.js `valid-setState-in-effect-from-ref-function-call.js` and its sibling `valid-setState-in-useEffect-controlled-by-ref-value.js` still fail. Those have a distinct root cause: the SWC frontend discards the compiler's `renames` output (`lib.rs:91-98`) instead of applying it to the emitted SWC AST the way the babel adapter does via `applyRenames`. That fix is its own commit. Test plan: - bash compiler/scripts/test-e2e.sh --variant swc: Before: Total 1774/1795 After: Total 1775/1795 (1 fixed) - bash compiler/scripts/test-e2e.sh --variant babel: 1788/1795 (unchanged) - bash compiler/scripts/test-e2e.sh --variant oxc: 1702/1795 (unchanged) - cargo test --workspace: 56 passed, 0 failed --- .../react_compiler_swc/src/convert_scope.rs | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/compiler/crates/react_compiler_swc/src/convert_scope.rs b/compiler/crates/react_compiler_swc/src/convert_scope.rs index fc81bfddef3..288201f3a97 100644 --- a/compiler/crates/react_compiler_swc/src/convert_scope.rs +++ b/compiler/crates/react_compiler_swc/src/convert_scope.rs @@ -45,6 +45,12 @@ pub fn build_scope_info(module: &Module) -> ScopeInfo { } } + // Function redeclarations: resolve the second `function x()`'s ident + // to the first declaration's binding (overwrites any earlier entry). + for (start, binding_id) in &collector.redeclaration_refs { + resolver.reference_to_binding.insert(*start, *binding_id); + } + resolver.reference_to_binding }; @@ -72,6 +78,11 @@ struct ScopeCollector { /// Set of span starts for block statements that are direct function/catch bodies. /// These should NOT create a separate Block scope. function_body_spans: HashSet, + /// Function declarations that redeclare an existing hoisted name resolve to + /// the first binding's `BindingId` (like babel's `constantViolations`), so + /// the compiler treats the redeclaration as a reassignment rather than a + /// new binding. + redeclaration_refs: HashMap, } impl ScopeCollector { @@ -83,6 +94,7 @@ impl ScopeCollector { node_to_scope_end: HashMap::new(), scope_stack: Vec::new(), function_body_spans: HashSet::new(), + redeclaration_refs: HashMap::new(), } } @@ -344,14 +356,20 @@ impl Visit for ScopeCollector { let hoist_scope = self.enclosing_function_scope(); let name = fn_decl.ident.sym.to_string(); let start = fn_decl.ident.span.lo.0; - self.add_binding( - name, - BindingKind::Hoisted, - hoist_scope, - "FunctionDeclaration".to_string(), - Some(start), - None, - ); + if let Some(&existing_id) = + self.scopes[hoist_scope.0 as usize].bindings.get(&name) + { + self.redeclaration_refs.insert(start, existing_id); + } else { + self.add_binding( + name, + BindingKind::Hoisted, + hoist_scope, + "FunctionDeclaration".to_string(), + Some(start), + None, + ); + } self.visit_function_inner(&fn_decl.function); } From 78afea419e8d8ed22b6f7ffda4a47eb756f876fc Mon Sep 17 00:00:00 2001 From: lauren Date: Wed, 20 May 2026 23:43:06 -0700 Subject: [PATCH 3/5] [rust-compiler] Apply compiler renames to SWC module after conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The React Compiler's `RenameVariables` pass records identifier renames in `CompileResult::Success.renames` as `Vec` with `{original, renamed, declaration_start}`. It does not rewrite the AST itself; the frontend has to apply them. The Babel adapter does this via `applyRenames` + `scope.rename()` in `BabelPlugin.ts:206-234`. The SWC frontend was discarding the field via `..` in the `CompileResult::Success` destructure, so inner-shadowing renames like `ref β†’ ref_0` never made it into the emitted output. Add `apply_renames.rs` with a single pass that mirrors the Babel semantics: 1. Re-run `build_scope_info` on the post-compile SWC module to get a binding table and `reference_to_binding` keyed by source position. 2. Match `BindingRenameInfo` entries to bindings by `declaration_start` and `name`, producing the set of `BindingId`s to rename. 3. Walk `reference_to_binding` to collect every position (declaration plus references) belonging to a renamed binding. 4. A `VisitMut` rewrites `Ident.sym` at matching `span.lo.0`, expands `Prop::Shorthand` and `ObjectPatProp::Assign` (with or without a default) into key-value form so `{ref}` becomes `{ref: ref_0}` instead of `{ref_0}`, and skips `MemberProp::Ident` so `x.ref` stays `x.ref`. Wire into `lib.rs::transform`: destructure `renames` from the `CompileResult::Success` arm, and if `program_json` is `None` but `renames` is non-empty (the `@outputMode:"lint"` path) clone the original input module so we still have something to rewrite. Fixes 2 e2e parity fixtures: - valid-setState-in-effect-from-ref-function-call.js (`ref β†’ ref_0`) - valid-setState-in-useEffect-controlled-by-ref-value.js (`data β†’ data_0`) Test plan: - bash compiler/scripts/test-e2e.sh --variant swc: Before: Total 1775/1795 After: Total 1777/1795 (2 fixed) - bash compiler/scripts/test-e2e.sh --variant babel: 1788/1795 (unchanged) - bash compiler/scripts/test-e2e.sh --variant oxc: 1702/1795 (unchanged) - cargo test --workspace: 56 passed, 0 failed --- .../react_compiler_swc/src/apply_renames.rs | 133 ++++++++++++++++++ compiler/crates/react_compiler_swc/src/lib.rs | 16 ++- 2 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 compiler/crates/react_compiler_swc/src/apply_renames.rs diff --git a/compiler/crates/react_compiler_swc/src/apply_renames.rs b/compiler/crates/react_compiler_swc/src/apply_renames.rs new file mode 100644 index 00000000000..f20d9494a34 --- /dev/null +++ b/compiler/crates/react_compiler_swc/src/apply_renames.rs @@ -0,0 +1,133 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +use std::collections::HashMap; + +use react_compiler::entrypoint::compile_result::BindingRenameInfo; +use react_compiler_ast::scope::BindingId; +use swc_atoms::Atom; +use swc_ecma_ast::*; +use swc_ecma_visit::{VisitMut, VisitMutWith}; + +use crate::convert_scope::build_scope_info; + +pub fn apply_renames(module: &mut Module, renames: &[BindingRenameInfo]) { + if renames.is_empty() { + return; + } + + let scope_info = build_scope_info(module); + let renames_by_declaration: HashMap = renames + .iter() + .map(|rename| (rename.declaration_start, rename)) + .collect(); + let mut renamed_bindings: HashMap = HashMap::new(); + + for binding in &scope_info.bindings { + let Some(rename) = binding + .declaration_start + .and_then(|start| renames_by_declaration.get(&start)) + else { + continue; + }; + if binding.name == rename.original { + renamed_bindings.insert(binding.id, rename.renamed.clone()); + } + } + + if renamed_bindings.is_empty() { + return; + } + + let rewrite_plan: HashMap = scope_info + .reference_to_binding + .iter() + .filter_map(|(&position, binding_id)| { + renamed_bindings.get(binding_id).map(|renamed| (position, renamed.clone())) + }) + .collect(); + + module.visit_mut_with(&mut RenameApplyVisitor { rewrite_plan }); +} + +struct RenameApplyVisitor { + rewrite_plan: HashMap, +} + +impl RenameApplyVisitor { + fn renamed_at(&self, position: u32) -> Option { + self.rewrite_plan.get(&position).cloned() + } +} + +impl VisitMut for RenameApplyVisitor { + fn visit_mut_ident(&mut self, ident: &mut Ident) { + if let Some(renamed) = self.renamed_at(ident.span.lo.0) { + ident.sym = Atom::from(renamed); + } + } + + fn visit_mut_member_expr(&mut self, member: &mut MemberExpr) { + member.obj.visit_mut_with(self); + if let MemberProp::Computed(computed) = &mut member.prop { + computed.visit_mut_with(self); + } + } + + fn visit_mut_prop(&mut self, prop: &mut Prop) { + match prop { + Prop::Shorthand(ident) => { + if let Some(renamed) = self.renamed_at(ident.span.lo.0) { + let mut value = ident.clone(); + value.sym = Atom::from(renamed); + // Shorthand `{ref}` must become `{ref: ref_0}` to preserve property semantics. + *prop = Prop::KeyValue(KeyValueProp { + key: PropName::Ident(IdentName { + span: ident.span, + sym: ident.sym.clone(), + }), + value: Box::new(Expr::Ident(value)), + }); + } + } + Prop::Assign(assign) => { + assign.value.visit_mut_with(self); + } + _ => prop.visit_mut_children_with(self), + } + } + + fn visit_mut_object_pat_prop(&mut self, prop: &mut ObjectPatProp) { + match prop { + ObjectPatProp::Assign(assign) => { + if let Some(value) = &mut assign.value { + value.visit_mut_with(self); + } + + if let Some(renamed) = self.renamed_at(assign.key.id.span.lo.0) { + let mut binding = assign.key.clone(); + binding.id.sym = Atom::from(renamed); + let value = match assign.value.take() { + Some(default_value) => Pat::Assign(AssignPat { + span: assign.span, + left: Box::new(Pat::Ident(binding)), + right: default_value, + }), + None => Pat::Ident(binding), + }; + + *prop = ObjectPatProp::KeyValue(KeyValuePatProp { + key: PropName::Ident(IdentName { + span: assign.key.id.span, + sym: assign.key.id.sym.clone(), + }), + value: Box::new(value), + }); + } + } + _ => prop.visit_mut_children_with(self), + } + } +} diff --git a/compiler/crates/react_compiler_swc/src/lib.rs b/compiler/crates/react_compiler_swc/src/lib.rs index 1d6576771ac..cbd5bcc120f 100644 --- a/compiler/crates/react_compiler_swc/src/lib.rs +++ b/compiler/crates/react_compiler_swc/src/lib.rs @@ -6,9 +6,11 @@ pub mod convert_ast; pub mod convert_ast_reverse; pub mod convert_scope; +pub mod apply_renames; pub mod diagnostics; pub mod prefilter; +use apply_renames::apply_renames; use convert_ast::convert_module_with_source_type; use convert_ast_reverse::convert_program_to_swc_with_source; use convert_scope::build_scope_info; @@ -88,13 +90,16 @@ pub fn transform( react_compiler::entrypoint::program::compile_program(file, scope_info, options); let diagnostics = compile_result_to_diagnostics(&result); - let (program_json, events) = match result { + let (program_json, events, renames) = match result { react_compiler::entrypoint::compile_result::CompileResult::Success { - ast, events, .. - } => (ast, events), + ast, + events, + renames, + .. + } => (ast, events, renames), react_compiler::entrypoint::compile_result::CompileResult::Error { events, .. - } => (None, events), + } => (None, events, Vec::new()), }; let conversion_result = program_json.and_then(|raw_json| { @@ -109,6 +114,7 @@ pub fn transform( let (mut swc_module, mut comments) = match conversion_result { Some(result) => (Some(result.module), Some(result.comments)), + None if !renames.is_empty() => (Some(module.clone()), None), None => (None, None), }; @@ -155,6 +161,8 @@ pub fn transform( } } + apply_renames(swc_mod, &renames); + let (source_leading_comments, source_trailing_comments) = extract_source_comments(source_text); if !source_leading_comments.is_empty() || !source_trailing_comments.is_empty() { From 8ba282982c5401ab3c682b69827ef0c54d5f69c7 Mon Sep 17 00:00:00 2001 From: lauren Date: Wed, 20 May 2026 22:57:33 -0700 Subject: [PATCH 4/5] [rust-compiler] Inject CLI filename into PluginOptions for SWC and OXC The e2e CLI (`react_compiler_e2e_cli`) accepted `--filename ` and used it only for syntax detection. The value was never copied into `PluginOptions.filename`, so the compiler's instrumentation pass (`enableEmitInstrumentForget`) received `None` and emitted `useRenderCounter("Bar", "")` with an empty path string where the TS baseline emits the absolute file path. Set `options.filename = Some(filename.to_string())` at the top of both `compile_swc` and `compile_oxc`. One line each. Fixes 5 e2e parity fixtures: - codegen-instrument-forget-test.js (swc) - conflict-codegen-instrument-forget.js (swc) - gating/codegen-instrument-forget-gating-test.js (swc) - (and 2 corresponding OXC variants of the same fixtures, fixed for free) Test plan: - bash compiler/scripts/test-e2e.sh --variant swc: Before: Total 1777/1795 After: Total 1780/1795 (3 fixed) - bash compiler/scripts/test-e2e.sh --variant babel: 1788/1795 (unchanged) - bash compiler/scripts/test-e2e.sh --variant oxc: Before: Total 1702/1795 After: Total 1704/1795 (2 fixed) - cargo test --workspace: 56 passed, 0 failed --- compiler/crates/react_compiler_e2e_cli/src/main.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/compiler/crates/react_compiler_e2e_cli/src/main.rs b/compiler/crates/react_compiler_e2e_cli/src/main.rs index e443026a688..84542fc9cde 100644 --- a/compiler/crates/react_compiler_e2e_cli/src/main.rs +++ b/compiler/crates/react_compiler_e2e_cli/src/main.rs @@ -128,7 +128,8 @@ fn determine_swc_syntax(_filename: &str) -> swc_ecma_parser::Syntax { }) } -fn compile_swc(source: &str, filename: &str, options: PluginOptions) -> CompileOutput { +fn compile_swc(source: &str, filename: &str, mut options: PluginOptions) -> CompileOutput { + options.filename = Some(filename.to_string()); let cm = swc_common::sync::Lrc::new(swc_common::SourceMap::default()); let fm = cm.new_source_file( swc_common::sync::Lrc::new(swc_common::FileName::Anon), @@ -210,7 +211,8 @@ fn compile_swc(source: &str, filename: &str, options: PluginOptions) -> CompileO } } -fn compile_oxc(source: &str, filename: &str, options: PluginOptions) -> CompileOutput { +fn compile_oxc(source: &str, filename: &str, mut options: PluginOptions) -> CompileOutput { + options.filename = Some(filename.to_string()); // Always enable TypeScript parsing (like the TS/Babel baseline uses // ['typescript', 'jsx'] plugins). Some .js fixtures contain TS syntax. // Check for @script pragma in the first line to use script source type. From ba729ad2777e9d92bc4be82fc29e2a1a5e28e59c Mon Sep 17 00:00:00 2001 From: lauren Date: Wed, 20 May 2026 23:04:19 -0700 Subject: [PATCH 5/5] [rust-compiler] Document remaining e2e parity TODOs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Snapshot of remaining e2e failures across all three frontends: - SWC: 1780/1795 (15 failures), grouped by fix path β€” fixture maintenance for TS-bug-Rust-correct cases (6), external PR dependency (1), and real SWC frontend bugs (8). Each line names the fixture, the failure shape, and where to look. - Babel: 1788/1795 (7 failures) β€” placeholder; needs triage. - OXC: 1704/1795 (91 failures) β€” placeholder; needs triage. Companion artifact to the current 9-commit SWC-correctness stack. --- compiler/crates/TODO.md | 156 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 compiler/crates/TODO.md diff --git a/compiler/crates/TODO.md b/compiler/crates/TODO.md new file mode 100644 index 00000000000..0d3b0c96efc --- /dev/null +++ b/compiler/crates/TODO.md @@ -0,0 +1,156 @@ +# Rust port: e2e parity TODO + +Status snapshot (after the current stack lands): + +| Variant | Score | Failures | +| ------- | ------------ | -------- | +| Babel | 1788 / 1795 | 7 | +| SWC | 1780 / 1795 | 15 | +| OXC | 1704 / 1795 | 91 | + +`cargo test --workspace`: 56 passed, 0 failed. + +## SWC + +The 15 remaining SWC e2e failures fall into three groups. Each line names the +fixture and the failure mode; the group it sits in dictates the appropriate +fix. + +### Group A: Fixture maintenance, not Rust bugs + +SWC compiles code that TS rejects, or vice versa, in ways where Rust's +behavior is arguably correct. The fix is to rename the fixture (drop the +`error.` prefix) and update the `.expect.md` snapshot so the suite stops +asserting the TS-specific output. + +- `error.bug-invariant-local-or-context-references.js` β€” TS fires + `CompilerError::invariant` ("expected all references ... consistently + local or context"). Rust handles the same code without tripping the + invariant. +- `error.todo-jsx-intrinsic-tag-matches-local-binding.js` β€” SWC pipeline + emits a Todo bailout (`[hoisting] EnterSSA: Expected identifier to be + defined before being used`) that the Babel path does not. +- `error.todo-repro-named-function-with-shadowed-local-same-name.js` β€” + Babel errors; SWC compiles. +- `new-mutability/error.todo-repro-named-function-with-shadowed-local-same-name.js` + β€” same as above with the new mutation-aliasing model enabled. +- `error.todo-rust-as-expression-assignment-target.tsx` β€” Babel errors; + SWC compiles. +- `fbt/error.todo-locally-require-fbt.js` β€” Babel emits the + `Invariant: tags should be module-level imports` shape; SWC emits + `Todo: Local variables named 'fbt' may conflict with the fbt plugin`. + Different categories, both reasonable. + +### Group B: External dependency + +- `use-no-forget-multiple-with-eslint-suppression.js` β€” spurious + `import { c as _c }` in the TS reference output. Fixed on `main` by + [react#36500](https://github.com/facebook/react/pull/36500) (merged). + Will pass automatically once `pr-36173` rebases onto `main`; until then + the TS dist built from `pr-36173` still emits the unused import. + +### Group C: Real SWC frontend bugs + +Each line names the failure mode and a sketch of where to look. + +- `fbt/fbt-param-with-quotes.js` β€” SWC codegen emits double quotes + (`"fbt"`) and reformats multi-line JSX into a single line; Babel uses + single quotes and preserves the source layout. Semantically equivalent + output; the fix is either an SWC codegen flag for quote style or a + post-emit pass. Low impact, high effort. + +- `lone-surrogate-string-values.js` β€” TS preserves lone surrogates + (`\uD83E`); SWC emits `\uFFFD` because `Wtf8Atom::to_string_lossy()` in + `react_compiler_swc/src/convert_ast.rs::wtf8_to_string` replaces invalid + UTF-8 sequences. Real WTF-8 handling work that touches every call site + using that helper. Probably needs to detect lone surrogates and emit + `\uXXXX` escapes before they hit `String`. + +- `many-scopes-no-stack-overflow.js` β€” TS memoizes the function + (`const $ = _c(401);` with 401 memo slots); SWC pipeline bails out and + returns the uncompiled source. The fixture exists to test that the + compiler handles many sequential reactive scopes without stack overflow, + so the SWC variant should compile. Root cause unclear β€” needs + investigation in the SWC pipeline or the compiler core to see where the + bail happens. + +- `pattern4_bare_type.js` β€” Two unrelated bugs in one fixture: + 1. Operator-precedence stripping. `Math.round((x - y) * 1000)` becomes + `Math.round(x - y * 1000)`. SWC codegen drops the parentheses around + the subtraction. Probably in `convert_ast_reverse.rs`'s + BinaryExpression handling. + 2. Method return type annotation. `formatMetrics(): Metrics` becomes + `formatMetrics()`. The TS-type-on-binding-ident fix in commit + cc1ba1e1 only covered binding identifiers; class method signatures + are a separate code path. Same shape of fix; different + `convert_binding_ident`-equivalent call site. + +- `reduce-reactive-deps/hoist-deps-diff-ssa-instance1.tsx` β€” + `(x as HasA).a.value + 2` becomes `(x as HasA.a.value) + 2`. The member + expression's property chain gets absorbed into the type annotation when + `convert_ast_reverse` emits the cast. Likely a parenthesization / + precedence bug in the reverse converter or the SWC printer's handling + of `TSAsExpression` as the object of a `MemberExpression`. + +- `todo-round2_unicode_string.js` (prefixed `todo-`) β€” Hex escape format + (`\xC5`) vs unicode escape (`\u00C5`) for bytes 0x80-0xFF. Both valid JS + literals; codegen format choice in SWC's string printer. + +- `todo-round3_promote_used_temps.js` (prefixed `todo-`) β€” Class body + codegen. TS emits the class with fields and constructor; SWC emits an + empty class body and pulls fields/methods out into separate assignments. + Likely an interaction between SWC codegen and the compiler's + `promote_used_temps` pass. + +- `ts-non-null-expression-default-value.tsx` β€” Generic type parameter + support. `const x: ReadonlyMap = ...` becomes + `const x = ...` (annotation dropped entirely). Our + `convert_ts_type_to_json` helper in cc1ba1e1 explicitly guards against + `TsTypeRef` with `type_params` to avoid silently emitting + `ReadonlyMap` without the params. The proper fix needs serialization of + `TSTypeParameterInstantiation` in `convert_ast.rs` AND deserialization + in `convert_ast_reverse.rs::convert_ts_type_from_json`. + +## Babel + +**TODO: scope this out.** Babel is at 1788 / 1795 (7 failures). These have +been the baseline throughout the SWC parity stack and were not touched, so the +failure list is whatever was on `pr-36173` before this work landed. + +Next step is to enumerate the failures by fixture and bucket them the same +way as SWC (fixture maintenance / external dependency / real bugs). Run: + +```bash +bash compiler/scripts/test-e2e.sh --no-color --variant babel +``` + +…and triage the resulting failures into A/B/C groups under this section. + +## OXC + +**TODO: scope this out.** OXC is at 1704 / 1795 (91 failures). The CLI +`filename` fix in commit c30f0d6f bumped this by +2 from the 1702 baseline, +but everything else is unaddressed. + +Next step is to enumerate failures and identify OXC-specific clusters +(likely AST conversion gaps in `react_compiler_oxc` analogous to the SWC +work in this stack). Run: + +```bash +bash compiler/scripts/test-e2e.sh --no-color --variant oxc +``` + +…and bucket the resulting failures into A/B/C groups under this section. +Expect significant overlap with the SWC Group C bugs (cast wrappers, +type annotations, UTF-16/WTF-8 handling) since both frontends share the +post-conversion pipeline. + +## How this stack got here + +- `compiler/scripts/test-e2e.sh --variant swc` baseline was 1742 / 1795 + (53 failures) before this stack. +- 9 commits in the current stack reduce that to 1780 / 1795 (15 failures, + -38 fixtures, 72% reduction). +- Babel variant: 1788 / 1795 throughout (no regressions). +- OXC variant: 1702 β†’ 1704 (the CLI filename commit also benefited OXC). +- `cargo test --workspace`: 56 passed, 0 failed throughout.