Skip to content

react_compiler: preserve E::UnaryFlags through codegen (delete/typeof Reference semantics) - #36741

Merged
Jarred-Sumner merged 6 commits into
mainfrom
farm/32740203/react-compiler-delete-flag
Aug 2, 2026
Merged

react_compiler: preserve E::UnaryFlags through codegen (delete/typeof Reference semantics)#36741
Jarred-Sumner merged 6 commits into
mainfrom
farm/32740203/react-compiler-delete-flag

Conversation

@robobun

@robobun robobun commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Problem

delete obj.prop and typeof undeclaredGlobal inside a react-compiled function both lose their parse-time E::UnaryFlags on the round-trip through HIR, so the printer's (0, ...) guard (src/js_printer/lib.rs:4003-4004) re-wraps them:

import { useMemo } from "react";
export function useThing(props) {
  return useMemo(() => {
    const x = { a: props.a, b: props.b };
    delete x.b;                                // prints `delete (0, x.b)` → no-op, returns true
    const k = "a";
    delete x[k];                               // prints `delete (0, x["a"])` → no-op
    return x;
  }, [props.a, props.b]);
}
export function useIsBrowser() {
  return useMemo(() => typeof window !== "undefined", []);  // prints `typeof (0, window)` → ReferenceError under SSR
}
$ bun build entry.jsx --react-compiler --external='*' --target=browser
    ..., delete (0, x.b), delete (0, x["a"]), ...
    return typeof (0, window) !== "undefined";

Cause

src/react_compiler/codegen.rs rebuilt every E::Unary with flags: UnaryFlags::empty():

  • InstructionValue::PropertyDelete / InstructionValue::ComputedDelete dropped WAS_ORIGINALLY_DELETE_OF_IDENTIFIER_OR_PROPERTY_ACCESS, so the printer wrapped every delete <dot|index> as delete (0, ...), which evaluates the property to a value and returns true without deleting.
  • InstructionValue::UnaryExpression dropped WAS_ORIGINALLY_TYPEOF_IDENTIFIER, so the printer wrapped typeof undeclaredGlobal as typeof (0, undeclaredGlobal), which throws ReferenceError instead of returning "undefined".

lower_unary (src/react_compiler/lowering/build_hir/expr.rs) discarded unary.flags at construction time, so codegen had nothing to restore.

Fix

  • typeof: thread unary.flags through HIR. InstructionValue::UnaryExpression gains a bun_flags: E::UnaryFlags field; lowering copies the visited node's flags in and codegen copies them back out. This preserves the visitor's own distinction between typeof x (flag set, no wrap) and typeof (folded-to-x) (flag unset, visitor already wrapped as a real (0, x) comma expression, stays throwing).
  • delete: set the flag unconditionally at both codegen sites, and gate lower_unary's EDot/EIndex arms on unary.flags so a flagless operand (the visitor folded delete (true ? a.b : c.d) to a bare EDot) falls through to the existing "Only object properties can be deleted" bailout. That matches upstream's Babel plugin, which sees the unfolded ConditionalExpression and bails the same way, and keeps the spec no-op semantics for that edge case.
  • E::UnaryFlags gains Debug (the HIR enum derives Debug); the other bitflags! in src/ast/ already do.

Babel's reference output for the upstream delete-property / delete-computed-property fixtures is plain delete x.b / delete x["b"], which this now matches.

Verification

$ bun bd test test/bundler/transpiler/react-compiler.test.ts

