Skip to content

react-compiler: keep the declaration of a local when dead code elimination keeps a store to it - #42385

Merged
Jarred-Sumner merged 1 commit into
mainfrom
robobun/a7b2362c/react-compiler-dead-store-declaration
Sep 12, 2026
Merged

Jarred-Sumner merged 1 commit into
mainfrom
robobun/a7b2362c/react-compiler-dead-store-declaration

Conversation

@robobun

@robobun robobun commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • bun build --react-compiler can print an assignment to a local with no declaration: ReferenceError: v is not defined. With v never read, try { try {..} catch { v = 1 } .. } catch { v = 2 } prints let v = 1 in the inner handler and a bare v = 2 in the outer one. In client mode the same cause can crash the build: panic: Expected identifier to be initialized.
  • Dead code elimination (src/react_compiler/optimization/dead_code_elimination.rs) keeps some stores to a local that nothing reads, but prunes let v: nothing marks the name. rewrite_instruction_kinds_based_on_reassignment then makes the first store left the declaration, in a scope that need not enclose the others. Upstream prints the same text.

Fix

  • A Reassign store that the pass retains now marks its variable, so let v stays. A Destructure retained as a block value marks each variable of its pattern.
  • Correct because a retained assignment needs its binding, whatever retained it. Unretained stores are still pruned.
  • Verified: three new tests in test/bundler/transpiler/react-compiler.test.ts fail without the fix. All 1806 upstream fixtures print byte-identical output before and after, in both output modes. Also ran react-compiler-fixtures.test.ts and the react tests in bundler_jsx.test.ts.

Background

  • The compiler lowers a function to HIR: basic blocks of SSA instructions. let v is a DeclareLocal, v = 1 a StoreLocal of kind Reassign.
  • The pass marks what is referenced and sweeps the rest. A declaration stays while the variable's name is marked. A Reassign store stays while its own SSA value is read.
  • Two more things keep a store: its own result is used (f(v = 2)), or it ends a block whose kind is not Block. That rule protects value blocks (a ternary branch, a for..of test). It also covers a catch handler, kind Catch.
Notes

Shapes probed (v is never read in each). Before: main 4b5862f and 1.4.3-canary.1+6a92015fc. After: this branch. "Left alone" means the compiler skips the function and the source runs as written.

shape before after
nested try, a store ends each handler (the report) let v = 1 / bare v = 2, ReferenceError let v kept, compiled
two try in sequence, a store ends each handler same same
[v] = p.items ends two handlers let [v] = .. / bare [v] = .., ReferenceError compiled
handler store, then f(v = 2) or c && (v = 2) ReferenceError compiled
let v = "init", handler stores, v = "final", <div>{v}</div> (the read folds to "final") ReferenceError compiled
for (v of xs) {} or for (v in xs) {}, then f(v = 2) or a handler store for (let v of xs) / bare v = 2, ReferenceError left alone
for (..; ..; v = i++) twice, client mode panic: Expected identifier to be initialized (prune_non_escaping_scopes.rs:1060, a scope reassigns a local that was never declared) compiled
f(v = 1); f(v = 2), c ? (v = 1) : (v = 2), a && (v = 1); b && (v = 2) left alone (Const declaration cannot be referenced as an expression) compiled

Nesting is not needed. In a handler nested in a try, every instruction ends its own Catch block (each one can throw to the outer handler), so every store there is kept. In a plain handler only the last instruction is.

Upstream. babel-plugin-react-compiler@1.0.0 prints let v = 1 / bare v = 2 for the two-try-in-sequence form. For the nested form it raises Expected a break target and skips the function, an older limit on nested try. 0.0.0-experimental-a1856f3-20260507 compiles the nested form and prints the same broken text. It also prints for (let v of p.items) {..} foo(v = 2). DeadCodeElimination.ts and the Rust port on facebook/react main have the same logic as UPSTREAM_PORTED.

Why not narrow the block-value rule to expression blocks. Upstream's isStatementBlockKind calls catch a statement kind, so the rule covers handlers by accident. Narrowing it removes the handler cases only. A for..of test is a real value block, and let v; for (v of xs) {}; f(v = 2) breaks the same way.

Why not fail in rewrite_instruction_kinds_based_on_reassignment. An invariant there for a Reassign store with no declaration also stops the bad output, but it leaves every such function uncompiled.

Cost. The dead stores stay in the output, as in the source. let v; for (v of xs) {} with v never read used to compile as for (const v of xs). It is now left alone, like any for (v of xs) over an outer local that is read later (Unexpected Reassign variable in for..of collection).

Not changed here. The panic above is an upstream invariant that the port writes as .expect(). Other invariants written that way also end the build where upstream skips the function.

… kept

Dead code elimination keeps some stores to a local that nothing reads:
the last instruction of a catch handler or of a for..of test, and a
store whose own value is used. It pruned the declaration all the same,
because nothing referenced the name. The first store left then became
the declaration, in a scope that did not have to enclose the other
stores, so the output threw `ReferenceError: v is not defined`. In
client mode a store left in a `for` update panicked with `Expected
identifier to be initialized`.

A reassignment that dead code elimination retains now marks its
variable as referenced, so the declaration stays.
@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 9 days. After that, they cost $0.25 per reviewed file.

Or wait 11 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: ec8ac22d-1b73-48ea-870c-a3fddbbdabd2

📥 Commits

