Skip to content

js_parser: stop dropping side effects when folding !/typeof on array/object/class literals - #34254

Merged
dylan-conway merged 4 commits into
mainfrom
farm/f0bf63a5/simplifier-side-effects
Jul 15, 2026
Merged

dylan-conway merged 4 commits into
mainfrom
farm/f0bf63a5/simplifier-side-effects

Conversation

@robobun

@robobun robobun commented Jul 15, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The expression simplifier folds !expr and typeof expr to a constant without checking whether the operand has side effects, so calls inside array/object/class literals are silently deleted:

$ printf 'const r = ![sideEffect()];\n' | bun build --no-bundle -
const r = false;           # sideEffect() is gone

$ printf 'const r = typeof [sideEffect()];\n' | bun build --no-bundle -
const r = "object";        # sideEffect() is gone

esbuild keeps both unchanged (const r = ![sideEffect()]; / const r = typeof [sideEffect()];).

A related issue in the same visitor: enum constant inlining runs on Foo?.["A"] (computed optional-chain access), turning Foo?.["A"]() into 0(). The dotted form Foo?.A was already guarded.

$ printf 'enum Foo { A }\nlet y = Foo?.["A"]()\n' | bun build --no-bundle -
...
let y = 0 /* A */();       # optional chain dropped

Cause

  • e_unary / UnNot called SideEffects::to_boolean() but only checked .ok, ignoring the CouldHaveSideEffects flag that EArray/EObject/EClass correctly set.
  • Tag::typeof_ returned Some("object") / Some("function") for EArray/EObject/EClass, but its contract (and doc comment) is that the operand has no side effects. Array elements, object properties, and class static initializers can run arbitrary code.
  • e_index called maybe_rewrite_property_access without the optional_chain.is_none() guard that e_dot already has (and that esbuild applies to both).

Fix

  • UnNot: require side_effects == NoSideEffects before replacing with an EBoolean, matching esbuild's UnOpNot path.
  • Tag::typeof_: drop EArray/EObject/EClass. EArrayJSON/EObjectJSON/ERegExp remain since they are pure literals. This matches esbuild's TypeofWithoutSideEffects.
  • e_index: guard maybe_rewrite_property_access with optional_chain.is_none().

The existing constant folding assertions for typeof [] / typeof {} are updated to reflect that these are no longer folded, matching esbuild's behaviour.

Verification

$ USE_SYSTEM_BUN=1 bun test test/bundler/transpiler/simplifier-side-effects.test.ts
# 3 pass, 21 fail

$ bun bd test test/bundler/transpiler/simplifier-side-effects.test.ts
# 24 pass, 0 fail

Also ran transpiler.test.js, bundler_minify.test.ts, esbuild/dce.test.ts, esbuild/default.test.ts, bundler_edgecase.test.ts, bundler_string.test.ts: all pass.


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

fails on main (without fix)
ASAN without fix: 3 failed, 22 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/transpiler/simplifier-side-effects.test.ts test/bundler/transpiler/transpiler.test.js
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (66d74a438)

