diff --git a/src/react_compiler/inference/align_object_method_scopes.rs b/src/react_compiler/inference/align_object_method_scopes.rs index 9755c1f6a65d..06457fe7aba7 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,22 @@ fn find_scopes_to_merge(func: &HirFunction, env: &Environment) -> DisjointSet { + merged_scopes.union(&[operand_sid, lvalue_sid]); + } + // No scopes exist without memoization. Upstream 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 +76,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 +103,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 +111,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 +180,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: {