[compiler] Port React Compiler to Rust#36173
Merged
Merged
Conversation
added 30 commits
March 21, 2026 11:17
…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.
Remove blank line output for unprinted outlined functions that caused Environment section misalignment. 1285/1717 fixtures now pass.
…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.
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.
BuildReactiveFunction is implemented with 1458/1717 fixtures passing (85%).
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.
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%).
…-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.
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.
…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).
…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.
…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.
…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.
…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.
…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).
…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)
…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.
…al correspondence with TS
…reassigned for structural correspondence
…tch TS non-null assertion" This reverts commit e3c80a2.
…ms for CompilationMode and PanicThreshold" This reverts commit 88bf21f.
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.
… 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.
…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.
… Compiler Copies the full react_compiler_oxc crate. Includes OXC 0.121 AST conversion, reverse conversion, scope handling, prefilter, and diagnostics.
… Compiler Copies the full react_compiler_swc crate. Includes SWC AST conversion, reverse conversion, scope handling, prefilter, diagnostics, and integration tests.
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.
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
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.
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.
Four TS_SKIP_FIXTURES entries are now vacuous: the three shadowed-own- name fixtures error identically in both compilers (and were renamed back to error.-prefixed names), and todo-jsx-intrinsic-tag-matches-local- binding now compiles identically in both. The remaining entries are genuinely divergent fixtures.
…semantics Four root causes, all in how the port approximated Babel/TS traversal: 1. Hoisting guard over-applied. The is_binding_in_block_direct_statements guard compensates for scope_bindings_with_children pulling in child block scopes, but it also rejected the block's OWN scope bindings. Babel attributes catch params and for-in/for-of head vars to the block's scope without any direct declaring statement (probe: the catch body's path.scope IS the CatchClause scope), and TS hoists them into DeclareContext. Guard now applies only to child-scope bindings. Fixes error.bug-context-variable-catch-in-lambda, error.bug-invariant-local-or-context-references (both now converge on TS's consistently-local-or-context invariant) and round2_loc_diff (a 10-file round-2 pattern). 2. Babel's scope crawl misses references its own isReferencedIdentifier classifies as referenced (observed: Flow FunctionTypeParam names resolving to value bindings are absent from binding.referencePaths under @babel/core's traverse, present under a bare re-traverse). TS's FindContextIdentifiers and hoisting re-traverse and so DO see them. scope.ts now maps crawl-missed referenced identifiers; the identifier loc index tracks in_type_annotation for them; gather_captured_context excludes annotation refs, matching TS's gatherCapturedContext which skips TypeAnnotation subtrees while FindContextIdentifiers does not. Fixes error.todo-update-expression-context-variable-via-type-annotation (StoreContext parity + the UpdateExpression-on-context todo) and todo-hir_identifier_diff (a 20-file pattern: React.Node annotation refs no longer captured into jest.mock factory contexts). 3. record_unsupported_lval recorded the TSAsExpression assignment-target todo and continued, so Rust logged HIR for functions TS never lowered (TS's handleAssignment default case throws immediately). It now returns Err. Fixes error.todo-rust-as-expression-assignment-target. 4. Hermes component-syntax desugar reuses source offsets, so a sibling reference (the forwardRef argument naming the desugared inner function) positionally aliases the function name it refers to and fell inside the function's capture range. Skip references whose offset equals their binding's declaration offset; impossible in real source, exact for desugared aliases. Fixes error.todo-round2_id_numbering (a 12-file round-2 pattern). e2e comparison: Results 1801/1803, Code 1803/1803 (remaining two are the parked fbt local-require and WTF-8 lone-surrogate items). Both snap channels 1804/1804 with the companion fixture-rename commit.
…hots, skip list With the hoisting parity fix, Rust errors identically to TS on the two catch-param-captured-by-lambda fixtures, so they return to their pre-4245fe23b9 error.bug- names with snapshots regenerated from the now-converged output, and their TS_SKIP_FIXTURES entries are dropped (three genuinely-divergent entries remain). Depends on the preceding parity commit; snap --rust is 1804/1804 only with both applied.
…ed_names
has_local_binding() checked used_names, which is only populated as
identifiers are resolved during HIR lowering. JSX tag names bypass
normal identifier resolution, so when lowering <fbt>, the fbt binding
from `const fbt = require('fbt')` might not be in used_names yet.
Switch to scope_info.find_binding_in_descendants(), which searches
Babel's complete scope data for any binding with the given name in the
compiled function's scope tree. This matches TS behavior where
resolveIdentifier uses scope.getBinding().
…shots - Bump snap's hermes-parser dependency from ^0.28.0 to ^0.32.0 to get enableExperimentalFlowMatchSyntax support for Flow match fixtures. Update yarn.lock to resolve ^0.32.0 to 0.32.0 with correct integrity. Yarn workspaces nests 0.32.0 in packages/snap/node_modules/ since babel-plugin-syntax-hermes-parser pins 0.25.1 at the workspace root. - Regenerate 6 match-expr/match-stmt fixture snapshots (now parse and compile) - Update method-call-scope-merge-mutable-range-sync snapshot - ts-namespace-export-declaration was already in SproutTodoFilter Both yarn snap --rust and yarn snap: 1804/1804, 0 failures. Verified: rm -rf node_modules && yarn install resolves hermes-parser 0.32.0 for snap, tests pass from clean state.
… version Update eval output from `(kind: exception) licensedGeos.toSorted is not a function` to the actual rendered HTML. The exception was an artifact of system Node 16 which lacks Array.prototype.toSorted(); CI uses Node 20+ where toSorted() works and the component renders successfully.
…l fixture toSorted() is unavailable on Node 16 (system default), causing the eval to throw instead of rendering. Replace with [...licensedGeos].sort() which works on all Node versions. The test exercises scope merging and mutable range sync, not Array.prototype.toSorted specifically.
mvitousek
approved these changes
Jun 9, 2026
mvitousek
left a comment
Contributor
There was a problem hiding this comment.
Sending it! Fixing forward the lone surrogate issue
mofeiZ
approved these changes
Jun 9, 2026
|
Can you publish the crates to the crates.io? |
|
+1 While Cargo supports git dependencies, crates that depend on git sources cannot themselves be published to crates.io. Projects like Oxc work around this by maintaining and publishing forked crates, so having the React team officially publish and version these crates would make adoption much easier. |
|
cool |
This was referenced Jun 10, 2026
4 tasks
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is an experimental, work-in-progress port of React Compiler to Rust. Key points:
correctness:
development:
yarn snap --rustis the primary test suite, testing that we error or compile as expected. It does not test the inner state of the compiler along the way, though, making it less suitable for finding subtle logic gaps btw the TS and Rust versions. It's also Babel based, making it less easy to test OXC and SWC integrations.compiler/scripts/test-e2e.shis an e2e test of all 3 variants (babel wrapper around Rust, OXC/SWC integrations) against the TS implementation. This does a partial comparison, focused on final output code only (doesn't test error details etc). Useful for getting the swc and oxc integrations closer to parity.compiler/script/test-rust-port.shdoes detailed testing of the internal compiler state after each pass, in addition to checking the final output code. This is the key script used to port the compiler, ensuring not just that the output was the same but that each pass was capturing all the same detail. This script can be pointed at any directory of JS files, which we expect to use for internal testing at Meta.For Partners
We're excited to partner with teams to integrate the Rust version of React Compiler into other tools, like OXC and SWC. If you're interested in working with us on this, the best place to start is by taking a look at the react_compiler_swc and react_compiler_oxc crates. These give you an idea of the API shape that we're thinking of.
Note that the conversion from any AST into our HIR is complex, and we can only maintain one version. Hence we've aligned on using a Babel-like AST as our public API. Another key point is that we don't yet implement our own scope analysis (since the TS version of the compiler relied on Babel's scope analysis), so for now we require that the scope data be serialized. It's a denormalized graph, and some metadata has to be stored to associate nodes with scopes. We're open to feedback about the AST and scope representation - we iterated a bit just to get things to work, but it can be more optimal.
Key changes that we are considering:
Option<Program>, which isSomeif anything changed. This requires replacing the entire program. We plan to change this to return a series of patches to apply, in a form that is reasonably usable and efficient for all the integrations we care about (Babel, OXC, SWC, etc).In terms of the shape of the integration, we anticipate that each integration would have the following:
crates/react_compiler_<name>from our repoThis setup lets us make changes to the integration layer easily within our repo. Feedback appreciated!