From 7af2e7c7ec70c2dd85cfb981e7d29a84b9006d05 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 11 Sep 2026 21:18:34 +0000 Subject: [PATCH 1/3] react_compiler: fix panic on an object method shorthand in ssr output mode In ssr mode the pipeline skips InferReactiveScopeVariables for the compiled function, so no identifier has a reactive scope. AlignObjectMethodScopes ran anyway and called expect() on the scope of every object method and its object literal, which aborted the process. Treat a pair with no scope on either side as nothing to align when memoization is disabled, like AlignMethodCallScopes does. Return any other mismatch as an invariant error, so the function is left uncompiled. --- .../inference/align_object_method_scopes.rs | 46 ++++++++++++----- src/react_compiler/pipeline.rs | 2 +- .../bundler/transpiler/react-compiler.test.ts | 50 +++++++++++++++++++ 3 files changed, 84 insertions(+), 14 deletions(-) diff --git a/src/react_compiler/inference/align_object_method_scopes.rs b/src/react_compiler/inference/align_object_method_scopes.rs index 9755c1f6a65d..b302d338d465 100644 --- a/src/react_compiler/inference/align_object_method_scopes.rs +++ b/src/react_compiler/inference/align_object_method_scopes.rs @@ -12,6 +12,7 @@ use std::cmp; use std::collections::{HashMap, HashSet}; +use crate::diagnostics::{CompilerDiagnostic, cold_invariant}; use crate::hir::environment::Environment; use crate::hir::{ EvaluationOrder, HirFunction, IdentifierId, InstructionValue, ObjectPropertyOrSpread, ScopeId, @@ -25,7 +26,10 @@ use crate::utils::DisjointSet; /// Identifies ObjectMethod lvalue identifiers and then finds ObjectExpression /// instructions whose operands reference those methods. Returns a disjoint set /// of scopes that must be merged. -fn find_scopes_to_merge(func: &HirFunction, env: &Environment) -> DisjointSet { +fn find_scopes_to_merge( + func: &HirFunction, + env: &Environment, +) -> Result, CompilerDiagnostic> { let mut object_method_decls: HashSet = HashSet::new(); let mut merged_scopes = DisjointSet::::new(); @@ -48,14 +52,25 @@ fn find_scopes_to_merge(func: &HirFunction, env: &Environment) -> DisjointSet { + merged_scopes.union(&[operand_sid, lvalue_sid]); + } + // With memoization disabled the pipeline does not run + // InferReactiveScopeVariables on the outermost function, + // so neither identifier has a scope and there is nothing + // to align. Upstream has no such arm and throws here. + (None, None) if !env.enable_memoization() => {} + // TS: CompilerError.invariant(operandScope != null && lvalueScope != null, ...) + _ => { + return Err(cold_invariant( + "Internal error: Expected all ObjectExpressions and ObjectMethods to have non-null scope.", + None, + None, + ) + .into()); + } + } } } } @@ -64,7 +79,7 @@ fn find_scopes_to_merge(func: &HirFunction, env: &Environment) -> DisjointSet DisjointSet Result<(), CompilerDiagnostic> { // Handle inner functions first (TS recurses before processing the outer function) for (_block_id, block) in &func.body.blocks { for &instr_id in &block.instructions { @@ -88,7 +106,7 @@ pub(crate) fn align_object_method_scopes(func: &mut HirFunction, env: &mut Envir &mut env.functions[func_id.0 as usize], crate::ssa::enter_ssa::placeholder_function(), ); - align_object_method_scopes(&mut inner_func, env); + align_object_method_scopes(&mut inner_func, env)?; env.functions[func_id.0 as usize] = inner_func; } _ => {} @@ -96,7 +114,7 @@ pub(crate) fn align_object_method_scopes(func: &mut HirFunction, env: &mut Envir } } - let mut merged_scopes = find_scopes_to_merge(func, env); + let mut merged_scopes = find_scopes_to_merge(func, env)?; // Step 1: Merge affected scopes to their canonical root. // Use a HashMap to accumulate min/max across all scopes mapping to the same root, @@ -165,4 +183,6 @@ pub(crate) fn align_object_method_scopes(func: &mut HirFunction, env: &mut Envir } } } + + Ok(()) } diff --git a/src/react_compiler/pipeline.rs b/src/react_compiler/pipeline.rs index c25efb956ae8..1f88dc3b19ea 100644 --- a/src/react_compiler/pipeline.rs +++ b/src/react_compiler/pipeline.rs @@ -562,7 +562,7 @@ fn run_hir_passes( timed!( "AlignObjectMethodScopes", crate::inference::align_object_method_scopes(hir, env) - ); + )?; timed!( "PruneUnusedLabelsHIR", diff --git a/test/bundler/transpiler/react-compiler.test.ts b/test/bundler/transpiler/react-compiler.test.ts index 94621cd153a7..3a2450d34965 100644 --- a/test/bundler/transpiler/react-compiler.test.ts +++ b/test/bundler/transpiler/react-compiler.test.ts @@ -253,6 +253,56 @@ describe("bundler", () => { }, }); + // With memoization off (ssr mode) the pipeline infers no reactive scopes for + // the compiled function itself, so an object literal and its method + // shorthands reach AlignObjectMethodScopes with no scope to align. + // + // The fake useState never returns its argument. The ssr pass inlines useState + // to its initial value, so a function prints that value only when the + // compiler compiled it and did not skip it. + itBundled("react-compiler/SsrObjectMethodShorthand", { + files: { + "/entry.jsx": /* jsx */ ` + import { useState } from "react"; + + function InJsxAttribute() { + const [n] = useState(1); + return
; + } + function InBody({ label }) { + const [n] = useState(2); + const v = { + m() { return label + n; }, + async am() { return n; }, + [label]() { return n; }, + nested() { return { inner() { return n; } }; }, + }; + return
; + } + function useApi(value) { + const [n] = useState(3); + return { get() { return value + n; } }; + } + + const a = InJsxAttribute().props["data-v"]; + const b = InBody({ label: "x" }).props["data-v"]; + console.log(JSON.stringify({ + InJsxAttribute: a.m(), + InBody: [b.m(), await b.am(), b.x(), b.nested().inner()], + useApi: useApi("v").get(), + })); + `, + "/node_modules/react/package.json": `{"name":"react","main":"./index.js"}`, + "/node_modules/react/index.js": `exports.useState = () => ["not inlined", () => {}];`, + "/node_modules/react/jsx-runtime.js": `exports.jsx = exports.jsxs = (type, props) => ({ type, props });`, + "/node_modules/react/jsx-dev-runtime.js": `exports.jsxDEV = (type, props) => ({ type, props });`, + }, + reactCompiler: true, + target: "bun", + backend: "cli", + run: { stdout: '{"InJsxAttribute":1,"InBody":["x2",2,2,2],"useApi":"v3"}' }, + }); + // https://github.com/oven-sh/bun/pull/32504#discussion_r3447488111 itBundled("react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client", { files: { From 6034585a4121b0d39436bec6926626178e3a7c98 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 11 Sep 2026 21:50:28 +0000 Subject: [PATCH 2/3] ci: retrigger From c8872c3e387900abd4e44b4db154c86cec7f9ccf Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 11 Sep 2026 21:52:47 +0000 Subject: [PATCH 3/3] react_compiler: shorten the comment on the no-scope arm --- src/react_compiler/inference/align_object_method_scopes.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/react_compiler/inference/align_object_method_scopes.rs b/src/react_compiler/inference/align_object_method_scopes.rs index b302d338d465..06457fe7aba7 100644 --- a/src/react_compiler/inference/align_object_method_scopes.rs +++ b/src/react_compiler/inference/align_object_method_scopes.rs @@ -56,10 +56,7 @@ fn find_scopes_to_merge( (Some(operand_sid), Some(lvalue_sid)) => { merged_scopes.union(&[operand_sid, lvalue_sid]); } - // With memoization disabled the pipeline does not run - // InferReactiveScopeVariables on the outermost function, - // so neither identifier has a scope and there is nothing - // to align. Upstream has no such arm and throws here. + // No scopes exist without memoization. Upstream throws here. (None, None) if !env.enable_memoization() => {} // TS: CompilerError.invariant(operandScope != null && lvalueScope != null, ...) _ => {