Repository navigation
react-compiler: keep the declaration of a local when dead code elimination keeps a store to it - #42385
Conversation
… 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.
|
Warning Review limit reached
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. View limit detailsLimit details: You’ve used all 10 included reviews currently available. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (2)
Comment |
|
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);The same happens without nesting (two
|
There was a problem hiding this comment.
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_variablesand both call sites — the block-value site coversDestructure; the retained-instruction site sits inside theStoreLocalbranch, and the comment there correctly explains why a retained non-block-valueDestructuredoesn't need it (rewrite_instructionprunes the dead pattern entries).stubReactWithEffectoverride — replacesreact/index.jswith onlyuseEffect, but the forms don't needuseState/createElementand the jsx-runtime / compiler-runtime stubs are preserved via the spread.- The
onAfterBundleregex asserts eachuseEffect(() => {})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.
|
Updated 5:07 PM PT - Sep 11th, 2026
❌ @robobun, your commit 68bda28 has 2 failures in
🧪 To try this PR locally: bunx bun-pr 42385That installs a local version of the PR into your bun-42385 --bun |
…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>
) ### 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 -->
…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 -->
…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>
…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 -->
…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 -->
Problem
bun build --react-compilercan print an assignment to a local with no declaration:ReferenceError: v is not defined. Withvnever read,try { try {..} catch { v = 1 } .. } catch { v = 2 }printslet v = 1in the inner handler and a barev = 2in the outer one. In client mode the same cause can crash the build:panic: Expected identifier to be initialized.src/react_compiler/optimization/dead_code_elimination.rs) keeps some stores to a local that nothing reads, but pruneslet v: nothing marks the name.rewrite_instruction_kinds_based_on_reassignmentthen makes the first store left the declaration, in a scope that need not enclose the others. Upstream prints the same text.Fix
Reassignstore that the pass retains now marks its variable, solet vstays. ADestructureretained as a block value marks each variable of its pattern.test/bundler/transpiler/react-compiler.test.tsfail without the fix. All 1806 upstream fixtures print byte-identical output before and after, in both output modes. Also ranreact-compiler-fixtures.test.tsand the react tests inbundler_jsx.test.ts.Background
let vis aDeclareLocal,v = 1aStoreLocalof kindReassign.Reassignstore stays while its own SSA value is read.f(v = 2)), or it ends a block whose kind is notBlock. That rule protects value blocks (a ternary branch, afor..oftest). It also covers a catch handler, kindCatch.Notes
Shapes probed (
vis 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.try, a store ends each handler (the report)let v = 1/ barev = 2, ReferenceErrorlet vkept, compiledtryin sequence, a store ends each handler[v] = p.itemsends two handlerslet [v] = ../ bare[v] = .., ReferenceErrorf(v = 2)orc && (v = 2)let v = "init", handler stores,v = "final",<div>{v}</div>(the read folds to"final")for (v of xs) {}orfor (v in xs) {}, thenf(v = 2)or a handler storefor (let v of xs)/ barev = 2, ReferenceErrorfor (..; ..; v = i++)twice, client modepanic: Expected identifier to be initialized(prune_non_escaping_scopes.rs:1060, a scope reassigns a local that was never declared)f(v = 1); f(v = 2),c ? (v = 1) : (v = 2),a && (v = 1); b && (v = 2)Const declaration cannot be referenced as an expression)Nesting is not needed. In a handler nested in a
try, every instruction ends its ownCatchblock (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.0printslet v = 1/ barev = 2for the two-try-in-sequence form. For the nested form it raisesExpected a break targetand skips the function, an older limit on nestedtry.0.0.0-experimental-a1856f3-20260507compiles the nested form and prints the same broken text. It also printsfor (let v of p.items) {..} foo(v = 2).DeadCodeElimination.tsand the Rust port on facebook/react main have the same logic asUPSTREAM_PORTED.Why not narrow the block-value rule to expression blocks. Upstream's
isStatementBlockKindcallscatcha statement kind, so the rule covers handlers by accident. Narrowing it removes the handler cases only. Afor..oftest is a real value block, andlet 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 aReassignstore 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) {}withvnever read used to compile asfor (const v of xs). It is now left alone, like anyfor (v of xs)over an outer local that is read later (Unexpected Reassign variable in for..of collection).Not changed here. The
panicabove is an upstream invariant that the port writes as.expect(). Other invariants written that way also end the build where upstream skips the function.