@samitouri / QOS-React-2 / commits / ae6fe8a3e1

[rust-compiler] Return the compiled AST by value instead of JSON (#36729)

`compile_program` serialized the transformed `File` to a JSON string and every in-process consumer immediately deserialized it back, so each oxc/swc compile paid a full serialize/parse round-trip of the entire program for nothing. `CompileResult::Success.ast` is now `Option<react_compiler_ast::File>`; the napi bridge keeps its JSON boundary by serializing at the edge, while the oxc and swc frontends consume the typed AST directly. This ports the typed-AST patch the Oxc team carries on their fork (co-authored with Boshen), which also makes their integration patch-free going forward. Verified: cargo workspace tests, both snap channels 1804/1804, e2e comparison harness at parity baseline (1802 byte-identical, fbt local-require the one known divergence). First of a stack of three with #36730 and #36731. Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>

lauren committed Jun 10, 2026 at 20:19 UTC ae6fe8a3e1d6806d4ed7c0f3930ad4de2f24ef2c
4 files changed +13 -39
compiler/crates/react_compiler/src/entrypoint/compile_result.rs
+6 -3
@@ -1,6 +1,7 @@
1 use react_compiler_ast::expressions::Identifier as AstIdentifier;
2 use react_compiler_ast::patterns::PatternLike;
3 use react_compiler_ast::statements::BlockStatement;
4 +use react_compiler_ast::File;
5 use react_compiler_diagnostics::SourceLocation;
6 use react_compiler_hir::ReactFunctionType;
7 use serde::Serialize;
@@ -81,10 +82,12 @@ pub struct BindingRenameInfo {
82 pub enum CompileResult {
83 /// Compilation succeeded (or no functions needed compilation).
84 /// `ast` is None if no changes were made to the program.
84 - /// The AST is stored as a pre-serialized JSON string (RawValue) to avoid
85 - /// double-serialization: File→Value→String becomes File→String directly.
85 + /// The compiled Babel AST is returned by value so in-process Rust consumers
86 + /// (the oxc/swc frontends) use it directly instead of round-tripping through
87 + /// JSON. CompileResult still derives Serialize, so the napi consumer
88 + /// serializes the whole result (inlining the File) as before.
89 Success {
87 - ast: Option<Box<serde_json::value::RawValue>>,
90 + ast: Option<File>,
91 events: Vec<LoggerEvent>,
92 /// Unified ordered log interleaving events and debug entries.
93 /// Items appear in the order they were emitted during compilation.
compiler/crates/react_compiler/src/entrypoint/program.rs
+3 -18
@@ -4020,27 +4020,12 @@ pub fn compile_program(mut file: File, scope: ScopeInfo, options: PluginOptions)
4020 // Now we can mutate file.program
4021 apply_compiled_functions(&replacements, &mut file.program, &mut context);
4022
4023 - // Serialize the modified File AST directly to a JSON string and wrap as RawValue.
4024 - // This avoids double-serialization (File→Value→String) by going File→String directly.
4025 - // The RawValue is embedded verbatim when the CompileResult is serialized.
4026 - let ast = match serde_json::to_string(&file) {
4027 - Ok(s) => match serde_json::value::RawValue::from_string(s) {
4028 - Ok(raw) => Some(raw),
4029 - Err(e) => {
4030 - eprintln!("RUST COMPILER: Failed to create RawValue: {}", e);
4031 - None
4032 - }
4033 - },
4034 - Err(e) => {
4035 - eprintln!("RUST COMPILER: Failed to serialize AST: {}", e);
4036 - None
4037 - }
4038 - };
4039 -
4023 let timing_entries = context.timing.into_entries();
4024
4025 + // Return the compiled File by value; in-process Rust consumers use it
4026 + // directly, and the napi consumer serializes the whole result as before.
4027 CompileResult::Success {
4043 - ast,
4028 + ast: Some(file),
4029 events: context.events,
4030 ordered_log: context.ordered_log,
4031 renames: convert_renames(&context.renames),
compiler/crates/react_compiler_oxc/src/lib.rs
+1 -9
@@ -77,16 +77,8 @@ pub fn transform(
77 // This maps source positions to new identifier names for uncompiled code.
78 let rename_plan = build_rename_plan(&scope_info, &renames);
79
80 - let compiled_file = program_ast.and_then(|raw_json| {
81 - // First parse to serde_json::Value which deduplicates "type" fields
82 - // (the compiler output can produce duplicate "type" keys due to
83 - // BaseNode.node_type + #[serde(tag = "type")] enum tagging)
84 - let value: serde_json::Value = serde_json::from_str(raw_json.get()).ok()?;
85 - serde_json::from_value(value).ok()
86 - });
87 -
80 TransformResult {
89 - file: compiled_file,
81 + file: program_ast,
82 diagnostics,
83 events,
84 rename_plan,
compiler/crates/react_compiler_swc/src/lib.rs
+3 -9
@@ -91,7 +91,7 @@ pub fn transform(
91 react_compiler::entrypoint::program::compile_program(file, scope_info, options);
92
93 let diagnostics = compile_result_to_diagnostics(&result);
94 - let (program_json, events, renames) = match result {
94 + let (program_ast, events, renames) = match result {
95 react_compiler::entrypoint::compile_result::CompileResult::Success {
96 ast,
97 events,
@@ -103,14 +103,8 @@ pub fn transform(
103 } => (None, events, Vec::new()),
104 };
105
106 - let conversion_result = program_json.and_then(|raw_json| {
107 - // First parse to serde_json::Value which deduplicates "type" fields
108 - // (the compiler output can produce duplicate "type" keys due to
109 - // BaseNode.node_type + #[serde(tag = "type")] enum tagging)
110 - let value: serde_json::Value = serde_json::from_str(raw_json.get()).ok()?;
111 - let file: react_compiler_ast::File = serde_json::from_value(value).ok()?;
112 - let result = convert_program_to_swc_with_source(&file, Some(source_text));
113 - Some(result)
106 + let conversion_result = program_ast.map(|file| {
107 + convert_program_to_swc_with_source(&file, Some(source_text))
108 });
109
110 let (mut swc_module, mut comments) = match conversion_result {