parser: drop dead-branch var with zero identifiers instead of emitting var;#31003
Conversation
…ing `var;`
`if (0) var []` panicked the printer at `src/js_printer/lib.rs:2308`
(`unreachable!()` for an empty decl list). DCE's
`should_keep_stmt_in_dead_control_flow` hoists identifiers out of
destructuring patterns so a dead `var [a, b] = expr` still reserves `a`
and `b` with `undefined`. When the pattern binds no identifiers
(`var []`, `var {}`) the hoisted list collapses to empty, but the
statement was kept in the output — producing an empty `var;` that the
printer refuses to emit.
Drop the statement entirely when the hoisted-identifier list is empty.
Nothing to hoist, nothing to emit.
Fixes #31002.
|
Updated 12:03 PM PT - May 18th, 2026
✅ @robobun, your commit 1eeb7004f94f7d1275ce9e5bd812efb571bc80ad passed in 🧪 To try this PR locally: bunx bun-pr 31003That installs a local version of the PR into your bun-31003 --bun |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughThis PR fixes a crash in Bun's parser when encountering invalid destructuring ChangesDead-code destructuring handling
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
Debug/ASAN builds fail with `use of undeclared identifier "assert"` in `uv__tty_make_raw`. libc `<assert.h>` was reaching the TU transitively through a WebKit header that the recent WebKit upgrade dropped. `ASSERT` is the WTF-style macro already used elsewhere in this file (see `uv_tty_reset_mode` below) and is in scope via WTF headers. Drive-by for the same break tracked in #30988 / fixed in #30992 — this PR needs the same one-liner so its debug lane can build.
There was a problem hiding this comment.
LGTM — minimal, well-targeted crash fix with a solid regression test.
Extended reasoning...
Overview
This PR touches three files. The core change is a 4-line early-return in src/js_parser/scan/scan_side_effects.rs::should_keep_stmt_in_dead_control_flow: when a dead-branch var destructuring pattern yields zero hoisted identifiers (e.g. if(0) var []), the statement is now dropped instead of being kept with an empty decl list — which previously tripped an unreachable!() in the printer. A new regression test at test/regression/issue/31002.test.ts exercises both the crash repros and a guard that non-empty destructuring still hoists correctly. A second commit swaps assert() → ASSERT() in wtf-bindings.cpp to unblock the debug build (matching the existing ASSERT usage elsewhere in that same file).
Security risks
None. The parser change only affects what gets emitted for statically-dead var declarations with empty destructuring patterns — there is no user-controlled data flow, allocation, or boundary crossing involved. The C++ change is a debug-only assertion macro swap with no runtime semantics change in release builds.
Level of scrutiny
Low. The Rust change is a textbook empty-collection guard placed exactly where the invariant would otherwise be violated, symmetric with the surrounding "omit entirely" branches for SEmpty/SExpr/etc. The fast-path above it (single BIdentifier decl) is unaffected. The assert → ASSERT swap is a one-token change to the project's standard WTF assertion macro, already used at line ~256 of the same file.
Other factors
The regression test is thorough: it runs each crash repro in a subprocess (so a regression panics the child, not the runner), covers both var [] and var {} forms plus a nested-scope variant, and separately verifies that if(0) var [a,b] = [] still hoists a/b as undefined. No CODEOWNERS paths are touched. The robobun CI failures reference the first commit (pre-ASSERT fix) and are the musl debug-build break the second commit addresses, plus an unrelated Windows-aarch64 agent provisioning failure. The wtf-bindings change is a small ride-along but is acknowledged in the PR description and overlaps with #30992; it's trivially safe.
Fixes #31002.
Repro
if (0) var []/if (0) var {}— a destructuringvarwith zerobindings inside a statically-dead
ifbody — crashed the printer inprint_declswhereunreachable!()guards against empty decl lists("var;" is invalid syntax).
if(0)is load-bearing in the reproducer:non-dead branches ship the statement through to JSC, which rejects it
with a regular
SyntaxError; the dead-branch path never reaches JSC.Cause
scan_side_effects.rs::should_keep_stmt_in_dead_control_flow(the DCEhelper that decides what to retain in
if (false) …) handlesvarbyhoisting every identifier out of its destructuring pattern so scope
semantics are preserved:
For
var []the destructuring pattern binds no identifiers, so thehoisted vector is empty. The code then installed the empty vec as the
new decl list and returned
true, telling the visitor to keep anS::Localcarrying zero decls. The next pass handed that to theprinter and tripped the invariant.
Fix
src/js_parser/scan/scan_side_effects.rs: when the collectedidentifier list is empty, return
falseand drop the statemententirely — nothing to hoist and no valid
varoutput exists. This issymmetric with the existing "omit entirely" branches for
S::Empty/S::Expr/S::Throw/etc.let mut decls: Vec<G::Decl> = Vec::with_capacity(local.decls.len_u32() as usize); for i in 0..(local.decls.len_u32() as usize) { let binding = local.decls.at(i).binding; Self::find_identifiers(binding, &mut decls); } +if decls.is_empty() { + return false; +} + local.decls = G::DeclList::move_from_list(decls); trueVerification
New test at
test/regression/issue/31002.test.tsruns each crashrepro in a subprocess and asserts
exitCode === 0with no output,plus a regression guard that destructuring
vars with realidentifiers still hoist them as
undefined.Alive-path behavior is unchanged:
var []/var {}at top level orin truthy branches still reach JSC, which rejects them with the
expected "Expected an initializer in destructuring variable
declaration" SyntaxError.
Note on debug-build compile error
The current debug build on
mainfails insrc/jsc/bindings/wtf-bindings.cppwith
use of undeclared identifier 'assert'(unrelated; trackedupstream in #30988, fix already open in #30992). That is not in this
diff; rebasing after #30992 lands will unblock the debug lane.