Repository navigation
react_compiler: fix panic on an object method shorthand in ssr output mode - #42375
Conversation
… 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.
|
Status: ready for review. CI passes. Reproduction: on 1.4.3 canary (6a92015) and on main 4b5862f, this aborts with exit 134: // a.jsx
export function App() {
return <div data-v={{ m() {} }} />;
}With this branch the same command exits 0 and prints the component with |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 10 included reviews per hour; 4 remain after this review. WalkthroughThe compiler now returns diagnostics for incomplete object-method scope pairs instead of panicking. The HIR pipeline propagates alignment errors. A new SSR regression test covers compiled object-method shorthand functions. ChangesObject-method scope alignment
Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to The change addresses the reported SSR compiler panic, with no unresolved merge-blocking risk identified. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
LGTM — focused panic→Result conversion with a narrowly-gated no-op arm, mirrors the sibling pass, well-tested.
What was reviewed:
- Confirmed the
(None, None)arm mirrorsalign_method_call_scopes.rs:55and is gated on!enable_memoization()so the invariant still fires when scopes are expected. - Checked that
?on the recursive inner-function call leaving the placeholder inenv.functionson error matches the existing pattern inanalyse_functions.rs:68andinfer_types.rs(env is abandoned on error). - Verified
run_hir_passesalready returnsResultso the new?inpipeline.rscomposes; no CODEOWNERS coverage for these paths. - Test uses a sentinel
useStateso it fails both on the old panic and if any shape gets skipped instead of compiled; covers JSX-attr, body, async, computed-key, nested, and hook variants.
Extended reasoning...
Overview
This PR fixes a user-reachable panic in Bun's React Compiler port. When bundling with --react-compiler --target=bun (SSR mode, memoization disabled), a component containing an object-method shorthand ({ m() {} }) hit .expect() on a None reactive scope in align_object_method_scopes.rs and aborted the process. The fix converts find_scopes_to_merge and align_object_method_scopes to return Result<_, CompilerDiagnostic>, adds a (None, None) if !env.enable_memoization() arm that treats the pair as nothing-to-align, replaces the remaining panic with cold_invariant(...), threads ? through the recursive walk and the pipeline.rs call site, and adds an itBundled regression test covering six shapes of object-method shorthand under target: "bun".
Security risks
None. This is an internal compiler pass over already-lowered HIR; the change replaces a process-aborting panic with a per-function recoverable diagnostic. No parsing of untrusted external data, no I/O, no auth/crypto surface.
Level of scrutiny
Low-to-medium. The change is small (~30 Rust lines) and mechanical: a panic-to-Result conversion plus one match arm. The (None, None) no-op arm is a strictly more conservative variant of the unconditional (None, None) arm already present in the sibling pass align_method_call_scopes.rs, and it is gated on the exact predicate (!env.enable_memoization()) that causes InferReactiveScopeVariables to be skipped in pipeline.rs. The deliberate upstream divergence is commented inline as REVIEW.md requires for ported code. I checked whether the new ? on the recursive inner-function call could leave env.functions[func_id] holding a placeholder on the error path — it can, but this matches the established pattern at analyse_functions.rs:68 and infer_types.rs:486/498, where the whole per-function Environment is abandoned once a pass returns Err.
Other factors
The test is added to the existing react-compiler.test.ts (correct location per CLAUDE.md), uses the itBundled harness, and is designed to fail for the right reason: the fake useState returns "not inlined", so the expected stdout is only produced if the SSR pass actually inlined useState in every tested function — catching both the original SIGABRT and any regression where a shape gets silently skipped instead of compiled. The variant matrix (JSX-attr value, body const, async, computed-key, nested, hook) satisfies the "cover the class, not the repro" rule. No CODEOWNERS entry covers src/react_compiler/ or test/bundler/transpiler/. No prior review activity on the PR. Bug-hunt exit reason was dry_streak with zero findings.
|
Updated 3:19 PM PT - Sep 11th, 2026
✅ @robobun, your commit c8872c3e387900abd4e44b4db154c86cec7f9ccf passed in 🧪 To try this PR locally: bunx bun-pr 42375That installs a local version of the PR into your bun-42375 --bun |
There was a problem hiding this comment.
LGTM — the follow-up commit tightening the comment looks good.
What was reviewed:
- The
(None, None) if !env.enable_memoization()arm mirrors the existing unconditional(None, None)arm inalign_method_call_scopes.rs, and the fallthrough keeps upstream's invariant as a recoverablecold_invariantdiagnostic instead of a process abort. - The
?on the recursive inner-function call sits between themem::replaceswap-out and swap-back, but this is the exact pattern used byenter_ssa.rs,analyse_functions.rs, andinfer_types.rs; on error the wholeenvis discarded byprogram.rs, so the placeholder left inenv.functionsis never observed. - The new
itBundledcase is hermetic (local stubreactpackage, no network), lands in the existing test file, and its sentineluseStateproves each function was actually compiled rather than skipped — it would print"not inlined"on a bail-out.
Extended reasoning...
Overview
This PR converts a user-reachable .expect() panic in the React compiler's AlignObjectMethodScopes HIR pass into a recoverable CompilerDiagnostic. In SSR output mode (--target=bun/--target=node), infer_reactive_scope_variables is gated on enable_memoization() and never runs, so object methods and their enclosing object expressions reach this pass with no reactive scope. The old code called .expect() on both scopes and aborted the whole build. The fix adds a match arm that skips the merge when both scopes are None and memoization is disabled, and returns a cold_invariant error (propagated via ? up through pipeline.rs) for any other mismatch. Three files touched: the pass itself, its one call site in pipeline.rs, and a new itBundled test case.
Security risks
None. This is an internal compiler pass over already-parsed HIR; no new inputs are trusted, no I/O, no allocation or bounds arithmetic changes. The change strictly reduces attack surface by removing a user-triggerable panic = "abort" DoS on valid JSX input.
Level of scrutiny
Low-to-medium. The diff is ~50 lines, mechanical, and follows established in-crate patterns exactly: the sibling pass align_method_call_scopes.rs already has a (None, None) => {} arm, and the mem::replace/?/swap-back sequence for inner-function recursion is identical to enter_ssa.rs:447-449, analyse_functions.rs:63-68, and infer_types.rs:473. REVIEW.md explicitly requires that user-reachable failures be recoverable errors rather than panics, so this is a mandated fix. The PR's gate evidence shows the new test SIGABRTs on the base build and passes with the fix, and the 1806-fixture sweep went from 5 aborts to 0.
Other factors
The test is well-constructed per test/CLAUDE.md: added to the existing react-compiler.test.ts, uses the itBundled harness, vendors a local stub react module (no external network), and asserts exact stdout. The stub useState returns a sentinel that never matches the expected output, so the assertion only passes if the SSR pass actually inlined useState — proving the compiler ran on each of the three shapes (JSX attribute, body with async/computed/nested methods, custom hook) rather than bailing out. Since the prior COMMENTED review, one substantive commit landed (shortening the in-source comment on the no-scope arm); the current comments are terse and load-bearing. No CODEOWNERS entry covers src/react_compiler/, and there are no outstanding CHANGES_REQUESTED reviews from human reviewers.
…ight-hand side (#42386) ### Problem - With `bun build --react-compiler`, `tail.next = tail = node` (list append) throws `TypeError: undefined is not an object (evaluating 'tail.next = tail = {...}')`: the output declares `let tail;` without `= head`. `v.z = v = 80` prints as `80 .z = v = 80`. `arr[i] = i++` stores at the wrong index. Both output modes, 1.4.2 and main. - `lower_simple_assignment` (`src/react_compiler/lowering/build_hir/expr.rs:671`) lowers the right-hand side before the object and key of a member target. JS evaluates the object and key first. So the target reads the value that the right-hand side assigned, and the old value has no reader left. Upstream has the same order. ### Fix - Lower the object, then a computed key, then the right-hand side, then the store. `a.b += c` and `a.b++` already lower in this order. - Correct because codegen prints `object[key] = value`, which JS evaluates in this order. - The upstream fixtures (1,587 in client mode, 1,734 in ssr mode) print the same text before and after. - Verified: `test/bundler/transpiler/react-compiler.test.ts` (2 new tests, 9 forms each, 8 fail on 1.4.3-canary). Also `react-compiler-fixtures.test.ts`, `bundler_jsx.test.ts`. ### Background - The compiler lowers a function to HIR: instructions in evaluation order, each result in a temporary. `a.b = c` becomes a load of `a`, the instructions of `c`, and a `PropertyStore`. - SSA, constant propagation and dead code elimination decide from that order which assignment a read sees. - Codegen puts each temporary back where it is used, so the output has the shape of the source. A wrong order shows only when a pass acted on it. <details><summary>Notes</summary> **Upstream.** babel-plugin-react-compiler 1.0.0 prints the same `let tail;` for the first form. The same order is in `compiler/crates/react_compiler_lowering/src/build_hir.rs` at the ported commit (560db514, "Member expression assignment" arm) and on facebook/react main (019019be). It is also in `BuildHIR.ts`: the `AssignmentExpression` case passes `lowerExpressionToTemporary(builder, expr.get('right'))` as an argument to `lowerAssignment`, and the `MemberExpression` case of `lowerAssignment` lowers the object after that. The new comment in `lower_simple_assignment` records that the port differs here on purpose. **Forms.** 44 forms. Each was built without the compiler, in ssr mode (`--target=bun`) and in client mode (`--target=browser`). Each output then rendered five times with a memo cache that persists between renders: the same props twice, two other prop sets, then the first props again. The compiled outputs must print what the uncompiled output prints. On 1.4.3-canary (6a92015) 17 forms differ. On this branch none differ, and client mode memoizes 42 of the 44 (the other two break a rule of React on purpose and are left uncompiled on both builds). Canary is two commits behind main and neither commit touches the lowering. A debug build of main (4b5862f) fails the same way on the 31 forms I also ran there. | form | expected | 1.4.3-canary | | --- | --- | --- | | `tail.next = tail = {...}` in `for of`, `for`, `while`, and in the update clause of a `for` | `0,1,2,3` | `TypeError: undefined is not an object` | | the same twice with no loop | the list `0,5,1` | the same TypeError | | `tail[p.key] = tail = {...}` and `tail[0] = tail = {...}` | `0,1,2,3` | the same TypeError | | `tail.next = tail = p.make(x)` | `0,1,2,3` | the same TypeError | | the same inside `try`/`catch` | `0,1,2,3` | the `catch` branch renders | | the `for of` form inside an arrow function | `0,1,2,3` | the same TypeError | | `box.child = box = { el: <span/> }`, `box.fn = box = { get: () => p.a }` | the old `box` gets the key | the same TypeError | | `v.z = v = 80` in an `if`, in a loop, at the top level | `[{"z":80},80]` | `TypeError: Attempted to assign to readonly property.` | | `o[key] = key = "b"` with `let key = "a"` | `[{"a":"b"},"b"]` | `[{"b":"b"},"b"]` | | `arr[i] = i++; arr[i] = i++; arr[i++] = i` with `let i = 0` | `[[0,1,3],3]` | `[[null,0,2],3]` | | `let a = {name: "a"}; if (p.flag) a.other = a = b;` | with `flag` false: `{"name":"a"}` | client mode, after a render with `flag` true: `{"name":"a","other":{"name":"b"}}` | The last row is the second effect in the report. The compiler saw no write to the first object, cached it behind a sentinel check, and the store then changed the cached object for every later render. 27 forms already matched on canary and still match: `v.z = v = p.a` and `v.z = v = {...}` at the top level, the `arr[i] = i++` forms inside a loop (no constant to propagate), `arr[0] = arr = [...]`, `a.b.c = a = {...}`, `o.x = o.y = o = {...}`, a sequence, conditional, logical, nullish or optional chain on the right, `(a = {...}).x = a.name`, `tail = tail.next = {...}`, a call as the object and as the key (the calls run in source order), `useState()` on the right, a local that a closure captures, `reduce((tail, x) => (tail.next = tail = {...}), head)`, `v.z += (v = {...}).z`, `seen.push(v, (v = {...}))`, and destructuring targets (`[a.x, a] = ...`, `[a, a.y] = ...`). **Why many forms already worked.** With the instructions in the wrong order the printed text is still `tail.next = tail = {...}`, in source order, because codegen puts the temporaries back at their place of use. The output is wrong only when a pass used the order: dead code elimination (the removed `= head`), constant propagation (`80 .z`, `o["b"]`, `arr[1] = i++`), or the mutation analysis (the last row of the table). **One form now compiles.** At the top level of a component, `let v = {}; const o = v; v.z = v = p.a;` was left uncompiled in client mode. With the wrong order the store looked like a write to the prop `p.a`, which the compiler rejects. Now the store goes to the local object and the component is memoized. **Other assignment lowerings.** A member target inside a destructuring pattern (`[a.b] = c`) or in `for (a.b of c)` goes through `lower_member_store` with a value that already exists. JS also produces that value before it evaluates the target, so those stay as they are. Calls, compound assignment and update expressions lower their operands in source order. **Fixture corpus.** I compiled every upstream fixture that builds (1,587 in one `Bun.build` call in client mode, 1,734 with one `bun build` per file in ssr mode) with a debug build of main and with a debug build of this branch, and compared the output files. No file differs. So the memo slot counts and scope boundaries of the corpus are unchanged. Five fixtures panic in ssr mode on both builds (`Expected all ObjectExpressions and ObjectMethods to have non-null scope`), which #42375 fixes. </details> <!-- robobun:evidence:begin --> --- **[human-review]** gate passed · iteration 0 · 2 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: 2 FAILED $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/transpiler/react-compiler.test.ts bun test v1.4.3 (6a92015) test/bundler/transpiler/react-compiler.test.ts: (pass) bundler > react-compiler/SimpleComponent [949.44ms] (pass) bundler > react-compiler/ComponentWithHooks [341.77ms] (pass) bundler > react-compiler/ObjectPatternRestInProps [454.80ms] (pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [526.88ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [178.43ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [156.04ms] (pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [548.32ms] (pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [156.65ms] (pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [226.14ms] (pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [156.10ms] (pass) bundler > react-compiler/BundledReactPreservesImportRefs [384.19ms] (pass) bundler > react-compiler/BundledCjsCompilerRuntimeSurvivesTreeShaking [742.38ms] ( ... (truncated) release without fix: 11 failed, 1 skipped bun test v1.4.3-canary.1 (c800ac8) test/bundler/transpiler/react-compiler.test.ts: (pass) bundler > react-compiler/SimpleComponent [37.06ms] (pass) bundler > react-compiler/ComponentWithHooks [13.21ms] (pass) bundler > react-compiler/ObjectPatternRestInProps [14.65ms] (pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [15.81ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [10.81ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [9.10ms] (pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [29.97ms] (pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [11.46ms] (pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [26.57ms] (pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [12.37ms] (pass) bundler > react-compiler/BundledReactPreservesImportRefs [14.27ms] (pass) bundler > react-compiler/BundledCjsCompilerRuntimeSurvivesTreeShaking [30.20ms] (pass) bundler > react-compiler/RequireStringPreservesImportRecord [18.43ms] (pass) bundler > react-compiler/BranchBooleanFeatureFlagPreservesDCE [10.10ms] (pass) bundler > react-compi ... (truncated) ``` </details> <details><summary>passes on PR (with fix)</summary> ```console ASAN with fix: all passed $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/transpiler/react-compiler.test.ts bun test v1.4.3 (6a92015) test/bundler/transpiler/react-compiler.test.ts: (pass) bundler > react-compiler/SimpleComponent [1041.63ms] (pass) bundler > react-compiler/ComponentWithHooks [316.85ms] (pass) bundler > react-compiler/ObjectPatternRestInProps [368.95ms] (pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [367.44ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [193.38ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [152.07ms] (pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [520.79ms] (pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [146.70ms] (pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [148.27ms] (pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [137.77ms] (pass) bundler > react-compiler/BundledReactPreservesImportRefs [485.46ms] (pass) bundler > react-compiler/BundledCjsCompilerRuntimeSurvivesTreeShaking [656.34ms] ... (truncated) release with fix: 1 skipped $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) in 815ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [0/5] cargo bun_runtime → libbun_runtime.a �[1m�[92m Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core) �[1m�[92m Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno) �[1m�[92m Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr) �[1m�[92m Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys) �[1m�[92m Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety) �[1m�[92m Compiling�[0m bun_base64 v0.0.0 (/workspace/bun/src/base64) �[1m�[92m Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys) �[1m�[92m Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys) �[1m�[92m Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd) �[1m�[92m Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp) �[1m�[92m Compiling�[0m bun_brotli v0.0.0 (/workspace/bun/src/brotli) �[1m�[92m Compiling�[0m bun_output v0.0.0 (/workspace/bun/src/output) �[1m�[92m Compiling�[0m bun_clap v0.0.0 (/workspace/bun/src/clap) �[1m�[92m Compiling�[0m bu ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` src/react_compiler/lowering/build_hir/expr.rs | 6 +- test/bundler/transpiler/react-compiler.test.ts | 136 +++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 2 deletions(-) ``` </details> **gate history** · 2 passed · 0 rejected · iteration 0 <details><summary>evidence per changed file</summary> ``` file reads edits tests src/react_compiler/lowering/build_hir/expr.rs 3 2 20 test/bundler/transpiler/react-compiler.test.ts 3 1 20 ``` </details> <!-- robobun:evidence:end -->
) ### Problem - `bun build --react-compiler --target=browser` aborts on `const r = (m = p.f()) ? 1 : 0; return <div data-v={[m, r]} />`: `panic: Expected a node for all scopes`. `Bun.build` aborts the calling process the same way. - `CollectDependenciesVisitor` skips the test of a `?:`, so the reactive scope that reassigns `m` gets no scope node. `visit_scope` still attaches that scope to `m`. When `m` is force-memoized, `force_memoize_scope_dependencies` (`src/react_compiler/reactive_scopes/prune_non_escaping_scopes.rs:1170`) calls `.expect()` on the missing node. ### Fix - That invariant is now a `cold_invariant` error. `program.rs` leaves that one function uncompiled, like upstream's `CompilerError.invariant`. - The three `Expected identifier to be initialized` invariants in the same pass get the same treatment. Their visitor callbacks return `()`, so they record the first error in `CollectState`, as `assert_scope_instructions_within_scopes` does. - Correct because babel-plugin-react-compiler 1.0.0 raises the same invariant on the same inputs and skips the same functions. Compiled output does not change. - Verified: `test/bundler/transpiler/react-compiler.test.ts` (new `AssignmentInConditionalTestSkipsOnlyThatFunction`, SIGABRT on 1.4.3 canary). Also `react-compiler-fixtures.test.ts`. ### Background - The React Compiler groups values that change together into reactive scopes and emits one memo block per scope. PruneNonEscapingScopes removes the scopes whose values never leave the function. - The pass builds a graph of identifier nodes and scope nodes, then walks it from the returned values. - Upstream's `CompilerError.invariant` is a caught per-function error in TS. Bun builds with `panic = "abort"`, so a ported `.expect()` ends the process. <details><summary>Notes</summary> Repro (any directory, 1.4.2, 1.4.3 canary 6a92015 and main 4b5862f): ```jsx // a.jsx export default function App(p) { let m; const r = (m = p.f()) ? 1 : 0; return <div data-v={[m, r]} />; } ``` ``` $ bun build --react-compiler --target=browser --external react a.jsx panic: Expected a node for all scopes Crashed while visiting a.jsx ``` Frames: `force_memoize_scope_dependencies` (`prune_non_escaping_scopes.rs:1170`), `compute_memoized_identifiers::visit` (`:1156`), `prune_non_escaping_scopes` (`:70`), `pipeline::run_hir_passes` (`pipeline.rs:622`), `compile_fn` (`:172`). The smallest shape: an assignment whose value allocates (a call, an array or an object literal) in the test of a `?:`, and the assigned local as an operand of a memoized value (`[m]`, `{ m }`). The result of the conditional does not need to reach that value. `m = p.a` compiles. `&&`, `??`, `?.` or an `if` statement in place of `?:` compile. `<div data-m={m} />` compiles because nothing force-memoizes `m`. `--target=bun` and `--target=node` (ssr mode) create no reactive scopes and compile. A second shape, with no assignment expression (the input of react/react#37228): `return (() => { try { return check(raw) ? raw : null; } catch { return null; } })();`. 1.4.2 aborts with the same panic and this branch skips the function with the same invariant. babel-plugin-react-compiler 1.0.0 skips it one step earlier, with `Todo: Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement`. Two loop shapes with no `?:` at all, where `identity` is an imported function: `let r; do { r = identity(value); } while (cond()); return r;` and `let r; while (cond()) { r = identity(value); break; } return r;`. 1.4.2 aborts on both with the same panic, this branch skips the function, and babel-plugin-react-compiler 1.0.0 raises the same invariant and leaves the function as written. With `String(value)` in place of `identity(value)` nothing aborts. I ran both by hand on this branch. They are not in the test. Why the node is missing. `compute_memoization_inputs` returns the rvalues of the consequent and the alternate of a `ConditionalExpression` only (upstream: "Conditionals do not alias their test value"), and `visit_instruction` does not traverse nested values on its own. So nothing calls `visit_operand` for `t = p.f()` or `m = t`, and `visit_operand` is the only place that creates a scope node. The scope is aligned to the whole `const r = ...` statement and lists `m` in `reassignments`, so `visit_scope` adds it to the node of `m`. The array `[m]` is `Memoized`, its scope depends on `m`, `m` is `Unmemoized` (from `let m;`) and forced, and the walk reaches the scope with no node. `(m = p.f()) ? p.a() : p.b()` compiles because the calls in the branches are in the same scope and create the node. Upstream. `PruneNonEscapingScopes.ts` on facebook/react main still has all four invariants, and `react_compiler_reactive_scopes/src/prune_non_escaping_scopes.rs` there has the same four `.expect()` calls. I ran babel-plugin-react-compiler 1.0.0 on 20 variants of the input. It logs `CompileError: Expected a node for all scopes` and leaves the function as written for exactly the 10 variants that abort 1.4.3 canary. The two agree on the other 10 as well: 8 compile with one memo cache, 1 compiles with none, and 1 fails in both with `Unexpected StoreLocal in codegenInstructionValue`, which Bun already returns as an error. With this change Bun skips the same 10 functions. Alternative not taken: create the scope node on demand in `force_memoize_scope_dependencies` from `env.scopes[id].dependencies`, which is all `visit_operand` stores. That compiles these functions, but upstream does not compile them, and nothing in that test value has been analysed by this pass. The three `Expected identifier to be initialized` sites. Upstream never reaches them for the inputs below, but two other bugs of the port do. `visit_operand` (`:195`): a closure above a `let` that it reads and that is reassigned later, for example `const row = () => <Row items={items} />; let items = p.items; if (p.c) items = [];`. The lowering did not declare `items` before the closure there. #42390 fixed that (merged), and upstream compiles and memoizes those functions. `visit_scope` (`:1060`): dead code elimination dropped `let v` but kept a store to it, fixed in #42385 (merged). Those inputs were not parity cases, and they compile on main now. The conversion here is for the next input that reaches one of these sites. To exercise the three paths on their own I also forced each lookup to miss in a local build (one site at a time, selected by an environment variable, not committed). Each time `bun build` exited 0, the debug log showed `compile_fn err: ... "Expected identifier to be initialized"` for the affected functions, and the other functions in the file still compiled. Not changed here: the other upstream invariants in this crate that are still `.expect()` or `assert!` (`build_reactive_scope_terminals_hir.rs:137/145/173`, `propagate_scope_dependencies_hir.rs:90/1772`, `align_reactive_scopes_to_block_scopes_hir.rs:262`, `eliminate_redundant_phi.rs:75`, `build_reactive_function.rs:216/1388`). No known input reaches them and each needs `Result` plumbing through a different pass. `align_object_method_scopes.rs` is #42375. What the test checks. It bundles one file with `--target=browser` through the CLI and runs the output against a fake `react/compiler-runtime` whose `c` counts its calls. Four functions have the shape (call, array and object in the test, a `let m = null` initializer, a hook that returns the array, the conditional nested in a binary expression). A fifth is the react/react#37228 input, a `?:` in a `try` in an IIFE. Each returns the right value and makes 0 calls to `c`. Two controls (the same `?:` with `m` not memoized, and a plain component) make 1 call each, so the skip is per function and not per file. Each of the five aborts 1.4.2 on its own. Sweep: built all 1807 source files under `test/bundler/transpiler/react-compiler-fixtures/` with `bun build --react-compiler --target=browser --external '*'` on 1.4.3 canary. None aborts, so no upstream fixture has this shape. Suites run with the debug build, after the rebase on 7a7cc96: `test/bundler/transpiler/react-compiler.test.ts` (58 pass), `test/bundler/transpiler/react-compiler-fixtures.test.ts` (3293 pass, 320 skip). `cargo clippy -p bun_react_compiler` and `cargo fmt --check` are clean. </details> <!-- robobun:evidence:begin --> --- **[human-review]** gate passed · iteration 1 · 2 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: 2 FAILED $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/transpiler/react-compiler.test.ts bun test v1.4.3 (6a92015) test/bundler/transpiler/react-compiler.test.ts: (pass) bundler > react-compiler/SimpleComponent [1077.04ms] (pass) bundler > react-compiler/ComponentWithHooks [1018.66ms] (pass) bundler > react-compiler/ObjectPatternRestInProps [466.17ms] (pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [677.15ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [586.00ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [413.21ms] (pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [963.12ms] (pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [227.16ms] (pass) bundler > react-compiler/SsrObjectMethodShorthand [1037.88ms] (pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [325.23ms] (pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [307.01ms] (pass) bundler > react-compiler/BundledReactPreservesImportRefs [538.68ms] (pass) bundler > r ... (truncated) release without fix: 12 failed, 1 skipped bun test v1.4.3-canary.1 (3c62dfb) test/bundler/transpiler/react-compiler.test.ts: (pass) bundler > react-compiler/SimpleComponent [63.16ms] (pass) bundler > react-compiler/ComponentWithHooks [20.36ms] (pass) bundler > react-compiler/ObjectPatternRestInProps [11.72ms] (pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [14.43ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [30.53ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [7.03ms] (pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [24.77ms] (pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [11.01ms] 1031 | ]); 1032 | const stdout = Buffer.from(stdoutBytes); 1033 | const stderr = Buffer.from(stderrBytes); 1034 | const success = exitCode === 0; 1035 | if (buildProc.signalCode) { 1036 | throw new Error( ^ error: [react-compiler/SsrObjectMethodShorthand] 'bun build' subprocess killed by SIGABRT cmd: /workspace/bun/build/release/bun build /tmp/bun-build-tests/bun-eKoWEm/react-compiler/SsrObjectMethodShorthand/entry.jsx --outfile=/tmp/bun-build-tests ... (truncated) ``` </details> <details><summary>passes on PR (with fix)</summary> ```console ASAN with fix: all passed $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/transpiler/react-compiler.test.ts bun test v1.4.3 (6a92015) test/bundler/transpiler/react-compiler.test.ts: (pass) bundler > react-compiler/SimpleComponent [984.04ms] (pass) bundler > react-compiler/ComponentWithHooks [341.71ms] (pass) bundler > react-compiler/ObjectPatternRestInProps [417.08ms] (pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [333.24ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [192.84ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [179.22ms] (pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [421.84ms] (pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [149.25ms] (pass) bundler > react-compiler/SsrObjectMethodShorthand [850.95ms] (pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [149.08ms] (pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [164.78ms] (pass) bundler > react-compiler/BundledReactPreservesImportRefs [344.39ms] (pass) bundler > reac ... (truncated) release with fix: 1 skipped $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) in 814ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [0/4] cargo bun_runtime → libbun_runtime.a ^[[1m^[[92m Compiling^[[0m bun_react_compiler v0.0.0 (/workspace/bun/src/react_compiler) ^[[1m^[[92m Compiling^[[0m bun_js_parser v0.0.0 (/workspace/bun/src/js_parser) ^[[1m^[[92m Compiling^[[0m bun_resolver v0.0.0 (/workspace/bun/src/resolver) ^[[1m^[[92m Compiling^[[0m bun_ini v0.0.0 (/workspace/bun/src/ini) ^[[1m^[[92m Compiling^[[0m bun_bundler v0.0.0 (/workspace/bun/src/bundler) ^[[1m^[[92m Compiling^[[0m bun_router v0.0.0 (/workspace/bun/src/router) ^[[1m^[[92m Compiling^[[0m bun_standalone_graph v0.0.0 (/workspace/bun/src/standalone_graph) ^[[1m^[[92m Compiling^[[0m bun_transpiler v0.0.0 (/workspace/bun/src/transpiler) ^[[1m^[[92m Compiling^[[0m bun_bunfig v0.0.0 (/workspace/bun/src/bunfig) ^[[1m^[[92m Compiling^[[0m bun_install v0.0.0 (/workspace/bun/src/install) ^[[1m^[[92m Compiling^[[0m bun_jsc v0.0.0 (/workspace/bun/src/jsc) ^[[1m^[[92m Compiling^[[0m bun_js_parser_jsc v0.0.0 (/workspace/bun/src/js_parser_jsc) ^[[1m^[[92m Compiling^[[0m bun_http_jsc v0.0.0 (/work ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` .../reactive_scopes/prune_non_escaping_scopes.rs | 103 +++++++++++++-------- test/bundler/transpiler/react-compiler.test.ts | 103 +++++++++++++++++++++ 2 files changed, 166 insertions(+), 40 deletions(-) ``` </details> **gate history** · 2 passed · 0 rejected · iteration 1 <details><summary>evidence per changed file</summary> ``` file reads edits tests …t_compiler/reactive_scopes/prune_non_escaping_scopes.rs 6 12 29 test/bundler/transpiler/react-compiler.test.ts 2 4 29 ``` </details> <!-- robobun:evidence:end -->
…ight-hand side (oven-sh#42386) ### Problem - With `bun build --react-compiler`, `tail.next = tail = node` (list append) throws `TypeError: undefined is not an object (evaluating 'tail.next = tail = {...}')`: the output declares `let tail;` without `= head`. `v.z = v = 80` prints as `80 .z = v = 80`. `arr[i] = i++` stores at the wrong index. Both output modes, 1.4.2 and main. - `lower_simple_assignment` (`src/react_compiler/lowering/build_hir/expr.rs:671`) lowers the right-hand side before the object and key of a member target. JS evaluates the object and key first. So the target reads the value that the right-hand side assigned, and the old value has no reader left. Upstream has the same order. ### Fix - Lower the object, then a computed key, then the right-hand side, then the store. `a.b += c` and `a.b++` already lower in this order. - Correct because codegen prints `object[key] = value`, which JS evaluates in this order. - The upstream fixtures (1,587 in client mode, 1,734 in ssr mode) print the same text before and after. - Verified: `test/bundler/transpiler/react-compiler.test.ts` (2 new tests, 9 forms each, 8 fail on 1.4.3-canary). Also `react-compiler-fixtures.test.ts`, `bundler_jsx.test.ts`. ### Background - The compiler lowers a function to HIR: instructions in evaluation order, each result in a temporary. `a.b = c` becomes a load of `a`, the instructions of `c`, and a `PropertyStore`. - SSA, constant propagation and dead code elimination decide from that order which assignment a read sees. - Codegen puts each temporary back where it is used, so the output has the shape of the source. A wrong order shows only when a pass acted on it. <details><summary>Notes</summary> **Upstream.** babel-plugin-react-compiler 1.0.0 prints the same `let tail;` for the first form. The same order is in `compiler/crates/react_compiler_lowering/src/build_hir.rs` at the ported commit (560db514, "Member expression assignment" arm) and on facebook/react main (019019be). It is also in `BuildHIR.ts`: the `AssignmentExpression` case passes `lowerExpressionToTemporary(builder, expr.get('right'))` as an argument to `lowerAssignment`, and the `MemberExpression` case of `lowerAssignment` lowers the object after that. The new comment in `lower_simple_assignment` records that the port differs here on purpose. **Forms.** 44 forms. Each was built without the compiler, in ssr mode (`--target=bun`) and in client mode (`--target=browser`). Each output then rendered five times with a memo cache that persists between renders: the same props twice, two other prop sets, then the first props again. The compiled outputs must print what the uncompiled output prints. On 1.4.3-canary (6a92015) 17 forms differ. On this branch none differ, and client mode memoizes 42 of the 44 (the other two break a rule of React on purpose and are left uncompiled on both builds). Canary is two commits behind main and neither commit touches the lowering. A debug build of main (4b5862f) fails the same way on the 31 forms I also ran there. | form | expected | 1.4.3-canary | | --- | --- | --- | | `tail.next = tail = {...}` in `for of`, `for`, `while`, and in the update clause of a `for` | `0,1,2,3` | `TypeError: undefined is not an object` | | the same twice with no loop | the list `0,5,1` | the same TypeError | | `tail[p.key] = tail = {...}` and `tail[0] = tail = {...}` | `0,1,2,3` | the same TypeError | | `tail.next = tail = p.make(x)` | `0,1,2,3` | the same TypeError | | the same inside `try`/`catch` | `0,1,2,3` | the `catch` branch renders | | the `for of` form inside an arrow function | `0,1,2,3` | the same TypeError | | `box.child = box = { el: <span/> }`, `box.fn = box = { get: () => p.a }` | the old `box` gets the key | the same TypeError | | `v.z = v = 80` in an `if`, in a loop, at the top level | `[{"z":80},80]` | `TypeError: Attempted to assign to readonly property.` | | `o[key] = key = "b"` with `let key = "a"` | `[{"a":"b"},"b"]` | `[{"b":"b"},"b"]` | | `arr[i] = i++; arr[i] = i++; arr[i++] = i` with `let i = 0` | `[[0,1,3],3]` | `[[null,0,2],3]` | | `let a = {name: "a"}; if (p.flag) a.other = a = b;` | with `flag` false: `{"name":"a"}` | client mode, after a render with `flag` true: `{"name":"a","other":{"name":"b"}}` | The last row is the second effect in the report. The compiler saw no write to the first object, cached it behind a sentinel check, and the store then changed the cached object for every later render. 27 forms already matched on canary and still match: `v.z = v = p.a` and `v.z = v = {...}` at the top level, the `arr[i] = i++` forms inside a loop (no constant to propagate), `arr[0] = arr = [...]`, `a.b.c = a = {...}`, `o.x = o.y = o = {...}`, a sequence, conditional, logical, nullish or optional chain on the right, `(a = {...}).x = a.name`, `tail = tail.next = {...}`, a call as the object and as the key (the calls run in source order), `useState()` on the right, a local that a closure captures, `reduce((tail, x) => (tail.next = tail = {...}), head)`, `v.z += (v = {...}).z`, `seen.push(v, (v = {...}))`, and destructuring targets (`[a.x, a] = ...`, `[a, a.y] = ...`). **Why many forms already worked.** With the instructions in the wrong order the printed text is still `tail.next = tail = {...}`, in source order, because codegen puts the temporaries back at their place of use. The output is wrong only when a pass used the order: dead code elimination (the removed `= head`), constant propagation (`80 .z`, `o["b"]`, `arr[1] = i++`), or the mutation analysis (the last row of the table). **One form now compiles.** At the top level of a component, `let v = {}; const o = v; v.z = v = p.a;` was left uncompiled in client mode. With the wrong order the store looked like a write to the prop `p.a`, which the compiler rejects. Now the store goes to the local object and the component is memoized. **Other assignment lowerings.** A member target inside a destructuring pattern (`[a.b] = c`) or in `for (a.b of c)` goes through `lower_member_store` with a value that already exists. JS also produces that value before it evaluates the target, so those stay as they are. Calls, compound assignment and update expressions lower their operands in source order. **Fixture corpus.** I compiled every upstream fixture that builds (1,587 in one `Bun.build` call in client mode, 1,734 with one `bun build` per file in ssr mode) with a debug build of main and with a debug build of this branch, and compared the output files. No file differs. So the memo slot counts and scope boundaries of the corpus are unchanged. Five fixtures panic in ssr mode on both builds (`Expected all ObjectExpressions and ObjectMethods to have non-null scope`), which oven-sh#42375 fixes. </details> <!-- robobun:evidence:begin --> --- **[human-review]** gate passed · iteration 0 · 2 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: 2 FAILED $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/transpiler/react-compiler.test.ts bun test v1.4.3 (6a92015) test/bundler/transpiler/react-compiler.test.ts: (pass) bundler > react-compiler/SimpleComponent [949.44ms] (pass) bundler > react-compiler/ComponentWithHooks [341.77ms] (pass) bundler > react-compiler/ObjectPatternRestInProps [454.80ms] (pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [526.88ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [178.43ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [156.04ms] (pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [548.32ms] (pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [156.65ms] (pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [226.14ms] (pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [156.10ms] (pass) bundler > react-compiler/BundledReactPreservesImportRefs [384.19ms] (pass) bundler > react-compiler/BundledCjsCompilerRuntimeSurvivesTreeShaking [742.38ms] ( ... (truncated) release without fix: 11 failed, 1 skipped bun test v1.4.3-canary.1 (c800ac8) test/bundler/transpiler/react-compiler.test.ts: (pass) bundler > react-compiler/SimpleComponent [37.06ms] (pass) bundler > react-compiler/ComponentWithHooks [13.21ms] (pass) bundler > react-compiler/ObjectPatternRestInProps [14.65ms] (pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [15.81ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [10.81ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [9.10ms] (pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [29.97ms] (pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [11.46ms] (pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [26.57ms] (pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [12.37ms] (pass) bundler > react-compiler/BundledReactPreservesImportRefs [14.27ms] (pass) bundler > react-compiler/BundledCjsCompilerRuntimeSurvivesTreeShaking [30.20ms] (pass) bundler > react-compiler/RequireStringPreservesImportRecord [18.43ms] (pass) bundler > react-compiler/BranchBooleanFeatureFlagPreservesDCE [10.10ms] (pass) bundler > react-compi ... (truncated) ``` </details> <details><summary>passes on PR (with fix)</summary> ```console ASAN with fix: all passed $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/transpiler/react-compiler.test.ts bun test v1.4.3 (6a92015) test/bundler/transpiler/react-compiler.test.ts: (pass) bundler > react-compiler/SimpleComponent [1041.63ms] (pass) bundler > react-compiler/ComponentWithHooks [316.85ms] (pass) bundler > react-compiler/ObjectPatternRestInProps [368.95ms] (pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [367.44ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [193.38ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [152.07ms] (pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [520.79ms] (pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [146.70ms] (pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [148.27ms] (pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [137.77ms] (pass) bundler > react-compiler/BundledReactPreservesImportRefs [485.46ms] (pass) bundler > react-compiler/BundledCjsCompilerRuntimeSurvivesTreeShaking [656.34ms] ... (truncated) release with fix: 1 skipped $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) in 815ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [0/5] cargo bun_runtime → libbun_runtime.a �[1m�[92m Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core) �[1m�[92m Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno) �[1m�[92m Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr) �[1m�[92m Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys) �[1m�[92m Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety) �[1m�[92m Compiling�[0m bun_base64 v0.0.0 (/workspace/bun/src/base64) �[1m�[92m Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys) �[1m�[92m Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys) �[1m�[92m Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd) �[1m�[92m Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp) �[1m�[92m Compiling�[0m bun_brotli v0.0.0 (/workspace/bun/src/brotli) �[1m�[92m Compiling�[0m bun_output v0.0.0 (/workspace/bun/src/output) �[1m�[92m Compiling�[0m bun_clap v0.0.0 (/workspace/bun/src/clap) �[1m�[92m Compiling�[0m bu ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` src/react_compiler/lowering/build_hir/expr.rs | 6 +- test/bundler/transpiler/react-compiler.test.ts | 136 +++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 2 deletions(-) ``` </details> **gate history** · 2 passed · 0 rejected · iteration 0 <details><summary>evidence per changed file</summary> ``` file reads edits tests src/react_compiler/lowering/build_hir/expr.rs 3 2 20 test/bundler/transpiler/react-compiler.test.ts 3 1 20 ``` </details> <!-- robobun:evidence:end -->
…n-sh#42378) ### Problem - `bun build --react-compiler --target=browser` aborts on `const r = (m = p.f()) ? 1 : 0; return <div data-v={[m, r]} />`: `panic: Expected a node for all scopes`. `Bun.build` aborts the calling process the same way. - `CollectDependenciesVisitor` skips the test of a `?:`, so the reactive scope that reassigns `m` gets no scope node. `visit_scope` still attaches that scope to `m`. When `m` is force-memoized, `force_memoize_scope_dependencies` (`src/react_compiler/reactive_scopes/prune_non_escaping_scopes.rs:1170`) calls `.expect()` on the missing node. ### Fix - That invariant is now a `cold_invariant` error. `program.rs` leaves that one function uncompiled, like upstream's `CompilerError.invariant`. - The three `Expected identifier to be initialized` invariants in the same pass get the same treatment. Their visitor callbacks return `()`, so they record the first error in `CollectState`, as `assert_scope_instructions_within_scopes` does. - Correct because babel-plugin-react-compiler 1.0.0 raises the same invariant on the same inputs and skips the same functions. Compiled output does not change. - Verified: `test/bundler/transpiler/react-compiler.test.ts` (new `AssignmentInConditionalTestSkipsOnlyThatFunction`, SIGABRT on 1.4.3 canary). Also `react-compiler-fixtures.test.ts`. ### Background - The React Compiler groups values that change together into reactive scopes and emits one memo block per scope. PruneNonEscapingScopes removes the scopes whose values never leave the function. - The pass builds a graph of identifier nodes and scope nodes, then walks it from the returned values. - Upstream's `CompilerError.invariant` is a caught per-function error in TS. Bun builds with `panic = "abort"`, so a ported `.expect()` ends the process. <details><summary>Notes</summary> Repro (any directory, 1.4.2, 1.4.3 canary 6a92015 and main 4b5862f): ```jsx // a.jsx export default function App(p) { let m; const r = (m = p.f()) ? 1 : 0; return <div data-v={[m, r]} />; } ``` ``` $ bun build --react-compiler --target=browser --external react a.jsx panic: Expected a node for all scopes Crashed while visiting a.jsx ``` Frames: `force_memoize_scope_dependencies` (`prune_non_escaping_scopes.rs:1170`), `compute_memoized_identifiers::visit` (`:1156`), `prune_non_escaping_scopes` (`:70`), `pipeline::run_hir_passes` (`pipeline.rs:622`), `compile_fn` (`:172`). The smallest shape: an assignment whose value allocates (a call, an array or an object literal) in the test of a `?:`, and the assigned local as an operand of a memoized value (`[m]`, `{ m }`). The result of the conditional does not need to reach that value. `m = p.a` compiles. `&&`, `??`, `?.` or an `if` statement in place of `?:` compile. `<div data-m={m} />` compiles because nothing force-memoizes `m`. `--target=bun` and `--target=node` (ssr mode) create no reactive scopes and compile. A second shape, with no assignment expression (the input of react/react#37228): `return (() => { try { return check(raw) ? raw : null; } catch { return null; } })();`. 1.4.2 aborts with the same panic and this branch skips the function with the same invariant. babel-plugin-react-compiler 1.0.0 skips it one step earlier, with `Todo: Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement`. Two loop shapes with no `?:` at all, where `identity` is an imported function: `let r; do { r = identity(value); } while (cond()); return r;` and `let r; while (cond()) { r = identity(value); break; } return r;`. 1.4.2 aborts on both with the same panic, this branch skips the function, and babel-plugin-react-compiler 1.0.0 raises the same invariant and leaves the function as written. With `String(value)` in place of `identity(value)` nothing aborts. I ran both by hand on this branch. They are not in the test. Why the node is missing. `compute_memoization_inputs` returns the rvalues of the consequent and the alternate of a `ConditionalExpression` only (upstream: "Conditionals do not alias their test value"), and `visit_instruction` does not traverse nested values on its own. So nothing calls `visit_operand` for `t = p.f()` or `m = t`, and `visit_operand` is the only place that creates a scope node. The scope is aligned to the whole `const r = ...` statement and lists `m` in `reassignments`, so `visit_scope` adds it to the node of `m`. The array `[m]` is `Memoized`, its scope depends on `m`, `m` is `Unmemoized` (from `let m;`) and forced, and the walk reaches the scope with no node. `(m = p.f()) ? p.a() : p.b()` compiles because the calls in the branches are in the same scope and create the node. Upstream. `PruneNonEscapingScopes.ts` on facebook/react main still has all four invariants, and `react_compiler_reactive_scopes/src/prune_non_escaping_scopes.rs` there has the same four `.expect()` calls. I ran babel-plugin-react-compiler 1.0.0 on 20 variants of the input. It logs `CompileError: Expected a node for all scopes` and leaves the function as written for exactly the 10 variants that abort 1.4.3 canary. The two agree on the other 10 as well: 8 compile with one memo cache, 1 compiles with none, and 1 fails in both with `Unexpected StoreLocal in codegenInstructionValue`, which Bun already returns as an error. With this change Bun skips the same 10 functions. Alternative not taken: create the scope node on demand in `force_memoize_scope_dependencies` from `env.scopes[id].dependencies`, which is all `visit_operand` stores. That compiles these functions, but upstream does not compile them, and nothing in that test value has been analysed by this pass. The three `Expected identifier to be initialized` sites. Upstream never reaches them for the inputs below, but two other bugs of the port do. `visit_operand` (`:195`): a closure above a `let` that it reads and that is reassigned later, for example `const row = () => <Row items={items} />; let items = p.items; if (p.c) items = [];`. The lowering did not declare `items` before the closure there. oven-sh#42390 fixed that (merged), and upstream compiles and memoizes those functions. `visit_scope` (`:1060`): dead code elimination dropped `let v` but kept a store to it, fixed in oven-sh#42385 (merged). Those inputs were not parity cases, and they compile on main now. The conversion here is for the next input that reaches one of these sites. To exercise the three paths on their own I also forced each lookup to miss in a local build (one site at a time, selected by an environment variable, not committed). Each time `bun build` exited 0, the debug log showed `compile_fn err: ... "Expected identifier to be initialized"` for the affected functions, and the other functions in the file still compiled. Not changed here: the other upstream invariants in this crate that are still `.expect()` or `assert!` (`build_reactive_scope_terminals_hir.rs:137/145/173`, `propagate_scope_dependencies_hir.rs:90/1772`, `align_reactive_scopes_to_block_scopes_hir.rs:262`, `eliminate_redundant_phi.rs:75`, `build_reactive_function.rs:216/1388`). No known input reaches them and each needs `Result` plumbing through a different pass. `align_object_method_scopes.rs` is oven-sh#42375. What the test checks. It bundles one file with `--target=browser` through the CLI and runs the output against a fake `react/compiler-runtime` whose `c` counts its calls. Four functions have the shape (call, array and object in the test, a `let m = null` initializer, a hook that returns the array, the conditional nested in a binary expression). A fifth is the react/react#37228 input, a `?:` in a `try` in an IIFE. Each returns the right value and makes 0 calls to `c`. Two controls (the same `?:` with `m` not memoized, and a plain component) make 1 call each, so the skip is per function and not per file. Each of the five aborts 1.4.2 on its own. Sweep: built all 1807 source files under `test/bundler/transpiler/react-compiler-fixtures/` with `bun build --react-compiler --target=browser --external '*'` on 1.4.3 canary. None aborts, so no upstream fixture has this shape. Suites run with the debug build, after the rebase on 7a7cc96: `test/bundler/transpiler/react-compiler.test.ts` (58 pass), `test/bundler/transpiler/react-compiler-fixtures.test.ts` (3293 pass, 320 skip). `cargo clippy -p bun_react_compiler` and `cargo fmt --check` are clean. </details> <!-- robobun:evidence:begin --> --- **[human-review]** gate passed · iteration 1 · 2 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: 2 FAILED $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/transpiler/react-compiler.test.ts bun test v1.4.3 (6a92015) test/bundler/transpiler/react-compiler.test.ts: (pass) bundler > react-compiler/SimpleComponent [1077.04ms] (pass) bundler > react-compiler/ComponentWithHooks [1018.66ms] (pass) bundler > react-compiler/ObjectPatternRestInProps [466.17ms] (pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [677.15ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [586.00ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [413.21ms] (pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [963.12ms] (pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [227.16ms] (pass) bundler > react-compiler/SsrObjectMethodShorthand [1037.88ms] (pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [325.23ms] (pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [307.01ms] (pass) bundler > react-compiler/BundledReactPreservesImportRefs [538.68ms] (pass) bundler > r ... (truncated) release without fix: 12 failed, 1 skipped bun test v1.4.3-canary.1 (3c62dfb) test/bundler/transpiler/react-compiler.test.ts: (pass) bundler > react-compiler/SimpleComponent [63.16ms] (pass) bundler > react-compiler/ComponentWithHooks [20.36ms] (pass) bundler > react-compiler/ObjectPatternRestInProps [11.72ms] (pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [14.43ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [30.53ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [7.03ms] (pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [24.77ms] (pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [11.01ms] 1031 | ]); 1032 | const stdout = Buffer.from(stdoutBytes); 1033 | const stderr = Buffer.from(stderrBytes); 1034 | const success = exitCode === 0; 1035 | if (buildProc.signalCode) { 1036 | throw new Error( ^ error: [react-compiler/SsrObjectMethodShorthand] 'bun build' subprocess killed by SIGABRT cmd: /workspace/bun/build/release/bun build /tmp/bun-build-tests/bun-eKoWEm/react-compiler/SsrObjectMethodShorthand/entry.jsx --outfile=/tmp/bun-build-tests ... (truncated) ``` </details> <details><summary>passes on PR (with fix)</summary> ```console ASAN with fix: all passed $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/transpiler/react-compiler.test.ts bun test v1.4.3 (6a92015) test/bundler/transpiler/react-compiler.test.ts: (pass) bundler > react-compiler/SimpleComponent [984.04ms] (pass) bundler > react-compiler/ComponentWithHooks [341.71ms] (pass) bundler > react-compiler/ObjectPatternRestInProps [417.08ms] (pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [333.24ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [192.84ms] (pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [179.22ms] (pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [421.84ms] (pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [149.25ms] (pass) bundler > react-compiler/SsrObjectMethodShorthand [850.95ms] (pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [149.08ms] (pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [164.78ms] (pass) bundler > react-compiler/BundledReactPreservesImportRefs [344.39ms] (pass) bundler > reac ... (truncated) release with fix: 1 skipped $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) in 814ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [0/4] cargo bun_runtime → libbun_runtime.a ^[[1m^[[92m Compiling^[[0m bun_react_compiler v0.0.0 (/workspace/bun/src/react_compiler) ^[[1m^[[92m Compiling^[[0m bun_js_parser v0.0.0 (/workspace/bun/src/js_parser) ^[[1m^[[92m Compiling^[[0m bun_resolver v0.0.0 (/workspace/bun/src/resolver) ^[[1m^[[92m Compiling^[[0m bun_ini v0.0.0 (/workspace/bun/src/ini) ^[[1m^[[92m Compiling^[[0m bun_bundler v0.0.0 (/workspace/bun/src/bundler) ^[[1m^[[92m Compiling^[[0m bun_router v0.0.0 (/workspace/bun/src/router) ^[[1m^[[92m Compiling^[[0m bun_standalone_graph v0.0.0 (/workspace/bun/src/standalone_graph) ^[[1m^[[92m Compiling^[[0m bun_transpiler v0.0.0 (/workspace/bun/src/transpiler) ^[[1m^[[92m Compiling^[[0m bun_bunfig v0.0.0 (/workspace/bun/src/bunfig) ^[[1m^[[92m Compiling^[[0m bun_install v0.0.0 (/workspace/bun/src/install) ^[[1m^[[92m Compiling^[[0m bun_jsc v0.0.0 (/workspace/bun/src/jsc) ^[[1m^[[92m Compiling^[[0m bun_js_parser_jsc v0.0.0 (/workspace/bun/src/js_parser_jsc) ^[[1m^[[92m Compiling^[[0m bun_http_jsc v0.0.0 (/work ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` .../reactive_scopes/prune_non_escaping_scopes.rs | 103 +++++++++++++-------- test/bundler/transpiler/react-compiler.test.ts | 103 +++++++++++++++++++++ 2 files changed, 166 insertions(+), 40 deletions(-) ``` </details> **gate history** · 2 passed · 0 rejected · iteration 1 <details><summary>evidence per changed file</summary> ``` file reads edits tests …t_compiler/reactive_scopes/prune_non_escaping_scopes.rs 6 12 29 test/bundler/transpiler/react-compiler.test.ts 2 4 29 ``` </details> <!-- robobun:evidence:end -->
Problem
bun build --react-compiler --target=bunaborts on a component that contains an object literal with a method shorthand, for example<div data-v={{ m() {} }} />:panic: Internal error: Expected all ObjectExpressions and ObjectMethods to have non-null scope.--target=nodeandreactCompilerOutputMode: "ssr"do the same.pipeline.rsrunsinfer_reactive_scope_variablesonly ifenv.enable_memoization(), which is false in ssr mode. So no identifier gets a reactive scope.align_object_method_scopes.rs:52runs in every mode and calls.expect()on the scope of each object method and its object literal.Fix
find_scopes_to_mergeskips a pair with no scope on either side. The sibling passalign_method_call_scopesalready has this(None, None)arm.program.rsthen leaves that one function uncompiled, like upstream'sCompilerError.invariant.test/bundler/transpiler/react-compiler.test.ts(newSsrObjectMethodShorthand, SIGABRT on 1.4.3). Alsoreact-compiler-fixtures.test.ts, and all 1806 upstream fixtures with--target=bun: 5 abort before, 0 after.Background
browseris client (memoize),bunandnodeare ssr (no memoization,useStateinlined).panic = "abort".Notes
Repro (any directory, 1.4.2 and main 4b5862f):
Frames:
find_scopes_to_merge(src/react_compiler/inference/align_object_method_scopes.rs:52),align_object_method_scopes(:99),pipeline::run_hir_passes(pipeline.rs:564),compile_fn(:172),program::maybe_compile_node(program.rs:1363).Why the guard is on
enable_memoization()and the arm is not unconditional: with memoization enabled,infer_reactive_scope_variablesgives everyObjectMethodandObjectExpressiona scope (may_allocateis true for both), so a missing scope there is a real invariant violation. That case keeps upstream's behavior, as an error and not as a panic. Inner functions always have scopes, in every mode:analyse_functionsrunsinfer_reactive_scope_variableson them without the gate.Alternative not taken: gate the two alignment passes in
pipeline.rsonenable_memoization(). The header ofpipeline.rssays the pass sequence and the gating predicates stay identical with upstream, and the in-pass arm mirrors the sibling pass.What the test checks: it bundles with
--target=bunthrough the CLI and runs the output. The fakeuseStatenever returns its argument. The ssr pass inlinesuseStateto its initial value, so each function prints that value only if the compiler compiled it. Shapes covered: the object literal as a JSX attribute value,const v = { ... }in the body with plain,async, computed-key and nested methods, and a hook that returns an object with a method. A generator method is not in the test: upstream does not loweryield, so that function is skipped in every mode.Sweep: built each of the 1806 files under
test/bundler/transpiler/react-compiler-fixtures/withbun build --react-compiler --target=bun --external '*'. On 1.4.3 canary five abort with this panic (infer-nested-object-method.jsx,object-method-shorthand-hook-dep.js,object-shorthand-method-nested.js,infer-object-method-uncond-access.tsx,infer-objectmethod-cond-access.js). With this change none abort, and the output of those five keeps each method inline in its object.Codegen does not depend on the scopes here: it stores each
ObjectMethodin a map that lives for the whole function (cx.object_methods,codegen.rs:1418) and reads it back at theObjectExpression. A missing entry there is already an error return.Upstream:
AlignObjectMethodScopes.tsandreact_compiler_inference/src/align_object_method_scopes.rson facebook/react main both still have the invariant, andPipeline.tsgatesinferReactiveScopeVariablesonenv.enableMemoizationthe same way.Suites run with the debug build:
test/bundler/transpiler/react-compiler.test.ts(46 pass),test/bundler/transpiler/react-compiler-fixtures.test.ts(3293 pass, 320 skip).cargo clippy -p bun_react_compileris clean.[human-review] gate passed · iteration 1 · 3 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 2 passed · 0 rejected · iteration 1
evidence per changed file