Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 30 additions & 13 deletions src/react_compiler/inference/align_object_method_scopes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<ScopeId> {
fn find_scopes_to_merge(
func: &HirFunction,
env: &Environment,
) -> Result<DisjointSet<ScopeId>, CompilerDiagnostic> {
let mut object_method_decls: HashSet<IdentifierId> = HashSet::new();
let mut merged_scopes = DisjointSet::<ScopeId>::new();

Expand All @@ -48,14 +52,22 @@ fn find_scopes_to_merge(func: &HirFunction, env: &Environment) -> DisjointSet<Sc
let lvalue_scope =
env.identifiers[instr.lvalue.identifier.0 as usize].scope;

// TS: CompilerError.invariant(operandScope != null && lvalueScope != null, ...)
let operand_sid = operand_scope.expect(
"Internal error: Expected all ObjectExpressions and ObjectMethods to have non-null scope.",
);
let lvalue_sid = lvalue_scope.expect(
"Internal error: Expected all ObjectExpressions and ObjectMethods to have non-null scope.",
);
merged_scopes.union(&[operand_sid, lvalue_sid]);
match (operand_scope, lvalue_scope) {
(Some(operand_sid), Some(lvalue_sid)) => {
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());
}
}
}
}
}
Expand All @@ -64,7 +76,7 @@ fn find_scopes_to_merge(func: &HirFunction, env: &Environment) -> DisjointSet<Sc
}
}

merged_scopes
Ok(merged_scopes)
}

// =============================================================================
Expand All @@ -75,7 +87,10 @@ fn find_scopes_to_merge(func: &HirFunction, env: &Environment) -> DisjointSet<Sc
/// ObjectExpression share the same scope.
///
/// Corresponds to TS `alignObjectMethodScopes(fn: HIRFunction): void`.
pub(crate) fn align_object_method_scopes(func: &mut HirFunction, env: &mut Environment) {
pub(crate) fn align_object_method_scopes(
func: &mut HirFunction,
env: &mut Environment,
) -> 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 {
Expand All @@ -88,15 +103,15 @@ 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;
}
_ => {}
}
}
}

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,
Expand Down Expand Up @@ -165,4 +180,6 @@ pub(crate) fn align_object_method_scopes(func: &mut HirFunction, env: &mut Envir
}
}
}

Ok(())
}
2 changes: 1 addition & 1 deletion src/react_compiler/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ fn run_hir_passes(
timed!(
"AlignObjectMethodScopes",
crate::inference::align_object_method_scopes(hir, env)
);
)?;

timed!(
"PruneUnusedLabelsHIR",
Expand Down
50 changes: 50 additions & 0 deletions test/bundler/transpiler/react-compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <div data-v={{ m() { return n; } }} />;
}
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 <div data-v={v} />;
}
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: {
Expand Down
Loading