[compiler] Port React Compiler to Rust by josephsavona · Pull Request #36173 · react/react

16 min read Original article ↗

added 30 commits

March 21, 2026 11:17

Joe Savona

…s in reactive printer

Add HirFunctionFormatter callback to reactive DebugPrinter so FunctionExpression
and ObjectMethod values can print their inner HIR functions with full detail.
Bridge debug_print.rs formatting into the reactive printer via format_hir_function_into.

Joe Savona

Remove blank line output for unprinted outlined functions that caused
Environment section misalignment. 1285/1717 fixtures now pass.

Joe Savona

…unction value blocks

Port the TS logic that converts StoreLocal to LoadLocal when the last instruction
of a value block stores to an unnamed temporary. This fixes identifier/place
mismatches in the reactive function output. 1459/1717 fixtures now pass.

Joe Savona

In BuildReactiveFunction, for-loops should use the update block as the continue
target when present, falling back to the test block. Matches TS
terminal.update ?? terminal.test pattern.

Joe Savona

BuildReactiveFunction is implemented with 1458/1717 fixtures passing (85%).

Joe Savona

Major fixes to match the TypeScript BuildReactiveFunction behavior:

- Add valueBlockResultToSequence for for/for-of/for-in init and for-of test values,
  which wraps value block results in SequenceExpressions with proper lvalue assignment
- Fix for-of continue_block to use init (not test), matching TS scheduleLoop call
- Add reachable() checks for if, switch, while, and label terminal fallthroughs
- Add loopId checks for all loop types (do-while, while, for, for-of, for-in) to
  verify loop blocks aren't already scheduled before traversal
- Add alternate != fallthrough check for if terminals (matching TS branch semantics)
- Fix switch case processing order to reverse (matching TS reverse-iterate-then-reverse)
- Fix switch to skip already-scheduled cases instead of pushing None blocks
- Fix value block catch-all to not propagate parent fallthrough (TS passes null)
- Clean up dead code in value block catch-all

Pass rate: 1635/1717 (95.2%). Remaining 82 failures are all earlier-pass issues.

Joe Savona

Ported 15 reactive passes and visitor/transform infrastructure from TypeScript to Rust.
Includes assertWellFormedBreakTargets, pruneUnusedLabels, assertScopeInstructionsWithinScopes,
pruneNonEscapingScopes, pruneNonReactiveDependencies, pruneUnusedScopes,
mergeReactiveScopesThatInvalidateTogether, pruneAlwaysInvalidatingScopes, propagateEarlyReturns,
pruneUnusedLValues, promoteUsedTemporaries, extractScopeDeclarationsFromDestructuring,
stabilizeBlockIds, renameVariables, and pruneHoistedContexts. 1603/1717 tests passing (93.4%).

Joe Savona

…-port.ts

The .replace(/\(generated\)/g, '(none)') normalization was effectively a no-op:
both TS and Rust event items go through the same formatLoc in the test harness,
producing identical (generated) strings. The HIR debug printers output "generated"
without parentheses, so the regex never matched HIR output either.

Joe Savona

Reorder the 4 create_temporary_place_id calls in apply_early_return_to_scope
to match the TypeScript allocation order (sentinelTemp first, then symbolTemp,
forTemp, argTemp). The Rust port had them in a different order, causing
IdentifierIds to be assigned differently and producing 33 test divergences
in PropagateEarlyReturns output.

Joe Savona

…S behavior

In TypeScript, `buildReverseGraph` (Dominator.ts:237) calls `fn.env.nextBlockId`
to create a synthetic exit node, which increments the block ID counter as a
side-effect. The Rust port reads `env.next_block_id_counter` without incrementing.

This causes block ID offsets: for a simple function, TS allocates 3 extra block
IDs (one each from ValidateHooksUsage, ValidateNoSetStateInRender, and
InferReactivePlaces) that Rust doesn't, causing all subsequent block IDs to
differ by 3.