Reviewing files that changed from the base of the PR and between 471b586 and 68bda28.

📒 Files selected for processing (2)
  • src/react_compiler/optimization/dead_code_elimination.rs
  • test/bundler/transpiler/react-compiler.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

Reproduced on main 4b5862f and on 1.4.3-canary.1+6a92015fc, client and ssr mode:

// ds.jsx (react stubbed under node_modules/react)
function App(p) {
  let v;
  try {
    try { JSON.parse(p.t); } catch { v = 1; }
    JSON.parse(p.t);
  } catch { v = 2; }
  return <div />;
}
console.log(App({ t: "{bad" }).type);
$ bun build --react-compiler --target=bun ds.jsx --outfile=c.js && bun c.js
ReferenceError: v is not defined

The same happens without nesting (two try in sequence) and without try (let v; for (v of xs) {}; f(v = 2)). babel-plugin-react-compiler 1.0.0 and 0.0.0-experimental-a1856f3-20260507 print the same text.

  • Fail before: USE_SYSTEM_BUN=1 bun test test/bundler/transpiler/react-compiler.test.ts -t DeadStore fails 3 of 3. A debug build of main fails the same 3.
  • Pass after: bun bd test test/bundler/transpiler/react-compiler.test.ts passes 48 of 48.
  • Self-review: the PR merged before the review of the diff finished. The review had raised no concern by then, and it was stopped.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and didn't find any bugs. Because the fix intentionally diverges from upstream DeadCodeElimination.ts in a ported optimization pass, a human sign-off on that divergence would still be worthwhile.

What was reviewed:

  • reference_reassigned_variables and both call sites — the block-value site covers Destructure; the retained-instruction site sits inside the StoreLocal branch, and the comment there correctly explains why a retained non-block-value Destructure doesn't need it (rewrite_instruction prunes the dead pattern entries).
  • stubReactWithEffect override — replaces react/index.js with only useEffect, but the forms don't need useState/createElement and the jsx-runtime / compiler-runtime stubs are preserved via the spread.
  • The onAfterBundle regex asserts each useEffect(() => {}) was outlined/dropped, so a bailout that "left alone" a form would fail the test — the assertions can fail for the right reason.
Extended reasoning...

Overview

The PR fixes a DCE bug in Bun's React Compiler port: when a Reassign store to a local is retained (as the last instruction of a non-Block block, or because its own SSA value is used) but nothing reads the variable, the original let v was pruned, and rewrite_instruction_kinds_based_on_reassignment promoted the first surviving store to a declaration in a scope that didn't enclose the others — yielding ReferenceError at runtime or a panic: Expected identifier to be initialized in client mode. The fix adds a small helper that marks the lvalue identifier(s) of a retained reassignment as referenced, keeping the original declaration. Rust change is ~30 lines (one helper, two call sites, doc comment); ~190 lines of itBundled tests cover nested/sequential catch handlers, destructuring in a handler, handler+argument, handler+logical, every-read-folded, for..of/for..in heads under both browser and bun targets, and the for-update panic case.

Security risks

None. This is an optimization pass over compiler-internal HIR; no user input parsing, no I/O, no auth/crypto surface. The worst case for a bug here is incorrect codegen or the compiler bailing out on a function.

Level of scrutiny