New tests (PropertyDeletePreservesReferenceSemantics, TypeofUnboundIdentifierPreservesFlag) fail on main (delete (0, / typeof (0, window) in the output; runtime {"a":1,"b":2,"c":3} / ReferenceError) and pass with this change. DeleteFoldedConditionalKeepsNoOpSemantics guards the edge case that an earlier revision of this PR regressed.

Full react-compiler.test.ts (35 pass), react-compiler-fixtures.test.ts (3293 pass), bundler_edgecase.test.ts (117 pass), transpiler.test.js (183 pass).


[review] gate passed · iteration 1 · 6 files touched

fails on main (without fix)
ASAN without fix: 2 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/transpiler/react-compiler.test.ts
bun test v1.4.0 (24ba9a8d5)

test/bundler/transpiler/react-compiler.test.ts:
(pass) bundler > react-compiler/SimpleComponent [666.07ms]
(pass) bundler > react-compiler/ComponentWithHooks [318.58ms]
(pass) bundler > react-compiler/ObjectPatternRestInProps [310.09ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [167.71ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [99.67ms]
(pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [98.31ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [92.83ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [88.96ms]
(pass) bundler > react-compiler/BundledReactPreservesImportRefs [328.38ms]
(pass) bundler > react-compiler/BundledCjsCompilerRuntimeSurvivesTreeShaking [408.04ms]
(pass) bundler > react-compiler/RequireStringPreservesImportRecord [308.97ms]
(pass) bundler > react-compiler/BranchBooleanFeatureFlagPreservesDCE [297.46ms]
(pass) bundler >
... (truncated)

release without fix: 2 FAILED
bun test v1.4.0-canary.1 (1498d7b77)

test/bundler/transpiler/react-compiler.test.ts:
(pass) bundler > react-compiler/SimpleComponent [25.82ms]
(pass) bundler > react-compiler/ComponentWithHooks [7.97ms]
(pass) bundler > react-compiler/ObjectPatternRestInProps [6.81ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [5.18ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [3.72ms]
(pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [3.44ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [3.27ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [2.90ms]
(pass) bundler > react-compiler/BundledReactPreservesImportRefs [7.82ms]
(pass) bundler > react-compiler/BundledCjsCompilerRuntimeSurvivesTreeShaking [17.67ms]
(pass) bundler > react-compiler/RequireStringPreservesImportRecord [6.30ms]
(pass) bundler > react-compiler/BranchBooleanFeatureFlagPreservesDCE [6.19ms]
(pass) bundler > react-compiler/ForwardRefSiblingFn [6.28ms]
(pass) bundler > react-compiler/SelfRefConstArrow [6.76ms]
(pass) bundler > react-compiler/OutlinedFunctionMinify-syntax=false-identifiers=true 
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/transpiler/react-compiler.test.ts
bun test v1.4.0 (24ba9a8d5)

test/bundler/transpiler/react-compiler.test.ts:
(pass) bundler > react-compiler/SimpleComponent [653.40ms]
(pass) bundler > react-compiler/ComponentWithHooks [308.29ms]
(pass) bundler > react-compiler/ObjectPatternRestInProps [318.35ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Browser [165.34ms]
(pass) bundler > react-compiler/OutputModeDefaultsByTarget-Bun [95.91ms]
(pass) bundler > react-compiler/OutputModeExplicitSsrOverridesTarget [96.14ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Client [91.32ms]
(pass) bundler > react-compiler/OutputModeIgnoredWhenCompilerDisabled-Ssr [95.26ms]
(pass) bundler > react-compiler/BundledReactPreservesImportRefs [334.42ms]
(pass) bundler > react-compiler/BundledCjsCompilerRuntimeSurvivesTreeShaking [419.07ms]
(pass) bundler > react-compiler/RequireStringPreservesImportRecord [330.33ms]
(pass) bundler > react-compiler/BranchBooleanFeatureFlagPreservesDCE [290.63ms]
(pass) bundler >
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     24ba9a8d51
  features     baseline

22 deps, 108 codegen, 1171 objects in 810ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1234] install /workspace/bun
bun install v1.4.0-canary.1 (1498d7b77)

Checked 124 installs across 170 packages (no changes) [17.00ms]
[2/1234] install /workspace/bun/packages/bun-error
bun install v1.4.0-canary.1 (1498d7b77)

Checked 1 install across 2 packages (no changes) [7.00ms]
[3/1234] gen bindgenv2
[4/1234] gen ErrorCode+*.h
[5/1234] fetch picohttpparser
[picohttpparser] up to date
[6/1234] gen .bind.ts → GeneratedBindings.cpp
[7/1234] install /workspace/bun/src/node-fallbacks
bun install v1.4.0-canary.1 (1498d7b77)

Checked 129 installs across 147 packages (no changes) [7.00ms]
[8/1234] fetch tinycc
[tinycc] up to date
[9/1234] fetch zlib
[zlib] up to date
[10/1234] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[11/1234] gen JSBuffer.lut.h
Generating /workspace/bun/build/release/codegen/JSBuffer.lut.h from /workspace/bun/src/jsc/bindings
... (truncated)
diff hotspot
src/ast/e.rs                                       |   2 +-
 src/react_compiler/codegen.rs                      |  12 ++-
 src/react_compiler/hir/mod.rs                      |   2 +
 src/react_compiler/lowering/build_hir/expr.rs      |  16 ++-
 .../optimization/constant_propagation.rs           |   1 +
 test/bundler/transpiler/react-compiler.test.ts     | 119 +++++++++++++++++++++
 6 files changed, 145 insertions(+), 7 deletions(-)

gate history · 1 passed · 0 rejected · iteration 1

evidence per changed file
file                                                     reads  edits  tests
src/ast/e.rs                                                 1      1      0
src/react_compiler/codegen.rs                                2      5      0
src/react_compiler/hir/mod.rs                                2      2      0
src/react_compiler/lowering/build_hir/expr.rs                3      4      0
src/react_compiler/optimization/constant_propagation.rs      1      1      0
test/bundler/transpiler/react-compiler.test.ts               1      4      0

PropertyDelete and ComputedDelete emitted E::Unary with UnaryFlags::empty().
The printer re-wraps any 'delete <dot|index>' missing the
WAS_ORIGINALLY_DELETE_OF_IDENTIFIER_OR_PROPERTY_ACCESS flag as
'delete (0, obj.prop)', which evaluates the property to a value and
returns true without deleting.

Lowering only creates these instructions from 'delete <EDot>' /
'delete <EIndex>' (both non-optional-chain), for which the parser
always sets the flag, so codegen can set it unconditionally.
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The React Compiler now preserves unary-expression flags through HIR and code generation. Delete and typeof behavior retain JavaScript reference semantics. Bundler regression tests cover property deletion, undeclared identifiers, conditional expressions, and folded deletes.

Unary flag preservation

Layer / File(s) Summary
Capture unary flags in HIR
src/ast/e.rs, src/react_compiler/hir/mod.rs, src/react_compiler/lowering/build_hir/expr.rs, src/react_compiler/optimization/constant_propagation.rs
HIR stores source unary flags. Delete lowering checks the original property-access flag before emitting property-delete operations.
Restore flags during code generation
src/react_compiler/codegen.rs
Code generation forwards HIR flags and marks dot and computed deletes with the original property-access delete flag.
Validate reference semantics
test/bundler/transpiler/react-compiler.test.ts
Regression tests cover property and computed deletion, typeof on undeclared identifiers, conditional throwing, and folded conditional deletion.

Possibly related PRs

  • oven-sh/bun#36734: Both PRs preserve JavaScript delete reference semantics in different compiler stages.
  • oven-sh/bun#36740: Both PRs modify JavaScript delete handling and add related regression tests.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies preserving E::UnaryFlags through React Compiler code generation for delete and typeof semantics.
Description check ✅ Passed The description explains the problem, cause, fix, affected semantics, and verification results with sufficient technical detail.

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

Comment thread src/react_compiler/codegen.rs Outdated
Comment thread src/react_compiler/codegen.rs Outdated
…ete lowering on parse-time flag

The printer's (0, x) re-wrap for typeof/delete is keyed on the parse-time
UnaryFlags. Lowering discarded them, so codegen rebuilt E::Unary with
empty flags and the printer re-wrapped every typeof/delete.

For typeof: add bun_flags to InstructionValue::UnaryExpression and pass
unary.flags through lowering -> codegen unchanged. This preserves the
visitor's distinction between 'typeof x' (flag set, no throw) and
'typeof (folded-to-x)' (flag unset, visitor wraps as (0, x), still throws).

For delete: gate lower_unary's EDot/EIndex arms on the parse-time flag.
A flagless EDot/EIndex operand (visitor folded 'delete (true ? a.b : c.d)')
now falls through to the 'Only object properties can be deleted' bailout,
matching upstream's behaviour on the unfolded ConditionalExpression. This
makes the codegen-side flag restoration unconditional and correct.
Comment thread src/react_compiler/codegen.rs Outdated
Comment thread src/react_compiler/codegen.rs Outdated
Comment thread src/react_compiler/codegen.rs Outdated
Comment thread src/react_compiler/hir/mod.rs Outdated
Comment thread src/react_compiler/lowering/build_hir/expr.rs Outdated
Comment thread src/react_compiler/lowering/build_hir/expr.rs Outdated
@robobun robobun changed the title react_compiler: preserve delete Reference semantics in codegen react_compiler: preserve E::UnaryFlags through codegen (delete/typeof Reference semantics) Aug 1, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/bundler/transpiler/react-compiler.test.ts`:
- Around line 560-565: Remove the explanatory regression comments surrounding
the affected tests and retain only the associated issue URL comment for each
regression test, preserving all test IDs and assertions unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4539d754-6de9-43f1-969a-6449638317ea

📥 Commits

Reviewing files that changed from the base of the PR and between f91d5c9 and ba9983e.

📒 Files selected for processing (6)
  • src/ast/e.rs
  • src/react_compiler/codegen.rs
  • src/react_compiler/hir/mod.rs
  • src/react_compiler/lowering/build_hir/expr.rs
  • src/react_compiler/optimization/constant_propagation.rs
  • test/bundler/transpiler/react-compiler.test.ts

Comment thread test/bundler/transpiler/react-compiler.test.ts

@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.

LGTM — both concerns from my earlier review are addressed (typeof now threads bun_flags through HIR rather than guessing from operand shape; lower_unary's EDot/EIndex arms are gated on the parse-time flag so the visitor-folded delete (cond ? a.b : c.d) case falls through to the bailout), and the comment-cop nits are trimmed.

What was reviewed:

  • Confirmed lower_unary is the only constructor of InstructionValue::UnaryExpression; every other match site uses .. so the new field needs no further updates.
  • Checked constant_propagation's TypeOf arm is a no-op (returns None), so bun_flags can't go stale mid-pipeline; a stale flag would be harmless anyway since the printer's wrap also requires is_unbound_identifier.
  • Verified the visitor's UnTypeof id_before/id_after guard is what makes threading (vs. operand-shape inference) correct for the typeof (true ? x : y) case the new test covers.
Extended reasoning...

Overview

This PR fixes two react-compiler codegen bugs where E::UnaryFlags were dropped on the HIR round-trip, causing the printer's (0, ...) guard (js_printer/lib.rs:4003-4004) to re-wrap delete obj.prop and typeof undeclaredGlobal — turning the former into a no-op and the latter into a ReferenceError. Six files touched: a Debug derive on E::UnaryFlags (needed by HIR's derive), a new bun_flags field on InstructionValue::UnaryExpression, the lowering/codegen wiring for it, a flag gate on lower_unary's delete arms, a .. on constant_propagation's match, and three new itBundled tests.

Security risks

None. This is AST-flag plumbing inside the bundler's react-compiler pass; no untrusted-input parsing, no I/O, no auth/crypto surface.

Level of scrutiny

Moderate. It's a semantics-affecting transpiler fix (incorrect output could silently change runtime behavior of user code), but the mechanism is well understood, narrowly scoped, and the printer guard it interacts with is exactly two lines. I already reviewed an earlier revision and raised the two issues that shaped the current design; both are now resolved with the more-robust threading approach (vs. my original suggestion of inferring the flag from the codegen-time operand shape, which robobun correctly pointed out would regress the folded-conditional typeof case).

Other factors

  • Whole-class coverage: both UnaryFlags bits are handled; the delete edge case (visitor-folded conditional operand) that the first revision regressed is now explicitly guarded and tested.
  • Constructor audit: grepped all InstructionValue::UnaryExpression sites — the sole constructor is lower_unary; every consumer either destructures with .. or is the codegen site this PR updates, so the field addition is complete.
  • Tests: each of the three new tests asserts both output shape (regex on the bundle) and runtime behavior (run: { stdout }), and each includes a sanity assertion that the function was actually compiled (not bailed) so the codegen path is on trial. Test comment style matches the file's existing // Regression: ... convention (CodeRabbit withdrew its objection).
  • Prior feedback: all inline threads (my two, comment-cop's five, CodeRabbit's one) are marked resolved; the follow-up commits ba9983e/0c85de39 trimmed the comments to single lines as requested.

Comment thread src/react_compiler/codegen.rs

@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.

LGTM — all three prior concerns are addressed at 24ba9a8 (typeof threads bun_flags through HIR; lower_unary now gates PropertyDelete/ComputedDelete on the parse-time flag so the codegen invariant holds; the accidental src/ revert in ddf056e is restored).

What was reviewed:

  • Verified lower_unary is the only InstructionValue::UnaryExpression construction site; every other match uses .., so the new field is fully wired.
  • git diff f91d5c95..HEAD at the current tip is the full 6-file change; the net src/ diff is no longer empty.
  • The three new tests cover dot/index delete, typeof window vs. typeof (folded ? …), and the folded-conditional-delete bailout.
Extended reasoning...

Overview

Threads E::UnaryFlags through the react-compiler HIR so delete obj.prop and typeof undeclaredGlobal round-trip with their parse-time flags intact, preventing the printer's (0, ...) guard from re-wrapping them into semantically-different forms. Touches src/ast/e.rs (adds Debug to the bitflags), hir/mod.rs (new bun_flags field), lowering/build_hir/expr.rs (captures the flag; gates delete lowering on it), codegen.rs (restores the flag; sets the delete flag at both delete sites), constant_propagation.rs (adds .. to its match), plus three itBundled tests.

Security risks

None. Purely a compiler correctness fix in the react-compiler pass; no untrusted input parsing, no auth/crypto/permissions.

Level of scrutiny

Medium-low. The change is mechanical (add a field, copy it in at lowering, copy it out at codegen). The one subtle piece — gating EDot/EIndex delete arms on the parse-time flag so a visitor-folded delete (true ? o.a : o.b) bails out instead of becoming a real delete — was raised in my earlier review and is now correctly implemented, with the codegen-side comment ("lower_unary only creates PropertyDelete when this flag was set") now accurate. The typeof fix threads the visited node's flags rather than guessing from operand shape, which correctly distinguishes typeof window (flag set → no wrap) from typeof (true ? undeclared : other) (visitor already wrapped, flag unset → stays throwing).

Other factors

This PR has been through two prior review rounds; both blocking findings and the accidental-revert catch are now resolved and the threads are marked resolved. The robobun evidence block shows the new tests fail on main (delete (0, / typeof (0, window)) and pass with the fix under both ASAN-debug and release. Full react-compiler.test.ts (35), react-compiler-fixtures.test.ts (3293), bundler_edgecase.test.ts (117), and transpiler.test.js (183) pass. I grep-verified that lower_unary is the sole construction site for InstructionValue::UnaryExpression; every other reference is a .. pattern match, so no other producer needs updating. No bugs were found by the bug hunting system this run.

@robobun

robobun commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: builds #87401 and #87419 both failed with all 26 build-* jobs in state expired (no agents picked them up before the queue timeout) and zero test annotations. Nothing in the diff was exercised.

The diff at 24ba9a8 is ready: react-compiler.test.ts (35 pass), react-compiler-fixtures.test.ts (3293 pass), bundler_edgecase.test.ts (117 pass) and transpiler.test.js (183 pass) all green locally under bun bd. Fast GitHub checks (cargo clippy, Format, Lint JavaScript, Source lints) passed. Needs a Buildkite rebuild once agents are available.

@Jarred-Sumner
Jarred-Sumner merged commit 81a68d3 into main Aug 2, 2026
50 of 51 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/32740203/react-compiler-delete-flag branch August 2, 2026 00:32
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