Fix by changing the 3 callers to use `env.next_block_id().0` instead of
`env.next_block_id_counter`, consuming the ID to match TS behavior. This
reduces block ID divergences from ~1505 to ~117 fixtures (remaining divergences
are from recursive dominator calls within inner function validation).

Joe Savona

…ew docs

Aggregate top issues from ~95 per-file reviews into 20260321-summary.md.
Key findings: ~55 panic!() calls that should be Err(...), type inference
logic bugs, severely compressed validation passes, weakened SSA invariants,
and JS semantics divergences in ConstantPropagation. Removes stale
aggregated summary docs (SUMMARY.md, README.md, etc.) while keeping
per-file reviews.

Joe Savona

…re guidelines

Corrected several recommendations that were inconsistent with rust-port-architecture.md:
removed "at minimum panic!()" as acceptable for invariants (must be Err), marked tryRecord
as unnecessary in Rust since Result handles the concern more cleanly, fixed incorrect
claim that obj.class is invalid JS, and clarified that invariant violations must propagate
via Err rather than accumulate on env.

Joe Savona

…eps, names scope, unify shapes, phi/cycle errors

Fix 5 bugs in InferTypes:
- 2a: Resolve types for captured context variables in apply phase (FunctionExpression/ObjectMethod)
- 2b: Resolve types for StartMemoize deps with NamedLocal kind
- 2d: Merge unify/unify_with_shapes so shapes are always available for property resolution
- 3a: Return Err(CompilerDiagnostic) for empty phi operands and cycle detection instead of silent return
Also updated pipeline.rs to handle the new Result return type.

Note: Bug 2c (shared names map) was already correct — inner functions use a fresh HashMap.

Joe Savona

…on-null assertion

Changed unwrap_or(0) to .expect() for unsealed_preds lookup. TS uses a
non-null assertion (!) which maps to unwrap/panic per the architecture guide.
Silently defaulting to 0 could produce incorrect SSA IDs.

Joe Savona

…ThatInvalidateTogether

Changed 'while index <= entry.to.saturating_sub(1)' to 'while index < entry.to'
to match TS semantics. The old code would incorrectly process index 0 when
entry.to was 0 (saturating_sub(1) returns 0, and 0 <= 0 is true).

Joe Savona

…and number formatting

- Added 'delete' and 'await' to is_reserved_word (6a)
- Changed integer overflow guard from n.abs() < 1e20 to n.abs() < (i64::MAX as f64)
  to prevent potential issues with large integers near the threshold (6c)
- js_to_number already handles empty/whitespace strings correctly (6b was already fixed)

Joe Savona

…ompilationMode and PanicThreshold

Created CompilationMode (Infer/Annotation/All) and PanicThreshold
(AllErrors/CriticalErrors/None) enums with serde support. Updated all
string comparisons in program.rs to use enum pattern matching.

Joe Savona

…al correspondence with TS

Joe Savona

…reassigned for structural correspondence

Joe Savona

…tch TS non-null assertion"

This reverts commit e3c80a2.

Joe Savona

…ms for CompilationMode and PanicThreshold"

This reverts commit 88bf21f.

Joe Savona

Mark completed items (2a-2d, 3a, 5b, 6a-6c, 7a-7c), note reverted items
(5c plugin enums broke serde, 8b enter_ssa fallback was correct), and
update remaining work items with findings from implementation.

Joe Savona

… and consolidate pipeline error handling

Converted all CompilerError.invariant() and CompilerError.throwTodo() panics to
Err(CompilerDiagnostic) returns across 29 files, matching the architecture guide.
Added From<CompilerDiagnostic> for CompilerError impl to enable clean ? propagation,
replacing 17 verbose .map_err() blocks in pipeline.rs. Restored weakened SSA invariant
checks in rewrite_instruction_kinds_based_on_reassignment.rs.

Joe Savona

…flatten(), convert remaining assert! calls