Medium. The Rust change is small and the mechanism is clear (mark the reassigned variable's name so pruneable_value on the DeclareLocal returns false). The second call site sits inside the if let InstructionValue::StoreLocal branch, so its Destructure arm is dead there — but that's intentional and the adjacent comment explains why (a non-block-value Destructure is handed to rewrite_instruction, which holes out the unread pattern entries, so no dangling reference survives). The Destructure arm is exercised at the block-value call site. What raises the bar is that this is ported code and the fix is explicitly "Not in upstream" — REVIEW.md treats the reference implementation as the spec for ported passes, so a deliberate divergence, however well-argued, is the kind of decision a maintainer should acknowledge.

Other factors

The PR description is unusually thorough: it names the exact mechanism, shows a shape table before/after, confirms upstream (both 1.0.0 and an experimental build) prints the same broken text, explains why two alternative fixes (narrowing the block-value rule; adding an invariant in rewrite_instruction_kinds_based_on_reassignment) were rejected, states the cost (dead stores stay in output; for (v of xs) over a dead v is now left alone rather than compiled to for (const v of xs)), and reports all 1806 upstream fixtures byte-identical in both output modes. Tests were added to the existing react-compiler.test.ts per convention, use itBundled, cover both targets, and include an onAfterBundle assertion that the compiler actually compiled the forms (not just bailed). No CODEOWNERS entry covers these paths, no prior reviewer objections in the timeline, and the bug hunt ran to dry_streak with no findings.

@robobun

robobun commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 5:07 PM PT - Sep 11th, 2026

❌ @robobun, your commit 68bda28 has 2 failures in Build #114465 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 42385

That installs a local version of the PR into your bun-42385 executable, so you can run:

bun-42385 --bun

@Jarred-Sumner
Jarred-Sumner merged commit e100240 into main Sep 12, 2026
10 of 12 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the robobun/a7b2362c/react-compiler-dead-store-declaration branch September 12, 2026 03:50
Jarred-Sumner pushed a commit that referenced this pull request Sep 14, 2026
…ssigns (#42642)

### Problem
- `bun build --react-compiler` throws `ReferenceError: x is not defined`
for `const r = {}; let x; r.a = 1; return <Stub r={r} v={(x = 5)} />`.
The output has `let x;` inside the memo block of `r` and `x = 5` in the
next block. With `v={(x = p.n)}` it throws `ReferenceError: t0 is not
defined` instead.
- Dependency collection
(`src/react_compiler/inference/propagate_scope_dependencies_hir.rs`)
hoists a `let` out of its scope only on a read (`visit_dependency`).
`visit_reassignment` does not hoist. Before #42385 the declaration was
pruned and the compiler bailed. `babel-plugin-react-compiler@1.0.0`
still bails.
- For a reactive value the store is a dependency of the block that reads
it, under a promoted name. `emit_store`
(`src/react_compiler/codegen.rs:1601`) still inlined it into that block:
`if ($[2] !== t0)` with no `t0` declared. Fixes #42629.

### Fix
- Move the hoist logic of `visit_dependency` into
`declare_outside_original_scope` and call it from `visit_reassignment`
too. An assignment from outside the declaring scope adds the variable to
that scope's `declarations`, the same way a read does.
- In `emit_store`, route every `StoreLocal` with a result through
`codegen_instruction`. An unnamed result is inlined as before. A
promoted one prints `const t0 = x = p.n;` before the block that reads
it.
- Correct because a variable a later scope assigns has to live outside
the memo block, and a scope declaration is the one mechanism codegen has
for that. A promoted temporary has a name because another scope reads it
by that name.
- Verified: `react-compiler/DeadStoreAfterTheScopeOfItsDeclaration` in
`test/bundler/transpiler/react-compiler.test.ts` (nine forms, all throw
on main). All 1739 upstream fixtures compile byte-identical before and
after. Also `react-compiler-fixtures.test.ts` and `bundler_jsx.test.ts`.

### Background
- The compiler groups the instructions that build one value (here `r`)
into a reactive scope. Codegen prints the scope as a memo block: `if
(deps changed) { compute; store } else { load }`. Declarations between
the creation of `r` and its last mutation are inside that block.
- A scope declaration is a variable the block produces for later code.
Codegen prints `let` for it before the block, stores it in the cache at
the end, and loads it in the `else` branch.
- A temporary read from another scope is promoted: it gets a name (`t0`)
and codegen declares it with `const` instead of inlining its value.

<details><summary>Notes</summary>

Compiled output for the reported component, after:

```js
function C() {
  let $ = c(3);
  let r, x;
  if ($[0] === __MEMO_CACHE_SENTINEL)
    r = {}, r.a = 1, $[0] = r, $[1] = x;
  else
    r = $[0], x = $[1];
  let t0;
  if ($[2] === __MEMO_CACHE_SENTINEL)
    t0 = jsxDEV(Stub, { r, v: x = 5 }, ...), $[2] = t0;
  else
    t0 = $[2];
  return t0;
}
```

With `v={(x = p.n)}`:

```js
  const t0 = x = p.n;
  let t1;
  if ($[2] !== t0)
    t1 = jsxDEV(Stub, { r, v: t0 }, ...), $[2] = t0, $[3] = t1;
  else
    t1 = $[3];
```

**Forms in the test.** A JSX prop `v={(x = 5)}`, a read that folds away
(`[(x = 5), x]`), a call argument `p.call((x = 5))`, a store that ends a
catch handler, `let x` inside two nested scopes (`q.b = r`), and the
prop, array and nested forms with `x = p.n`. `DeclaredAbove` has `let x`
above `const r = {}` with `[(x = p.n), x++]`: only the codegen part is
wrong there, and it throws `t0 is not defined` on main.

**Forms that are not affected.** `let x` above `const r = {}` with a
literal, a read that does not fold with a literal (`[(x = 5), x++]`),
and no `r={r}` prop (the scope of `r` is pruned) already worked. A
destructuring assignment used as an expression (`p.call(([x] =
p.items))`) is left uncompiled before and after.

**Why the declaring scope caches `x`.** A scope declaration is always an
output of the block. For `let x;` the cached value is `undefined`, which
is what the block would compute. This is the output a read from a later
scope already produced (`let r, x;` for `[(x = 5), x++]`).

**Upstream and the codegen change.** Upstream
`codegenInstructionNullable` inlines every `StoreLocal` value that is
not a `StoreContext`. A promoted `StoreLocal` result never reaches its
codegen: the input bails earlier. In this port it does, so the branch
now takes the path the `StoreContext` case already took.

**Differential run.** All 1739 upstream fixtures under
`test/bundler/transpiler/react-compiler-fixtures` compiled with
`Bun.build({ reactCompiler: true })` on `1.4.3-canary.1+b99371011` and
on this branch give byte-identical output, and the same 68 build errors.

Self-reviewed: two concerns raised, two addressed (the promoted store in
codegen, and the non-literal forms in the test).

</details>

<!-- robobun:evidence:begin -->

---

**[human-review]** gate passed · iteration 0 · 3 files touched

<details><summary>fails on main (without fix)</summary>

```console
ASAN without fix: 1 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 (b993710)

test/bundler/transpiler/react-compiler.test.ts:
(pass) bundler > react-compiler/SimpleComponent [674.86ms]
(pass) bundler > react-compiler/ComponentWithHooks [288.08ms]
(pass) bundler > react-compiler/ObjectPatternRestInProps [406.16ms]
(pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [373.43ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [143.43ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [103.16ms]
(pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [368.71ms]
(pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [119.97ms]
(pass) bundler > react-compiler/SsrObjectMethodShorthand [720.78ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [112.13ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [116.91ms]
(pass) bundler > react-compiler/BundledReactPreservesImportRefs [300.78ms]
(pass) bundler > reac
... (truncated)

release without fix: 2 failed, 1 skipped
bun test v1.4.3-canary.1 (b993710)

test/bundler/transpiler/react-compiler.test.ts:
(pass) bundler > react-compiler/SimpleComponent [33.62ms]
(pass) bundler > react-compiler/ComponentWithHooks [13.86ms]
(pass) bundler > react-compiler/ObjectPatternRestInProps [16.41ms]
(pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [16.91ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [15.00ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [26.64ms]
(pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [53.49ms]
(pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [13.00ms]
(pass) bundler > react-compiler/SsrObjectMethodShorthand [53.98ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [28.58ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [38.49ms]
(pass) bundler > react-compiler/BundledReactPreservesImportRefs [24.48ms]
(pass) bundler > react-compiler/BundledCjsCompilerRuntimeSurvivesTreeShaking [32.99ms]
(pass) bundler > react-compiler/RequireStringPreservesImportRecord [22.53ms]
(pass) bundler > react-compiler/BranchB
... (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 (b993710)

test/bundler/transpiler/react-compiler.test.ts:
(pass) bundler > react-compiler/SimpleComponent [673.01ms]
(pass) bundler > react-compiler/ComponentWithHooks [282.53ms]
(pass) bundler > react-compiler/ObjectPatternRestInProps [309.01ms]
(pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [301.46ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [141.55ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [121.72ms]
(pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [395.20ms]
(pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [149.21ms]
(pass) bundler > react-compiler/SsrObjectMethodShorthand [984.75ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [158.59ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [141.16ms]
(pass) bundler > react-compiler/BundledReactPreservesImportRefs [341.57ms]
(pass) bundler > reac
... (truncated)

release with fix: 1 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 824ms (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/codegen.rs                      |  19 +--
 .../inference/propagate_scope_dependencies_hir.rs  |  59 +++++-----
 test/bundler/transpiler/react-compiler.test.ts     | 127 +++++++++++++++++++++
 3 files changed, 164 insertions(+), 41 deletions(-)
```

</details>

**gate history** · 1 passed · 0 rejected · iteration 0

<details><summary>evidence per changed file</summary>

```
file                                                      reads  edits  tests
src/react_compiler/codegen.rs                                 2      3     12
…_compiler/inference/propagate_scope_dependencies_hir.rs      2      5     13
test/bundler/transpiler/react-compiler.test.ts                2      6     12
```

</details>

**root cause** · written by the author bot

In `propagate_scope_dependencies_hir.rs`, only a read of a variable
added it to the `declarations` of the enclosing reactive scope, so a
local that was declared inside the memo block for `r` and then only
assigned in a later block kept its `let` inside the first block, and the
later assignment referenced a name that was out of scope. This became
reachable once dead code elimination started retaining a `let x;` whose
store survives only for its own value, where the compiler previously
bailed out instead of emitting broken code. The fix centralizes
declaration propagation so that a reassignme…

<!-- robobun:evidence:end -->

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Jarred-Sumner pushed a commit that referenced this pull request Sep 15, 2026
)

### 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 -->
Jarred-Sumner pushed a commit that referenced this pull request Sep 15, 2026
…42644)

