This is an experimental, work-in-progress port of React Compiler to Rust. Key points: * Work-in-progress - we are sharing early, prior to testing internally at Meta, to get feedback from partners in parallel with continued development. * No builds available yet, you'll have to do some hacking if you want to try this. * All fixtures pass, no known gaps but there may be lurking bugs. * The architecture was heavily guided by humans (me, @josephsavona) but majority coded by AI. I was very hands-on in setting the architecture, the testing and verification strategy, incremental migration approach, etc. I also kept a close eye on the code and spent a decent amount of time going back and forth to get code quality to a decent level. * The public API is basically "Rust Babel AST" + Scope Info in, Rust Babel AST out. We use a Rust representation of the Babel AST as our "public API", as it were, and then each integration (Babel, OXC, SWC) converts to/from their native representation. For now integrations must also provide scope information - in the future React Compiler may compute bindings and references itself from the AST. * Internally, the Rust version uses the same architecture as the TypeScript version. The compiler converts from the AST into our own intermediate representation (HIR, short for High-level Intermediate Representation) which uses a control-flow graph (CFG) and single-static assignment (SSA). We go through the same series of passes, with the same overall algorithms. It's very much a pass-by-pass port. The main differences are in the data representation - using arena-like structures (and indices into these arenas) to work within Rust's borrowing system. * Early performance numbers are derived from AI and i haven't spent much time validating the benchmark setup, beyond the fact that the optimization opportunities it discovered made complete sense and the fixes were right. With that caveat, itt does appear that the Rust version is quite fast already: 3x faster when operating as a Babel plugin. The serialization cost is quite high, but the actual transformation logic is ~10x faster, so it's net faster. Native integrations (oxc, swc) should be even faster. * There are 3 integrations right now: an alternative Babel plugin (which will eventually get removed as we integrate into babel-plugin-react-compiler), and examples of what OXC and SWC integrations could look like (see react_compiler_oxc and react_compiler_swc crates). correctness: * all 1725 fixtures pass in snap when comparing the temporary rust version of the plugin with the main version. this compares generated code output as well as errors. * all fixtures also pass a full comparison of the per-pass compiler intermediate representation — the intermediate state (including log events and errors) are ~identical after every single pass (modulo some normalization of ids) * The OXC and SWC example integrations seem to be working well, though i haven't manually verified this to the same extent as i have the Babel integration. development: * `yarn snap --rust` is 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.sh` is 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.sh` does 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: * Currently the compiler returns `Option<Program>`, which is `Some` if 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). * The Rust representation of the Babel AST is fine enough, but we could make it more optimal by doing arena allocation. We also plan to change the string representation to smol_str. * The scope representation, and association of data btw AST and scope, is very much a first pass approach that is good enough. We expect to implement our own scope resolution, though, so we hopefully won't need to iterate on the scope representation and can just throw it away. In terms of the shape of the integration, we anticipate that each integration would have the following: * Implementor repo (OXC, SWC, etc): lightweight code transform and lint pipeline integration that delegates to `crates/react_compiler_<name>` from our repo * Our repo: one crate per implementor, eg react_compiler_swc, react_compiler_oxc, where most of the logic lives. This setup lets us make changes to the integration layer easily within our repo. Feedback appreciated! --------- Co-authored-by: Joe Savona <[email protected]> Co-authored-by: Mike Vitousek <[email protected]> Co-authored-by: Mike Vitousek <[email protected]> Co-authored-by: lauren <[email protected]> Co-authored-by: lauren <[email protected]>
10 KiB
Plan: react_compiler_oxc — OXC Frontend for React Compiler
Context
The Rust React Compiler (compiler/crates/) currently accepts Babel-format AST (react_compiler_ast::File) + scope info (ScopeInfo) and compiles via compile_program(). The only frontend is a Babel NAPI bridge (compiler/packages/babel-plugin-react-compiler-rust/). This plan adds an OXC frontend that enables both build-time code transformation and linting via the OXC ecosystem, all in pure Rust (no JS/NAPI boundary).
Crate Structure
compiler/crates/react_compiler_oxc/
Cargo.toml
src/
lib.rs — Public API: transform(), lint(), ReactCompilerRule
prefilter.rs — Quick check for React-like function names in OXC AST
convert_ast.rs — OXC AST → react_compiler_ast::File
convert_ast_reverse.rs — react_compiler_ast → OXC AST (for applying results)
convert_scope.rs — OXC Semantic → ScopeInfo
diagnostics.rs — CompileResult → OxcDiagnostic conversion
Dependencies (Cargo.toml)
[dependencies]
react_compiler_ast = { path = "../react_compiler_ast" }
react_compiler = { path = "../react_compiler" }
react_compiler_diagnostics = { path = "../react_compiler_diagnostics" }
oxc_parser = "..."
oxc_ast = "..."
oxc_semantic = "..."
oxc_allocator = "..."
oxc_span = "..."
oxc_diagnostics = "..."
oxc_linter = "..." # for Rule trait
indexmap = "..."
Module Details
1. prefilter.rs — Quick React Function Check
Port of babel-plugin-react-compiler-rust/src/prefilter.ts.
pub fn has_react_like_functions(program: &oxc_ast::ast::Program) -> bool
- Use
oxc_ast::Visittrait to walk the AST - Check
FunctionDeclarationnames,VariableDeclaratorinits that are arrow/function expressions - Skip class bodies
- Name check:
starts_with(uppercase)or matchesuse[A-Z0-9] - Return
trueon first match (early exit)
2. convert_scope.rs — OXC Semantic → ScopeInfo
pub fn convert_scope_info(semantic: &oxc_semantic::Semantic) -> ScopeInfo
This is the most natural conversion — both use arena-indexed flat tables with copyable u32 IDs.
Scopes: Iterate semantic.scopes(). For each scope:
ScopeId— direct u32 remappingparent— fromscope_tree.get_parent_id()kind— mapScopeFlags→ScopeKind(Top→Program, Function→Function, CatchClause→Catch, etc.; use parent AST node to distinguish For vs Block)bindings— fromscope_tree.get_bindings(), map name→SymbolId to name→BindingId
Bindings: Iterate semantic.symbols(). For each symbol:
BindingId— direct u32 remapping from SymbolIdname,scope— direct from SymbolTablekind— inspect declaration AST node type: VariableDeclaration(var/let/const), FunctionDeclaration→Hoisted, param→Param, ImportDeclaration→Moduledeclaration_type— string name of the declaring AST node typedeclaration_start— span.start of the binding's declaring identifierimport— for Module bindings, extract source/kind/imported from the ImportDeclaration
node_to_scope: Walk AST nodes that create scopes; map node.span().start → ScopeId.
reference_to_binding: Iterate all references from SymbolTable. For each resolved reference: map reference.span().start → BindingId. Also add each symbol's declaration identifier span.
program_scope: ScopeId(0).
Key files:
- Target types:
compiler/crates/react_compiler_ast/src/scope.rs - Reference impl:
compiler/packages/babel-plugin-react-compiler-rust/src/scope.ts
3. convert_ast.rs — OXC AST → react_compiler_ast::File
pub fn convert_program(
program: &oxc_ast::ast::Program,
source_text: &str,
comments: &[oxc_ast::Comment],
) -> react_compiler_ast::File
Approach: Recursive conversion, one function per AST category (statement, expression, pattern, JSX, etc.). Data is copied out of OXC's arena into owned react_compiler_ast types.
ConvertCtx: Holds a line-offset table (built from source_text at init) for computing Position { line, column, index } from byte offsets.
BaseNode construction:
start = Some(span.start),end = Some(span.end)— critical for scope lookupsloc— computed via line-offset table binary search
Key mappings:
| OXC | react_compiler_ast |
|---|---|
Statement enum variants |
statements::Statement variants |
Expression enum variants |
expressions::Expression variants |
Declaration (separate in OXC) |
Folded into Statement (Babel style) |
BindingPattern |
patterns::PatternLike |
JSXElement/Fragment/etc |
jsx::* types |
| TS type annotations | Option<Box<serde_json::Value>> (opaque passthrough) |
Comments: Map OXC Comment { kind, span } → react_compiler_ast::common::Comment (CommentBlock/CommentLine with start/end/value).
Key files:
- Target types:
compiler/crates/react_compiler_ast/src/(all modules)
4. convert_ast_reverse.rs — react_compiler_ast → OXC AST
Mirror of convert_ast.rs. Converts the compiled Babel-format AST back into OXC AST nodes.
pub fn convert_program_to_oxc<'a>(
file: &react_compiler_ast::File,
allocator: &'a oxc_allocator::Allocator,
) -> oxc_ast::ast::Program<'a>
- Allocates new OXC AST nodes into the provided arena
- Maps each
react_compiler_asttype back to its OXC equivalent - The
CompileResult::Success { ast, .. }returnsast: Option<serde_json::Value>— first deserialize toreact_compiler_ast::File, then convert to OXC
This is the most labor-intensive module but avoids the perf cost of re-parsing.
5. diagnostics.rs — Compiler Results → OXC Diagnostics
pub fn compile_result_to_diagnostics(
result: &CompileResult,
source_text: &str,
) -> Vec<oxc_diagnostics::OxcDiagnostic>
Map compiler events/errors to OXC diagnostics:
LoggerEvent::CompileError { fn_loc, detail }→OxcDiagnostic::warn/errorwith label at fn_loc spanCompileResult::Error { error, .. }→OxcDiagnostic::error- Preserve error messages and source locations
6. lib.rs — Public API
Transform API (build pipeline)
/// Result of compiling a program
pub struct TransformResult<'a> {
/// The compiled program (None if no changes needed)
pub program: Option<oxc_ast::ast::Program<'a>>,
pub diagnostics: Vec<oxc_diagnostics::OxcDiagnostic>,
pub events: Vec<LoggerEvent>,
}
/// Primary API — accepts pre-parsed AST + semantic
pub fn transform<'a>(
program: &oxc_ast::ast::Program,
semantic: &oxc_semantic::Semantic,
source_text: &str,
comments: &[oxc_ast::Comment],
options: PluginOptions,
output_allocator: &'a oxc_allocator::Allocator,
) -> TransformResult<'a>
/// Convenience wrapper — parses from source text
pub fn transform_source<'a>(
source_text: &str,
source_type: oxc_span::SourceType,
options: PluginOptions,
output_allocator: &'a oxc_allocator::Allocator,
) -> TransformResult<'a>
Flow:
- Prefilter (
has_react_like_functions). Skip ifcompilationMode == "all". - Convert AST (
convert_program) - Convert scope (
convert_scope_info) - Call
compile_program(file, scope_info, options) - On success with modified AST: deserialize JSON →
File, reverse-convert to OXC AST - Convert diagnostics
Lint API
pub struct LintResult {
pub diagnostics: Vec<oxc_diagnostics::OxcDiagnostic>,
}
/// Lint — accepts pre-parsed AST + semantic
pub fn lint(
program: &oxc_ast::ast::Program,
semantic: &oxc_semantic::Semantic,
source_text: &str,
comments: &[oxc_ast::Comment],
options: PluginOptions,
) -> LintResult
/// Convenience wrapper
pub fn lint_source(
source_text: &str,
source_type: oxc_span::SourceType,
options: PluginOptions,
) -> LintResult
Same as transform but with no_emit = true / lint output mode. Only collects diagnostics, no AST output.
oxc_linter::Rule Implementation
pub struct ReactCompilerRule {
options: PluginOptions,
}
impl oxc_linter::Rule for ReactCompilerRule {
fn run_once(&self, ctx: &LintContext) {
// ctx already has parsed AST + semantic
let result = lint(
ctx.program(),
ctx.semantic(),
ctx.source_text(),
ctx.comments(),
self.options.clone(),
);
for diagnostic in result.diagnostics {
ctx.diagnostic(diagnostic);
}
}
}
This avoids double-parsing since oxc_linter provides pre-parsed AST and semantic analysis.
Implementation Phases
Phase 1: Foundation (convert_scope + convert_ast + prefilter)
convert_scope.rswith unit tests comparing against Babel scope extractionconvert_ast.rswith unit tests comparing against Babel parser JSON outputprefilter.rswith simple true/false tests- These are independently testable without the full pipeline
Phase 2: Lint path (diagnostics + lint API + Rule)
diagnostics.rslint()function inlib.rsReactCompilerRuleimpl- Test against existing compiler fixtures — verify diagnostics match
Phase 3: Transform path (reverse converter + transform API)
convert_ast_reverse.rstransform()function inlib.rs- Integration tests: compile fixtures through OXC pipeline, compare output with Babel pipeline
Phase 4: Differential testing
- Cross-validate AST conversion: parse same source with both Babel and OXC, convert both to
react_compiler_ast::File, diff - Cross-validate scope conversion: compare
ScopeInfofrom both paths - Run full fixture suite through both pipelines, compare compiled output
Verification
- Unit tests: Each module has tests for its conversion logic
- Fixture tests: Use existing fixtures at
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ - Differential tests: Compare OXC path output against Babel path output for same inputs
cargo test -p react_compiler_oxc— run all crate tests- Scope correctness: Most critical — incorrect scope info causes wrong compilation. Snapshot
ScopeInfoJSON and compare against Babel extraction golden files