Replaced .ok().flatten() with ? in callers that return Result to properly
propagate invariant errors from environment shape resolution. Converted 10
remaining assert!/assert_eq! calls in build_reactive_function.rs to
Err(CompilerDiagnostic) returns. Simplified lower_expression's function
lowering to use .expect() since the error path is unreachable.

Joe Savona

… Compiler

Copies the full react_compiler_oxc crate. Includes OXC 0.121 AST conversion,
reverse conversion, scope handling, prefilter, and diagnostics.

Joe Savona

… Compiler

Copies the full react_compiler_swc crate. Includes SWC AST conversion,
reverse conversion, scope handling, prefilter, diagnostics, and integration tests.

Joe Savona

Copies codegen_reactive_function.rs (~2800 lines) from the prior working branch.
Converts ReactiveFunction tree back into Babel-compatible AST with memoization
(useMemoCache) wired in. Includes pruneHoistedContexts fix for inner functions.

Joe Savona

Connects codegen_reactive_function to the compilation pipeline:
- Added codegen module and pub use to reactive_scopes lib.rs
- Added react_compiler_ast dependency to reactive_scopes Cargo.toml
- Updated pipeline.rs to call codegen_function after PruneHoistedContexts
- Mapped codegen results (memo stats, outlined functions) to CodegenFunction
- Fixed build_reactive_function calls to handle Result return type

Joe Savona

Extend the Rust port test script to capture and compare the final JavaScript
code produced by each compiler's Babel plugin, in addition to the existing
debug log entry comparison. The code is formatted with prettier before diffing.
Results are reported separately with their own pass/fail counts and diff output.

Joe Savona

Add react_compiler_e2e_cli binary crate for testing SWC and OXC frontends
via stdin/stdout, codegen helpers (emit functions) to both react_compiler_swc
and react_compiler_oxc, and a test-e2e.ts orchestrator that compares output
from all 3 Rust frontends (Babel/NAPI, SWC, OXC) against the TS baseline.

@blka blka mentioned this pull request

Jun 10, 2026

This was referenced

Jun 10, 2026

kdy1 added a commit to swc-project/swc that referenced this pull request

Jun 15, 2026
**Description:**

Add experimental SWC support for the Rust React Compiler from
react/react#36173.

This adds the `swc_ecma_react_compiler` bridge, SWC <-> React Compiler
AST/scope conversion, `.swcrc` `jsc.transform.reactCompiler`
configuration, diagnostics forwarding, JS/WASM option types, and tests
ported from the upstream SWC integration.

**TODO:**

- Wait for the React Compiler Rust crates to be published, then replace
the temporary git dependencies with published crate versions.


**Related issue:**
- react/react#36173

---------

Co-authored-by: DongYun Kang <kdy.1997.dev@gmail.com>

robobun added a commit to oven-sh/bun that referenced this pull request

Jun 15, 2026
Adds experimental React Compiler support to the bundler, wired as a
source-to-source pre-parse pass in ParseTask. Enable with
`bun build --react-compiler` or `Bun.build({ reactCompiler: true })` —
no Babel plugin or extra installs; compiled output imports
react/compiler-runtime (ships with react >= 19).

The compiler is the Rust port from react/react#36173, vendored at a
pinned commit via the lolhtml-style fetch-only dep
(scripts/build/deps/react-compiler.ts) and compiled into libbun_rust.a
as path deps of the new bun_react_compiler crate. The wrapper parses
with oxc, pre-scans for syntax the vendored AST converter cannot handle
yet (its todo!() arms would abort under panic=abort), runs the compiler
with compilationMode: infer / panicThreshold: none semantics, and falls
back to the original source on any bailout. node_modules files are
skipped, "use no memo" is honored.

A patch disables oxc_codegen's default sourcemap feature: the optional
oxc_sourcemap dep declares crate-type cdylib, which fails to link under
bun's static-relocation rustflags (same class of fix as lolhtml's
rlib-only patch).