### Problem
- With `bun build --react-compiler`, `let x; return [(x = 5), -(x = 10),
x++]` compiles to `x = 10; return [x = 5, -10, x++]`. The function
returns `[5,-10,5]` instead of `[5,-10,10]`. Fixes #42628.
- The `StoreLocal` arm of `evaluate_instruction` in
`src/react_compiler/optimization/constant_propagation.rs` returns the
stored constant for the store's own result temporary. The `-t` consumer
folds to `-10`, so nothing reads the temporary. `prune_unused_lvalues`
removes the lvalue. `emit_store` in `src/react_compiler/codegen.rs`
(`InstructionKind::Reassign`) prints a store with no lvalue as a
statement at once, while the earlier `x = 5` is still inlined at its
use. The `PostfixUpdate` and `PrefixUpdate` arms have the same shape:
`[(x = 5), -(x++)]` prints `x++` first.

### Fix
- The `StoreLocal`, `PostfixUpdate` and `PrefixUpdate` arms return
`None` instead of a constant for their own result temporary. They still
record the constant for the assigned variable, so later reads of `x`
fold as before.
- This is correct because the three instructions have a side effect and
must run at their position. A consumer that can no longer fold the
temporary keeps it, so the instruction stays inlined at its use, in
source order. The pass already has the same rule for the last value of a
sequence block.
- This diverges from upstream `babel-plugin-react-compiler`, which emits
the same wrong output. The sites carry a `Not in upstream` note, as
`dead_code_elimination.rs` does for #42385. The trade: `const b = a++`
with a constant `a` no longer folds `b`. Program output does not change.
- Verified: `test/bundler/transpiler/react-compiler.test.ts` (new
`react-compiler/StoreOrderInSameExpression`, fails on stock bun). Also
all of `react-compiler-fixtures.test.ts` (3293 pass) and
`bundler_jsx.test.ts`.