test/bundler/transpiler/transpiler.test.js:
(pass) Bun.Transpiler > handles errors when parsing macros [4.80ms]
(pass) Bun.Transpiler > normalizes \r\n [6.43ms]
1
(pass) Bun.Transpiler > doesn't hang indefinitely #2746 [4.23ms]
(pass) Bun.Transpiler > property access inlining > bails out with spread [6.75ms]
(pass) Bun.Transpiler > property access inlining > bails out with multiple items [2.68ms]
(pass) Bun.Transpiler > property access inlining > works [2.42ms]
(pass) Bun.Transpiler > property access inlining > works nested [2.11ms]
141 |     });
142 |     it("bails out on optional-chain index into enum", () => {
143 |       const pre = "enum Foo { A }\nenu
... (truncated)

release without fix: 22 skipped
bun test v1.4.0-canary.1 (0dc53862f)

test/bundler/transpiler/transpiler.test.js:
(pass) Bun.Transpiler > handles errors when parsing macros [2.32ms]
(pass) Bun.Transpiler > normalizes \r\n [0.75ms]
1
(pass) Bun.Transpiler > doesn't hang indefinitely #2746 [1.94ms]
(pass) Bun.Transpiler > property access inlining > bails out with spread [0.14ms]
(pass) Bun.Transpiler > property access inlining > bails out with multiple items [0.03ms]
(pass) Bun.Transpiler > property access inlining > works [0.03ms]
(pass) Bun.Transpiler > property access inlining > works nested [0.02ms]
(pass) Bun.Transpiler > property access inlining > bails out on optional-chain index into enum [0.28ms]
(pass) Bun.Transpiler > TypeScript > import Foo = Baz.Bar [0.04ms]
(pass) Bun.Transpiler > TypeScript > ternary should parse correctly when parsing typescript fails [0.03ms]
(pass) Bun.Transpiler > TypeScript > contextual keywords used as plain identifiers keep their statements [0.24ms]
(pass) Bun.Transpiler > TypeScript > does not crash when export default abstract is an expression followed by a class [0.24ms]
(pass) Bun.Transpiler > TypeScript > scope tracking stays balanced when a contextual key
... (truncated)
passes on PR (with fix)
ASAN with fix: 22 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/transpiler/simplifier-side-effects.test.ts test/bundler/transpiler/transpiler.test.js
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (66d74a438)

test/bundler/transpiler/transpiler.test.js:
(pass) Bun.Transpiler > handles errors when parsing macros [4.67ms]
(pass) Bun.Transpiler > normalizes \r\n [6.38ms]
1
(pass) Bun.Transpiler > doesn't hang indefinitely #2746 [4.20ms]
(pass) Bun.Transpiler > property access inlining > bails out with spread [6.92ms]
(pass) Bun.Transpiler > property access inlining > bails out with multiple items [2.62ms]
(pass) Bun.Transpiler > property access inlining > works [2.41ms]
(pass) Bun.Transpiler > property access inlining > works nested [2.10ms]
(pass) Bun.Transpiler > property access inlining > bails out on optional-chain index into enum [17.31ms]
(pass) Bun.Transpiler
... (truncated)

release with fix: 22 skipped
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 755ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[0/5] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: component rust-std is up to date

  nightly-2026-05-06-x86_64-unknown-linux-gnu unchanged - rustc 1.97.0-nightly (e95e73209 2026-05-05)

info: checking for self-update (current version: 1.29.0)
�[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
... (truncated)
diff hotspot
src/ast/expr.rs                                    | 12 +++--
 src/js_parser/visit/visit_expr.rs                  | 33 ++++++++------
 .../transpiler/simplifier-side-effects.test.ts     | 23 ++++++++++
 test/bundler/transpiler/transpiler.test.js         | 52 +++++++++++++++++-----
 4 files changed, 89 insertions(+), 31 deletions(-)

gate history · 4 passed · 0 rejected · iteration 1

evidence per changed file
file                                                     reads  edits  tests
src/ast/expr.rs                                              2      1      0
src/js_parser/visit/visit_expr.rs                            6      3      0
test/bundler/transpiler/simplifier-side-effects.test.ts      0      5      0
test/bundler/transpiler/transpiler.test.js                   2      5      0

The unary simplifier folded `![array]`, `!{obj}`, `!void expr` and
`typeof [array]`/`typeof {obj}`/`typeof class{}` to a constant while
discarding the side effects inside the operand. esbuild only performs
these folds when the operand is known to have no side effects.

* UnNot: require SideEffects::NoSideEffects from to_boolean() before
  replacing with an EBoolean, matching esbuild's UnOpNot handling.
* Tag::typeof_: drop EArray/EObject/EClass so the helper honours its
  "no side effects" contract (array elements, object properties and
  class static initializers can run arbitrary code). EArrayJSON,
  EObjectJSON and ERegExp remain because they are pure literals.

Also guard e_index's maybe_rewrite_property_access with
optional_chain.is_none(), mirroring e_dot. Without this, enum constant
inlining turned `Foo?.["A"]()` into `0()`, losing the optional-chain
guard that the dotted form `Foo?.A` already preserved.

Updated the existing transpiler constant-folding assertions for
`typeof []`/`typeof {}` to reflect that these are no longer folded,
matching esbuild.
@robobun

robobun commented Jul 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Reproduced with printf 'const r = ![sideEffect()];\n' | bun build --no-bundle - emitting const r = false; (side effect dropped). Fix is up; tests fail on the released bun and pass with this build.

Pushback review addressed in 0dc5386: restored ![]/![1,2,3] → !1 folding for removable operands (matching esbuild), folded test assertions into transpiler.test.js, and expanded coverage to every CouldHaveSideEffects branch of to_boolean plus non-identifier enum keys under minify. Comment-length nit addressed in 66d74a4.

Status: diff is green, ready for maintainer review. All transpiler and bundler tests pass locally and on CI. Remaining red lanes across builds #73311, #73346, #73390 are unrelated flaky tests (next-pages dev server, spawn, worker-message-port, socket.io, fetch backpressure/TLS, net memleak, install registry, napi, valkey, webview, es-module-lexer timeout); none touch the parser.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 6 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0b7fbb9c-c5ad-4548-bd42-79c64b241beb

📥 Commits

Reviewing files that changed from the base of the PR and between be77b65 and bbb54e0.

📒 Files selected for processing (4)
  • src/ast/expr.rs
  • src/js_parser/visit/visit_expr.rs
  • test/bundler/transpiler/simplifier-side-effects.test.ts
  • test/bundler/transpiler/transpiler.test.js

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

@robobun

robobun commented Jul 15, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 4:10 PM PT - Jul 15th, 2026

❌ @robobun, your commit 66d74a4 has 3 failures in Build #73390 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 34254

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

bun-34254 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I didn't find any issues — the three fixes each mirror an existing guard in the same file and only make folding more conservative — but this touches js_parser and drops a previously-asserted optimization (typeof [] → "object"), so it's worth a maintainer glance.

Checked: UnNot now requires NoSideEffects (same pattern as visit_binary.rs:395); e_index's new optional_chain.is_none() guard matches e_dot at visit_expr.rs:1441.
Checked: Tag::typeof_ callers all flow through to_typeof() whose doc contract already required no side effects — dropping EArray/EObject/EClass aligns impl with contract.
Checked: EFunction/EArrow/ERegExp/E*JSON remaining in typeof_ are genuinely pure at evaluation.

Extended reasoning...

Overview

Three targeted correctness fixes in the expression simplifier, all in the direction of less aggressive constant folding:

  • src/ast/expr.rs: Tag::typeof_ drops EArray/EObject/EClass so typeof [f()] is no longer folded to "object" while discarding f().
  • src/js_parser/visit/visit_expr.rs UnNot: adds side_effects == NoSideEffects before replacing !expr with a boolean literal.
  • src/js_parser/visit/visit_expr.rs e_index: gates maybe_rewrite_property_access on optional_chain.is_none(), so Enum?.["A"] is no longer inlined to 0.

Plus a new test file and updates to transpiler.test.js reflecting that typeof [] / typeof {} are no longer folded.

Security risks

None. This is transpiler constant-folding logic; no auth, crypto, I/O, or untrusted-input parsing surface changed. The changes only cause the transpiler to preserve more of the input.

Level of scrutiny

Medium-high. js_parser runs on every file Bun processes, so regressions have wide blast radius. That said, each change here strictly narrows an optimization (adds a guard), so the failure mode is "missed optimization" rather than "wrong output". Each guard is copied from an adjacent, already-shipping site: the NoSideEffects conjunction matches visit_binary.rs:395, and the optional_chain.is_none() guard matches the e_dot path at visit_expr.rs:1441. The typeof_ change realigns the implementation with its own doc comment at expr.rs:3142.

Other factors

  • The one non-mechanical decision is dropping EArray/EObject/EClass from typeof_ wholesale rather than inspecting their contents for purity. That's what esbuild's TypeofWithoutSideEffects does and typeof_ only sees the tag, so it's the natural fix — but it does mean typeof [] on a genuinely-pure empty literal is no longer folded, and existing test assertions were updated to reflect that. A maintainer should confirm they're fine with that trade-off.
  • I confirmed the remaining typeof_ arms (EFunction, EArrow, ERegExp, EArrayJSON, EObjectJSON, primitives) are pure at evaluation time, so no analogous bug remains there.
  • Test coverage is thorough: both minify modes, all three literal kinds, !/!!/typeof/typeof ===, a runtime spawn test asserting side effects actually execute, and negative tests that pure primitives still fold. PR reports the wider bundler/esbuild suites pass.
  • No CODEOWNERS on these paths; no prior reviewer comments to address.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I didn't find any bugs. The three changes are each strictly more conservative (less folding / less rewriting), so they can't introduce new miscompilations — but this is the core transpiler simplifier that runs on every file, and it changes existing typeof [] / typeof {} folding behaviour, so a human should sign off.

What was reviewed:

  • Tag::typeof_ — only caller is the UnTypeof fold at visit_expr.rs:1223; EFunction/EArrow/ERegExp/E*JSON remain and are genuinely side-effect-free.
  • UnNot — the new NoSideEffects guard matches the pattern already used at scan_side_effects.rs:84/95.
  • e_index optional-chain guard — mirrors the existing e_dot guard at visit_expr.rs:1441.
  • Updated transpiler.test.js assertions match the new (esbuild-aligned) output; no other callers of to_typeof/typeof_ exist.
Extended reasoning...

Overview

Three narrow correctness fixes in the expression simplifier, all in src/js_parser/visit/visit_expr.rs and src/ast/expr.rs:

  1. UnNot folding now requires side_effects == NoSideEffects in addition to .ok before replacing !expr with a boolean literal.
  2. Tag::typeof_ drops EArray / EObject / EClass so typeof [f()] etc. are no longer folded to a string constant. EArrayJSON / EObjectJSON / ERegExp / EFunction / EArrow remain (all side-effect-free at evaluation time).
  3. e_index now guards maybe_rewrite_property_access with optional_chain.is_none(), matching the sibling e_dot path at line 1441 and esbuild's behaviour, so Enum?.["A"] is no longer inlined to a bare constant.

Plus a new 24-case test file and updates to 11 existing assertions in transpiler.test.js.

Security risks

None. Pure AST-level constant-folding logic; no I/O, auth, crypto, or untrusted-input parsing changes.

Level of scrutiny

High — this is the transpiler's expression simplifier, which runs on every JS/TS file Bun processes (runtime, bundler, bun build --no-bundle). A regression here miscompiles user code silently. That said, every change is in the conservative direction: each one adds a bailout condition to an existing optimization. The worst plausible failure mode is losing a valid optimization (e.g. typeof [] no longer folds to "object"), not introducing a new wrong transformation.

Other factors

  • I confirmed Tag::typeof_ has exactly one effective call site (visit_expr.rs:1223 via SideEffects::typeof_ → to_typeof), so narrowing it can't break other consumers. SideEffects::to_type_of exists but has no callers.
  • The NoSideEffects check pattern is already used identically elsewhere in scan_side_effects.rs (lines 82-99), so this is following an established idiom.
  • The e_index guard is a copy of the existing e_dot guard — the asymmetry was clearly an oversight.
  • Test coverage is thorough: both minify on/off, all three literal kinds, a runtime execution assertion, and negative tests confirming pure primitives still fold. The PR ran the wider bundler/esbuild test suites clean.
  • The one behaviour change a human should explicitly acknowledge: typeof [] and typeof {} (with pure/empty contents) no longer fold. This matches esbuild and is reflected in the updated transpiler.test.js assertions, but it is a visible output change to previously-asserted behaviour.

Deferring because core-transpiler changes that alter existing test expectations warrant a human sign-off, even when the diff itself looks correct.

* UnNot: also fold when expr_can_be_removed_if_unused() is true, so pure
  ![]/!{}/![1,2,3] keep folding to false. This mirrors esbuild's
  SimplifyBooleanExpr default arm and avoids regressing the minify output
  for side-effect-free literals.
* Cover every CouldHaveSideEffects branch of to_boolean under UnNot:
  !(f(),true), !(f()||1), !(f()&&0), !typeof f().
* Add enum optional-chain tests to the existing property-access-inlining
  describe, including a non-identifier key under minify (the path that
  reaches the new e_index guard).
* Trim the new test file to the runtime spawn assertion; the expectPrinted
  assertions now live next to the existing constant-folding coverage in
  transpiler.test.js.

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

Beyond the comment-length nit: I checked the other callers of Tag::typeof_/to_typeof — only the UnTypeof visitor and SideEffects wrappers reach it, so dropping EArray/EObject/EClass does not silently regress a separate comparison-folding path. The new optional_chain.is_none() guard in e_index mirrors the existing one for e_dot (visit_expr.rs:1442). EFunction/EArrow remain in typeof_; function/arrow expressions are pure at evaluation time so keeping them is correct.

Extended reasoning...

This is a semantic change to the JS simplifier that affects every transpiled file, so I am not approving. The logic itself looks right and matches the documented contract on to_typeof ("has no side effects (i.e. can be removed without consequence)"), and the UnNot gate now short-circuits on NoSideEffects before falling back to expr_can_be_removed_if_unused, which is why pure []/!{} still fold. I traced the callers of Tag::typeof_ to confirm the removal is scoped to the one visitor path, and confirmed the e_index optional-chain guard is the same shape as the pre-existing e_dot guard. Noting these here so a human reviewer does not need to re-derive them.

Comment thread test/bundler/transpiler/simplifier-side-effects.test.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I didn't find any correctness bugs, but this touches the parser's constant-folding hot path and changes existing optimization behavior, so it's worth a human look.

What was reviewed:

  • Tag::typeof_ now honors its no-side-effects contract; checked callers (to_typeof/to_type_of → UnTypeof visitor) — all fall through safely on None.
  • UnNot guard: expr_can_be_removed_if_unused fallback keeps []→!1 for pure operands, so no regression there.
  • e_index optional-chain guard mirrors the existing e_dot guard at visit_expr.rs:1442 exactly.
  • Noted: typeof []/typeof {} on pure literals are no longer folded (unlike []) — the asymmetry is because typeof_ only sees the tag; a maintainer may want to weigh keeping that fold vs. the simpler fix.
Extended reasoning...

Overview

Three targeted fixes in the JS parser's expression simplifier, all correctness-over-optimization:

  • src/ast/expr.rs — Tag::typeof_() drops EArray/EObject/EClass (whose contents can run arbitrary code), keeping only guaranteed-pure tags. This makes the function honor the contract stated in to_typeof()'s doc comment.
  • src/js_parser/visit/visit_expr.rs — UnNot now requires NoSideEffects or expr_can_be_removed_if_unused before folding to a boolean; e_index gains the same optional_chain.is_none() guard that e_dot already has (line 1442) so enum inlining doesn't strip ?. semantics.
  • Tests: a new runtime spawn test plus ~30 transpiler-output assertions in transpiler.test.js, including updates to 10 pre-existing assertions that now expect unfolded output.

Security risks

None. This is pure AST-rewriting logic operating on already-parsed data; no I/O, no auth, no untrusted length arithmetic. The change is strictly more conservative (folds less), so it can't introduce new incorrect code emission — only reduce optimization.

Level of scrutiny

High. js_parser is production-critical: every line of JS/TS Bun compiles flows through this visitor, and the removed typeof []/typeof {} folds change output for existing user code. The fix itself is small and well-argued against esbuild's reference (TypeofWithoutSideEffects, UnOpNot), but two things merit a maintainer's judgment:

  1. Asymmetry: UnNot retains the pure-literal fold via expr_can_be_removed_if_unused, but UnTypeof does not (because Tag::typeof_ only sees the tag, not the expr). So [] still folds to !1 but typeof [] no longer folds to "object". This matches esbuild, but a maintainer might prefer adding the same expr_can_be_removed_if_unused check at the UnTypeof call site instead.
  2. Changed existing assertions: 10 assertions in transpiler.test.js were rewritten from folded to unfolded. That's the correct move given the fix, but per CLAUDE.md's "never silently weaken an existing test" guidance, a human should confirm the optimization loss is acceptable.

Other factors

  • The e_index guard is a mechanical mirror of the e_dot guard two hundred lines below — verified byte-for-byte pattern match.
  • Test coverage is thorough: every CouldHaveSideEffects branch of to_boolean, both minify modes for the enum optional-chain case, and a runtime subprocess proof.
  • My prior nit (4-line header comment) was addressed in 66d74a4.
  • The bundled e_index optional-chain fix is a separate bug in the same visitor; it's small and correct but does widen the PR's scope slightly.

@dylan-conway
dylan-conway merged commit 8395f15 into main Jul 15, 2026
77 of 80 checks passed
@dylan-conway
dylan-conway deleted the farm/f0bf63a5/simplifier-side-effects branch July 15, 2026 23:34
dylan-conway pushed a commit that referenced this pull request Jul 16, 2026
### What

`#34249` changed TypeScript enum lowering so that only **module-scope**
enums emit `var`; an enum in a function, method, or block body now emits
`let`.

`#34250` merged four minutes earlier and added `it("rejects
yield/await/this/super in enum initializers")`, whose expectations still
assert `var x` for **block-scoped** enums. Each PR was green against a
`main` that lacked the other's change, so the collision only appeared
once both had landed — and since `main` pushes run no test shards,
nothing caught it.

The result is that `main` asserts output its own parser no longer
produces. `test/bundler/transpiler/transpiler.test.js` currently fails
on every PR that merges `main`.

### The fix

Updates the five stale expectations to `let`. All five are enums nested
in a function or method body:

| Line | Case |
|---|---|
| 771 | `function *f() { enum x { y = (function*() { yield 1 })() } }` |
| 775 | `async function f() { enum x { y = (async () => await 1)() } }`
|
| 785 | `function *f() { enum x { y = 1 } yield 1; }` |
| 789 | `async function f() { enum x { y = 1 } await 1; }` |
| 793 | `class C extends B { m() { enum x { y = 1 } super.foo(); } }` |

Deliberately unchanged:

- **Line 779** — `enum x { y = (function() { return this })() }` is
top-level, so `var` is still correct.
- **Namespace expectations** — namespaces only appear at module scope or
nested in another namespace, where both the old and new predicates
agree.

### Note for reviewers

Only the line-771 failure is visible in CI: `expectPrinted_` throws at
the first mismatch, masking the other four. A fix touching only the
reported line would go red again on the next one, so all five are
updated together.

### Verification

Ran `test/bundler/transpiler/transpiler.test.js` against a build
containing `#34249`:

- pristine `main`: 176 pass / 4 fail
- with this change: 177 pass / 3 fail

The change flips exactly the one enum test and touches nothing else. The
3 remaining failures are unrelated to this diff — they are skew between
that build and two commits that landed after it (`#34254`, `#34258`),
and build from source in CI.

Also swept the repo for any other assertion of enum-lowering text (the
closure IIFE shape `(x ||= {})` / `(x = x || {})`). Only two files
assert it: this one, and
`test/js/node/module/require-extensions.test.ts:129`, whose fixture
declares a top-level enum and is correctly `var`.
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