The react crates pull serde_json into workspace subgraphs that never
had it, and serde_json's impl PartialEq<Value> for {u16,i32,...} makes
`errno != E::EEXIST as _`-style comparisons ambiguous. Fixed the nine
latent sites (bun_install, node_fs, win_watcher, dns options) to the
canonical `err.get_errno() == E::X` idiom.

Dunqing added a commit to Dunqing/react that referenced this pull request

Jun 17, 2026
These 6 .js (+ .expect.md) were introduced only by the Rust-port commit
03e7755 (react#36173) and are absent from its parent — they are HIR-diff
categorization probes (headers: "HIR Pattern: LOC_DIFF (45 files, 13%)",
"Round 2/3", "IDENTIFIER_DIFF", "NUMERIC_FORMAT_DIFF") committed into the
fixtures dir by mistake. They test no React semantics and only inflate the
failing-fixture count with non-bugs. Removed: hir_loc_diff, round2_loc_diff,
todo-hir_numeric_format, todo-pattern1b_type_to_primitive,
todo-hir_identifier_diff, todo-round3_promote_used_temps.
Corpus 1803 -> 1797.

javache added a commit to javache/react that referenced this pull request

Jun 29, 2026
The Rust port (react#36173) changed CompileError events to carry plain
serialized detail objects instead of CompilerError class instances. The
ESLint integrations previously called detail.printErrorMessage(source,
{eslint: true}), which printed the source code frame(s) and location for
each error detail. The replacement printErrorMessage() only emitted the
reason and description, so code frames were dropped from lint output.

Restore the per-detail printing in both ESLint integrations by exporting
printCodeFrame from CompilerError and reusing it. The detail loop handles
both detail shapes that flow through LoggerEvents: a `details` array
(CompilerDiagnostic and the Rust compiler) and a legacy flat `loc`
(deprecated CompilerErrorDetail), so it works with the Rust compiler as
well as the TypeScript one.

javache added a commit that referenced this pull request

Jun 30, 2026
)

## Summary

The Rust port (#36173) changed `CompileError` `LoggerEvent`s to carry
plain serialized detail objects instead of
`CompilerError`/`CompilerDiagnostic` class instances. As a result, the
ESLint integrations could no longer call
`detail.printErrorMessage(source, {eslint: true})` and were given a
replacement `printErrorMessage()` helper that only emitted the `reason`
and `description`.

This regressed error printing: the **source code frame(s) and
`file:line:column` location** that used to appear for each error detail
were dropped from lint output.

This PR restores the previous behaviour:

- Export `printCodeFrame` from `CompilerError` and reuse it from both
ESLint integrations instead of duplicating it.
- Rebuild the full message (reason, description, per-detail code frames,
and hints) in `printErrorMessage`.
- Handle **both** detail shapes that flow through `LoggerEvent`s:
- a `details` array (`CompilerDiagnostic` and the **Rust** compiler),
and
  - a legacy flat `loc` (deprecated `CompilerErrorDetail`).

`formatDetailForLogging` emits one or the other, so the previous
unconditional iteration over `error.details` would have thrown
`TypeError: not iterable` on the flat-`loc` path. Normalizing to a list
fixes that and keeps the code working with both the TypeScript and Rust
compilers.

## Test plan

- `tsc --noEmit` on both ESLint packages is clean (no new errors vs.
baseline).
- Verified the detail loop handles the Rust compiler's `details` array
shape and the legacy flat `loc` shape.

github-actions Bot pushed a commit that referenced this pull request

Jun 30, 2026
)

## Summary

The Rust port (#36173) changed `CompileError` `LoggerEvent`s to carry
plain serialized detail objects instead of
`CompilerError`/`CompilerDiagnostic` class instances. As a result, the
ESLint integrations could no longer call
`detail.printErrorMessage(source, {eslint: true})` and were given a
replacement `printErrorMessage()` helper that only emitted the `reason`
and `description`.

This regressed error printing: the **source code frame(s) and
`file:line:column` location** that used to appear for each error detail
were dropped from lint output.

This PR restores the previous behaviour:

- Export `printCodeFrame` from `CompilerError` and reuse it from both
ESLint integrations instead of duplicating it.
- Rebuild the full message (reason, description, per-detail code frames,
and hints) in `printErrorMessage`.
- Handle **both** detail shapes that flow through `LoggerEvent`s:
- a `details` array (`CompilerDiagnostic` and the **Rust** compiler),
and
  - a legacy flat `loc` (deprecated `CompilerErrorDetail`).

`formatDetailForLogging` emits one or the other, so the previous
unconditional iteration over `error.details` would have thrown
`TypeError: not iterable` on the flat-`loc` path. Normalizing to a list
fixes that and keeps the code working with both the TypeScript and Rust
compilers.

## Test plan

- `tsc --noEmit` on both ESLint packages is clean (no new errors vs.
baseline).
- Verified the detail loop handles the Rust compiler's `details` array
shape and the legacy flat `loc` shape.

DiffTrain build for [9c1f097](9c1f097)

github-actions Bot pushed a commit that referenced this pull request

Jun 30, 2026
)

## Summary

The Rust port (#36173) changed `CompileError` `LoggerEvent`s to carry
plain serialized detail objects instead of
`CompilerError`/`CompilerDiagnostic` class instances. As a result, the
ESLint integrations could no longer call
`detail.printErrorMessage(source, {eslint: true})` and were given a
replacement `printErrorMessage()` helper that only emitted the `reason`
and `description`.

This regressed error printing: the **source code frame(s) and
`file:line:column` location** that used to appear for each error detail
were dropped from lint output.

This PR restores the previous behaviour:

- Export `printCodeFrame` from `CompilerError` and reuse it from both
ESLint integrations instead of duplicating it.
- Rebuild the full message (reason, description, per-detail code frames,
and hints) in `printErrorMessage`.
- Handle **both** detail shapes that flow through `LoggerEvent`s:
- a `details` array (`CompilerDiagnostic` and the **Rust** compiler),
and
  - a legacy flat `loc` (deprecated `CompilerErrorDetail`).

`formatDetailForLogging` emits one or the other, so the previous
unconditional iteration over `error.details` would have thrown
`TypeError: not iterable` on the flat-`loc` path. Normalizing to a list
fixes that and keeps the code working with both the TypeScript and Rust
compilers.

## Test plan

- `tsc --noEmit` on both ESLint packages is clean (no new errors vs.
baseline).
- Verified the detail loop handles the Rust compiler's `details` array
shape and the legacy flat `loc` shape.

DiffTrain build for [9c1f097](9c1f097)

github-actions Bot pushed a commit to code/lib-react that referenced this pull request

Jul 3, 2026
…ct#36901)

## Summary

The Rust port (react#36173) changed `CompileError` `LoggerEvent`s to carry
plain serialized detail objects instead of
`CompilerError`/`CompilerDiagnostic` class instances. As a result, the
ESLint integrations could no longer call
`detail.printErrorMessage(source, {eslint: true})` and were given a
replacement `printErrorMessage()` helper that only emitted the `reason`
and `description`.

This regressed error printing: the **source code frame(s) and
`file:line:column` location** that used to appear for each error detail
were dropped from lint output.

This PR restores the previous behaviour:

- Export `printCodeFrame` from `CompilerError` and reuse it from both
ESLint integrations instead of duplicating it.
- Rebuild the full message (reason, description, per-detail code frames,
and hints) in `printErrorMessage`.
- Handle **both** detail shapes that flow through `LoggerEvent`s:
- a `details` array (`CompilerDiagnostic` and the **Rust** compiler),
and
  - a legacy flat `loc` (deprecated `CompilerErrorDetail`).

`formatDetailForLogging` emits one or the other, so the previous
unconditional iteration over `error.details` would have thrown
`TypeError: not iterable` on the flat-`loc` path. Normalizing to a list
fixes that and keeps the code working with both the TypeScript and Rust
compilers.

## Test plan

- `tsc --noEmit` on both ESLint packages is clean (no new errors vs.
baseline).
- Verified the detail loop handles the Rust compiler's `details` array
shape and the legacy flat `loc` shape.

DiffTrain build for [9c1f097](react@9c1f097)

github-actions Bot pushed a commit to code/lib-react that referenced this pull request

Jul 3, 2026
…ct#36901)

## Summary

The Rust port (react#36173) changed `CompileError` `LoggerEvent`s to carry
plain serialized detail objects instead of
`CompilerError`/`CompilerDiagnostic` class instances. As a result, the
ESLint integrations could no longer call
`detail.printErrorMessage(source, {eslint: true})` and were given a
replacement `printErrorMessage()` helper that only emitted the `reason`
and `description`.

This regressed error printing: the **source code frame(s) and
`file:line:column` location** that used to appear for each error detail
were dropped from lint output.

This PR restores the previous behaviour:

- Export `printCodeFrame` from `CompilerError` and reuse it from both
ESLint integrations instead of duplicating it.
- Rebuild the full message (reason, description, per-detail code frames,
and hints) in `printErrorMessage`.
- Handle **both** detail shapes that flow through `LoggerEvent`s:
- a `details` array (`CompilerDiagnostic` and the **Rust** compiler),
and
  - a legacy flat `loc` (deprecated `CompilerErrorDetail`).

`formatDetailForLogging` emits one or the other, so the previous
unconditional iteration over `error.details` would have thrown
`TypeError: not iterable` on the flat-`loc` path. Normalizing to a list
fixes that and keeps the code working with both the TypeScript and Rust
compilers.

## Test plan

- `tsc --noEmit` on both ESLint packages is clean (no new errors vs.
baseline).
- Verified the detail loop handles the Rust compiler's `details` array
shape and the legacy flat `loc` shape.

DiffTrain build for [9c1f097](react@9c1f097)

camc314 pushed a commit to oxc-project/oxc that referenced this pull request

Jul 3, 2026
…22942)

Closes #10048

Integrates the Rust port of the React Compiler ([react/react#36173](react/react#36173)) into oxc.

## Notes (resolved)

- Published crates can't reference Git URLs, so using this from Rolldown needs the React Compiler crates on crates.io. ✅ Published as a fork at https://crates.io/crates/forked_react_compiler; this PR depends on those `forked_react_compiler*` crates so people can get earlier access.
- The React Compiler crates had no license field. ✅ The published fork carries `license = "MIT"` (per the React repo's MIT license), so Cargo Deny and Security Analysis pass.

## Benchmark

**Wall-clock overhead vs plain transform** (release `transformSync`, same `jsx: automatic`, toggling only `reactCompiler`):

| fixture | without | with | overhead |
| --- | --- | --- | --- |
| RadixUIAdoptionSection.jsx (2.5 KiB) | 0.03 ms | 1.81 ms | +1.8 ms |
| excalidraw `App.tsx` (406 KiB) | 4.09 ms | 14.49 ms | +10.4 ms (3.5×) |

So roughly a fixed ~1.8 ms floor per file plus a per-size cost — about **3.5× the transform time** on a large real-world component.

## Binary size

Linking the React Compiler pulls the whole compiler pipeline (HIR, lowering, inference, SSA, optimization, reactive scopes, validation) plus the oxc⇄Babel AST conversion into the binding. Release build of the napi transform addon (`transform.darwin-arm64.node`, `--release`, stripped), darwin-arm64:

| build | size |
| --- | --- |
| baseline (`main`) | 3.51 MiB |
| with React Compiler | 8.66 MiB |
| **delta** | **+5.14 MiB (+146%, 2.46×)** |