### Background
- The compiler lowers each function to HIR: every expression becomes an
instruction with a result temporary. Codegen inlines an unnamed
temporary at its single use and prints an instruction with no result as
a statement where it stands.
- Constant propagation records a constant per SSA identifier. When an
instruction's operands are constant, it rewrites the instruction to a
`Primitive` and records the constant for its result temporary.
- `prune_unused_lvalues` runs before codegen. It removes the result
temporary of any instruction that nothing reads.

<details><summary>Notes</summary>

- Three things have to be present: an earlier `x = K1` whose value is
not folded (array element, call argument), a later `x = K2` in the same
statement whose value is folded (`-(x = 10)`, `!(x = 10)`, template
literal, computed member), and a later read of `x` that is not folded,
so that dead code elimination keeps the second store.
- The update variant in the test: `let x = 1; const a = [(x = 5),
-(x++)]; if (cond) x = 100; return [a, x]` printed `x++` before `a`. The
phi on `x` after the `if` does not fold, so the last read is real. Stock
bun returns `[[5,-5],5]`, correct is `[[5,-5],6]`.
- A guard in `emit_store` was considered. That arm is a 1:1 port of
`CodegenReactiveFunction.ts`, and flushing pending temporaries there
would change output for many fixtures. The constant propagation pass is
the only consumer that orphans the lvalue of a side-effecting
instruction, so the fix lives there.
- No fixture in `test/bundler/transpiler/react-compiler-fixtures`
triggers the bug. One fixture, `update-expression-constant-propagation`,
now folds less than upstream's expected output (`let b = a++` stays,
instead of `b: 0`). The fixture harness checks the memo slot count,
which is unchanged.
- Self-reviewed: the first draft merged the two update arms into one
match arm. The review asked for the minimal hunk with the divergence
marked, to keep the upstream sync mechanical. This is that shape.
</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: 1 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 (b993710)

test/bundler/transpiler/react-compiler.test.ts:
(pass) bundler > react-compiler/SimpleComponent [619.25ms]
(pass) bundler > react-compiler/ComponentWithHooks [233.46ms]
(pass) bundler > react-compiler/ObjectPatternRestInProps [225.36ms]
(pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [309.13ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [129.64ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [97.58ms]
(pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [329.97ms]
(pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [108.76ms]
(pass) bundler > react-compiler/SsrObjectMethodShorthand [564.72ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [115.48ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [100.59ms]
(pass) bundler > react-compiler/BundledReactPreservesImportRefs [314.90ms]
(pass) bundler > react
... (truncated)

release without fix: 2 failed, 1 skipped
bun test v1.4.3-canary.1 (b993710)

test/bundler/transpiler/react-compiler.test.ts:
(pass) bundler > react-compiler/SimpleComponent [15.66ms]
(pass) bundler > react-compiler/ComponentWithHooks [6.45ms]
(pass) bundler > react-compiler/ObjectPatternRestInProps [6.80ms]
(pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [11.70ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [5.56ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [3.00ms]
(pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [8.61ms]
(pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [3.31ms]
(pass) bundler > react-compiler/SsrObjectMethodShorthand [13.31ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [3.68ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [3.69ms]
(pass) bundler > react-compiler/BundledReactPreservesImportRefs [7.13ms]
(pass) bundler > react-compiler/BundledCjsCompilerRuntimeSurvivesTreeShaking [10.89ms]
(pass) bundler > react-compiler/RequireStringPreservesImportRecord [6.83ms]
(pass) bundler > react-compiler/BranchBooleanFeat
... (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 (b993710)

test/bundler/transpiler/react-compiler.test.ts:
(pass) bundler > react-compiler/SimpleComponent [603.87ms]
(pass) bundler > react-compiler/ComponentWithHooks [236.36ms]
(pass) bundler > react-compiler/ObjectPatternRestInProps [319.01ms]
(pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [432.39ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [147.80ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [97.64ms]
(pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [280.45ms]
(pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [102.62ms]
(pass) bundler > react-compiler/SsrObjectMethodShorthand [553.36ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [108.30ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [115.93ms]
(pass) bundler > react-compiler/BundledReactPreservesImportRefs [246.87ms]
(pass) bundler > react
... (truncated)

release with fix: 1 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 684ms (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>

```
.../optimization/constant_propagation.rs           | 22 ++++++-----
 test/bundler/transpiler/react-compiler.test.ts     | 46 ++++++++++++++++++++++
 2 files changed, 58 insertions(+), 10 deletions(-)
```

</details>

**gate history** · 1 passed · 0 rejected · iteration 0

<details><summary>evidence per changed file</summary>

```
file                                                     reads  edits  tests
src/react_compiler/optimization/constant_propagation.rs      5      4     10
test/bundler/transpiler/react-compiler.test.ts               1      1      8
```

</details>

**root cause** · written by the author bot

In the constant propagation pass, the StoreLocal, PostfixUpdate, and
PrefixUpdate arms returned the stored constant for the instruction's own
result temporary, so when a consumer such as a negation folded to a
primitive, nothing referenced that temporary and the store's lvalue was
pruned. Codegen then printed the lvalue-less store as a standalone
statement ahead of an earlier store that remained inlined at its use,
reordering the side effects within the expression. The fix makes those
three arms return None for the result temporary while still recording
the constant for the assigned variabl…

<!-- robobun:evidence:end -->
usrbinkat pushed a commit to usrbinkat/bun that referenced this pull request Sep 15, 2026
…ssigns (oven-sh#42642)

### Problem
- `bun build --react-compiler` throws `ReferenceError: x is not defined`
for `const r = {}; let x; r.a = 1; return <Stub r={r} v={(x = 5)} />`.
The output has `let x;` inside the memo block of `r` and `x = 5` in the
next block. With `v={(x = p.n)}` it throws `ReferenceError: t0 is not
defined` instead.
- Dependency collection
(`src/react_compiler/inference/propagate_scope_dependencies_hir.rs`)
hoists a `let` out of its scope only on a read (`visit_dependency`).
`visit_reassignment` does not hoist. Before oven-sh#42385 the declaration was
pruned and the compiler bailed. `babel-plugin-react-compiler@1.0.0`
still bails.
- For a reactive value the store is a dependency of the block that reads
it, under a promoted name. `emit_store`
(`src/react_compiler/codegen.rs:1601`) still inlined it into that block:
`if ($[2] !== t0)` with no `t0` declared. Fixes oven-sh#42629.

### Fix
- Move the hoist logic of `visit_dependency` into
`declare_outside_original_scope` and call it from `visit_reassignment`
too. An assignment from outside the declaring scope adds the variable to
that scope's `declarations`, the same way a read does.
- In `emit_store`, route every `StoreLocal` with a result through
`codegen_instruction`. An unnamed result is inlined as before. A
promoted one prints `const t0 = x = p.n;` before the block that reads
it.
- Correct because a variable a later scope assigns has to live outside
the memo block, and a scope declaration is the one mechanism codegen has
for that. A promoted temporary has a name because another scope reads it
by that name.
- Verified: `react-compiler/DeadStoreAfterTheScopeOfItsDeclaration` in
`test/bundler/transpiler/react-compiler.test.ts` (nine forms, all throw
on main). All 1739 upstream fixtures compile byte-identical before and
after. Also `react-compiler-fixtures.test.ts` and `bundler_jsx.test.ts`.

### Background
- The compiler groups the instructions that build one value (here `r`)
into a reactive scope. Codegen prints the scope as a memo block: `if
(deps changed) { compute; store } else { load }`. Declarations between
the creation of `r` and its last mutation are inside that block.
- A scope declaration is a variable the block produces for later code.
Codegen prints `let` for it before the block, stores it in the cache at
the end, and loads it in the `else` branch.
- A temporary read from another scope is promoted: it gets a name (`t0`)
and codegen declares it with `const` instead of inlining its value.

<details><summary>Notes</summary>

Compiled output for the reported component, after:

```js
function C() {
  let $ = c(3);
  let r, x;
  if ($[0] === __MEMO_CACHE_SENTINEL)
    r = {}, r.a = 1, $[0] = r, $[1] = x;
  else
    r = $[0], x = $[1];
  let t0;
  if ($[2] === __MEMO_CACHE_SENTINEL)
    t0 = jsxDEV(Stub, { r, v: x = 5 }, ...), $[2] = t0;
  else
    t0 = $[2];
  return t0;
}
```

With `v={(x = p.n)}`:

```js
  const t0 = x = p.n;
  let t1;
  if ($[2] !== t0)
    t1 = jsxDEV(Stub, { r, v: t0 }, ...), $[2] = t0, $[3] = t1;
  else
    t1 = $[3];
```

**Forms in the test.** A JSX prop `v={(x = 5)}`, a read that folds away
(`[(x = 5), x]`), a call argument `p.call((x = 5))`, a store that ends a
catch handler, `let x` inside two nested scopes (`q.b = r`), and the
prop, array and nested forms with `x = p.n`. `DeclaredAbove` has `let x`
above `const r = {}` with `[(x = p.n), x++]`: only the codegen part is
wrong there, and it throws `t0 is not defined` on main.

**Forms that are not affected.** `let x` above `const r = {}` with a
literal, a read that does not fold with a literal (`[(x = 5), x++]`),
and no `r={r}` prop (the scope of `r` is pruned) already worked. A
destructuring assignment used as an expression (`p.call(([x] =
p.items))`) is left uncompiled before and after.

**Why the declaring scope caches `x`.** A scope declaration is always an
output of the block. For `let x;` the cached value is `undefined`, which
is what the block would compute. This is the output a read from a later
scope already produced (`let r, x;` for `[(x = 5), x++]`).

**Upstream and the codegen change.** Upstream
`codegenInstructionNullable` inlines every `StoreLocal` value that is
not a `StoreContext`. A promoted `StoreLocal` result never reaches its
codegen: the input bails earlier. In this port it does, so the branch
now takes the path the `StoreContext` case already took.

**Differential run.** All 1739 upstream fixtures under
`test/bundler/transpiler/react-compiler-fixtures` compiled with
`Bun.build({ reactCompiler: true })` on `1.4.3-canary.1+b99371011` and
on this branch give byte-identical output, and the same 68 build errors.

Self-reviewed: two concerns raised, two addressed (the promoted store in
codegen, and the non-literal forms in the test).

</details>

<!-- robobun:evidence:begin -->

---

**[human-review]** gate passed · iteration 0 · 3 files touched

<details><summary>fails on main (without fix)</summary>

```console
ASAN without fix: 1 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 (b993710)

test/bundler/transpiler/react-compiler.test.ts:
(pass) bundler > react-compiler/SimpleComponent [674.86ms]
(pass) bundler > react-compiler/ComponentWithHooks [288.08ms]
(pass) bundler > react-compiler/ObjectPatternRestInProps [406.16ms]
(pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [373.43ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [143.43ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [103.16ms]
(pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [368.71ms]
(pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [119.97ms]
(pass) bundler > react-compiler/SsrObjectMethodShorthand [720.78ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [112.13ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [116.91ms]
(pass) bundler > react-compiler/BundledReactPreservesImportRefs [300.78ms]
(pass) bundler > reac
... (truncated)

release without fix: 2 failed, 1 skipped
bun test v1.4.3-canary.1 (b993710)

test/bundler/transpiler/react-compiler.test.ts:
(pass) bundler > react-compiler/SimpleComponent [33.62ms]
(pass) bundler > react-compiler/ComponentWithHooks [13.86ms]
(pass) bundler > react-compiler/ObjectPatternRestInProps [16.41ms]
(pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [16.91ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [15.00ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [26.64ms]
(pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [53.49ms]
(pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [13.00ms]
(pass) bundler > react-compiler/SsrObjectMethodShorthand [53.98ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [28.58ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [38.49ms]
(pass) bundler > react-compiler/BundledReactPreservesImportRefs [24.48ms]
(pass) bundler > react-compiler/BundledCjsCompilerRuntimeSurvivesTreeShaking [32.99ms]
(pass) bundler > react-compiler/RequireStringPreservesImportRecord [22.53ms]
(pass) bundler > react-compiler/BranchB
... (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 (b993710)

test/bundler/transpiler/react-compiler.test.ts:
(pass) bundler > react-compiler/SimpleComponent [673.01ms]
(pass) bundler > react-compiler/ComponentWithHooks [282.53ms]
(pass) bundler > react-compiler/ObjectPatternRestInProps [309.01ms]
(pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [301.46ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [141.55ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [121.72ms]
(pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [395.20ms]
(pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [149.21ms]
(pass) bundler > react-compiler/SsrObjectMethodShorthand [984.75ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [158.59ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [141.16ms]
(pass) bundler > react-compiler/BundledReactPreservesImportRefs [341.57ms]
(pass) bundler > reac
... (truncated)

release with fix: 1 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 824ms (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/codegen.rs                      |  19 +--
 .../inference/propagate_scope_dependencies_hir.rs  |  59 +++++-----
 test/bundler/transpiler/react-compiler.test.ts     | 127 +++++++++++++++++++++
 3 files changed, 164 insertions(+), 41 deletions(-)
```

</details>

**gate history** · 1 passed · 0 rejected · iteration 0

<details><summary>evidence per changed file</summary>

```
file                                                      reads  edits  tests
src/react_compiler/codegen.rs                                 2      3     12
…_compiler/inference/propagate_scope_dependencies_hir.rs      2      5     13
test/bundler/transpiler/react-compiler.test.ts                2      6     12
```

</details>

**root cause** · written by the author bot

In `propagate_scope_dependencies_hir.rs`, only a read of a variable
added it to the `declarations` of the enclosing reactive scope, so a
local that was declared inside the memo block for `r` and then only
assigned in a later block kept its `let` inside the first block, and the
later assignment referenced a name that was out of scope. This became
reachable once dead code elimination started retaining a `let x;` whose
store survives only for its own value, where the compiler previously
bailed out instead of emitting broken code. The fix centralizes
declaration propagation so that a reassignme…

<!-- robobun:evidence:end -->

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
usrbinkat pushed a commit to usrbinkat/bun that referenced this pull request Sep 15, 2026
…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 -->
usrbinkat pushed a commit to usrbinkat/bun that referenced this pull request Sep 15, 2026
…ven-sh#42644)

### Problem
- With `bun build --react-compiler`, `let x; return [(x = 5), -(x = 10),
x++]` compiles to `x = 10; return [x = 5, -10, x++]`. The function
returns `[5,-10,5]` instead of `[5,-10,10]`. Fixes oven-sh#42628.
- The `StoreLocal` arm of `evaluate_instruction` in
`src/react_compiler/optimization/constant_propagation.rs` returns the
stored constant for the store's own result temporary. The `-t` consumer
folds to `-10`, so nothing reads the temporary. `prune_unused_lvalues`
removes the lvalue. `emit_store` in `src/react_compiler/codegen.rs`
(`InstructionKind::Reassign`) prints a store with no lvalue as a
statement at once, while the earlier `x = 5` is still inlined at its
use. The `PostfixUpdate` and `PrefixUpdate` arms have the same shape:
`[(x = 5), -(x++)]` prints `x++` first.

### Fix
- The `StoreLocal`, `PostfixUpdate` and `PrefixUpdate` arms return
`None` instead of a constant for their own result temporary. They still
record the constant for the assigned variable, so later reads of `x`
fold as before.
- This is correct because the three instructions have a side effect and
must run at their position. A consumer that can no longer fold the
temporary keeps it, so the instruction stays inlined at its use, in
source order. The pass already has the same rule for the last value of a
sequence block.
- This diverges from upstream `babel-plugin-react-compiler`, which emits
the same wrong output. The sites carry a `Not in upstream` note, as
`dead_code_elimination.rs` does for oven-sh#42385. The trade: `const b = a++`
with a constant `a` no longer folds `b`. Program output does not change.
- Verified: `test/bundler/transpiler/react-compiler.test.ts` (new
`react-compiler/StoreOrderInSameExpression`, fails on stock bun). Also
all of `react-compiler-fixtures.test.ts` (3293 pass) and
`bundler_jsx.test.ts`.

### Background
- The compiler lowers each function to HIR: every expression becomes an
instruction with a result temporary. Codegen inlines an unnamed
temporary at its single use and prints an instruction with no result as
a statement where it stands.
- Constant propagation records a constant per SSA identifier. When an
instruction's operands are constant, it rewrites the instruction to a
`Primitive` and records the constant for its result temporary.
- `prune_unused_lvalues` runs before codegen. It removes the result
temporary of any instruction that nothing reads.

<details><summary>Notes</summary>

- Three things have to be present: an earlier `x = K1` whose value is
not folded (array element, call argument), a later `x = K2` in the same
statement whose value is folded (`-(x = 10)`, `!(x = 10)`, template
literal, computed member), and a later read of `x` that is not folded,
so that dead code elimination keeps the second store.
- The update variant in the test: `let x = 1; const a = [(x = 5),
-(x++)]; if (cond) x = 100; return [a, x]` printed `x++` before `a`. The
phi on `x` after the `if` does not fold, so the last read is real. Stock
bun returns `[[5,-5],5]`, correct is `[[5,-5],6]`.
- A guard in `emit_store` was considered. That arm is a 1:1 port of
`CodegenReactiveFunction.ts`, and flushing pending temporaries there
would change output for many fixtures. The constant propagation pass is
the only consumer that orphans the lvalue of a side-effecting
instruction, so the fix lives there.
- No fixture in `test/bundler/transpiler/react-compiler-fixtures`
triggers the bug. One fixture, `update-expression-constant-propagation`,
now folds less than upstream's expected output (`let b = a++` stays,
instead of `b: 0`). The fixture harness checks the memo slot count,
which is unchanged.
- Self-reviewed: the first draft merged the two update arms into one
match arm. The review asked for the minimal hunk with the divergence
marked, to keep the upstream sync mechanical. This is that shape.
</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: 1 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 (b993710)

test/bundler/transpiler/react-compiler.test.ts:
(pass) bundler > react-compiler/SimpleComponent [619.25ms]
(pass) bundler > react-compiler/ComponentWithHooks [233.46ms]
(pass) bundler > react-compiler/ObjectPatternRestInProps [225.36ms]
(pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [309.13ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [129.64ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [97.58ms]
(pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [329.97ms]
(pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [108.76ms]
(pass) bundler > react-compiler/SsrObjectMethodShorthand [564.72ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [115.48ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [100.59ms]
(pass) bundler > react-compiler/BundledReactPreservesImportRefs [314.90ms]
(pass) bundler > react
... (truncated)

release without fix: 2 failed, 1 skipped
bun test v1.4.3-canary.1 (b993710)

test/bundler/transpiler/react-compiler.test.ts:
(pass) bundler > react-compiler/SimpleComponent [15.66ms]
(pass) bundler > react-compiler/ComponentWithHooks [6.45ms]
(pass) bundler > react-compiler/ObjectPatternRestInProps [6.80ms]
(pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [11.70ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [5.56ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [3.00ms]
(pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [8.61ms]
(pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [3.31ms]
(pass) bundler > react-compiler/SsrObjectMethodShorthand [13.31ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [3.68ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [3.69ms]
(pass) bundler > react-compiler/BundledReactPreservesImportRefs [7.13ms]
(pass) bundler > react-compiler/BundledCjsCompilerRuntimeSurvivesTreeShaking [10.89ms]
(pass) bundler > react-compiler/RequireStringPreservesImportRecord [6.83ms]
(pass) bundler > react-compiler/BranchBooleanFeat
... (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 (b993710)

test/bundler/transpiler/react-compiler.test.ts:
(pass) bundler > react-compiler/SimpleComponent [603.87ms]
(pass) bundler > react-compiler/ComponentWithHooks [236.36ms]
(pass) bundler > react-compiler/ObjectPatternRestInProps [319.01ms]
(pass) bundler > react-compiler/UnderscoreAndDollarComponentTags [432.39ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [147.80ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [97.64ms]
(pass) bundler > react-compiler/FullstackHtmlImportCompilesClientGraphInClientMode [280.45ms]
(pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [102.62ms]
(pass) bundler > react-compiler/SsrObjectMethodShorthand [553.36ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [108.30ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [115.93ms]
(pass) bundler > react-compiler/BundledReactPreservesImportRefs [246.87ms]
(pass) bundler > react
... (truncated)

release with fix: 1 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 684ms (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>

```
.../optimization/constant_propagation.rs           | 22 ++++++-----
 test/bundler/transpiler/react-compiler.test.ts     | 46 ++++++++++++++++++++++
 2 files changed, 58 insertions(+), 10 deletions(-)
```

</details>

**gate history** · 1 passed · 0 rejected · iteration 0

<details><summary>evidence per changed file</summary>

```
file                                                     reads  edits  tests
src/react_compiler/optimization/constant_propagation.rs      5      4     10
test/bundler/transpiler/react-compiler.test.ts               1      1      8
```

</details>

**root cause** · written by the author bot

In the constant propagation pass, the StoreLocal, PostfixUpdate, and
PrefixUpdate arms returned the stored constant for the instruction's own
result temporary, so when a consumer such as a negation folded to a
primitive, nothing referenced that temporary and the store's lvalue was
pruned. Codegen then printed the lvalue-less store as a standalone
statement ahead of an earlier store that remained inlined at its use,
reordering the side effects within the expression. The fix makes those
three arms return None for the result temporary while still recording
the constant for the assigned variabl…

<!-- robobun:evidence:end -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants