From 77a9ed6ff8c87ec5aa994779011a2151f6acd609 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 18 May 2026 18:04:15 +0000 Subject: [PATCH 1/2] parser: drop dead-branch `var` with zero identifiers instead of emitting `var;` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- src/js_parser/scan/scan_side_effects.rs | 7 +++ test/regression/issue/31002.test.ts | 70 +++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 test/regression/issue/31002.test.ts diff --git a/src/js_parser/scan/scan_side_effects.rs b/src/js_parser/scan/scan_side_effects.rs index f7b61bbaf66..f78c9124c3b 100644 --- a/src/js_parser/scan/scan_side_effects.rs +++ b/src/js_parser/scan/scan_side_effects.rs @@ -652,6 +652,13 @@ impl SideEffects { Self::find_identifiers(binding, &mut decls); } + // Drop the statement entirely when the destructuring pattern binds no + // identifiers — e.g. `if (0) var []`. An empty `var;` is invalid syntax, + // and the printer asserts it never sees an empty decl list. + if decls.is_empty() { + return false; + } + local.decls = G::DeclList::move_from_list(decls); true } diff --git a/test/regression/issue/31002.test.ts b/test/regression/issue/31002.test.ts new file mode 100644 index 00000000000..1a08764a638 --- /dev/null +++ b/test/regression/issue/31002.test.ts @@ -0,0 +1,70 @@ +// https://github.com/oven-sh/bun/issues/31002 +// +// `if (0) var []` (a destructuring `var` with zero bindings inside a +// statically-dead branch) panicked the printer with +// `panic: internal error: entered unreachable code` at +// `src/js_printer/lib.rs:2308`. The printer asserts that a `var` decl list +// is never empty ("var;" is invalid syntax). The dead-code elimination +// pass in `scan_side_effects.rs::should_keep_stmt_in_dead_control_flow` +// hoists identifiers out of destructuring patterns to preserve `var` +// hoisting semantics; when the pattern binds no identifiers (`var []`, +// `var {}`) the list collapsed to empty but the statement was still +// kept, tripping the printer invariant in the next pass. +// +// The fix: when the hoisted identifiers vector is empty, drop the whole +// statement. There is nothing to hoist and no valid `var` output exists. +// +// Run in a subprocess so a reintroduction (panic in the child) doesn't +// tear down the in-process test runner. + +import { expect, test } from "bun:test"; +import { bunEnv, bunExe } from "harness"; + +test.concurrent.each([ + // Bare `var []` / `var {}` as the only statement in a dead `if` body. + // These were the exact forms in the bug report; without the fix, each + // one crashed the printer. + "if(0)var[]", + "if(0)var{}", + // Same empty destructuring inside a function body — the DCE path runs + // per-scope and must not crash regardless of enclosing scope. + "function f(){if(0)var[]}", +])("`%s` does not crash the printer (dead-branch destructuring var with no bindings)", async source => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", source], + env: bunEnv, + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", + timeout: 10_000, + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // Before the fix: stderr carried `panic: internal error: entered + // unreachable code` and the process exited with a crash signal. + // After the fix: the dead branch is dropped and the program exits 0 + // with no output. + expect(stdout).toBe(""); + expect(stderr).toBe(""); + expect(proc.signalCode).toBeNull(); + expect(exitCode).toBe(0); +}); + +test.concurrent("dead-branch destructuring with real identifiers still hoists them as `undefined`", async () => { + // Regression guard: the fix must not drop destructuring `var`s that do + // introduce bindings — those still need to be hoisted so that reads + // after the dead `if` see `undefined` (not ReferenceError). + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", "if(0) var [a, b] = []; console.log(typeof a, typeof b);"], + env: bunEnv, + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", + timeout: 10_000, + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout).toBe("undefined undefined\n"); + expect(stderr).toBe(""); + expect(exitCode).toBe(0); +}); From 1eeb7004f94f7d1275ce9e5bd812efb571bc80ad Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 18 May 2026 18:17:06 +0000 Subject: [PATCH 2/2] build: swap libc assert() to WTF ASSERT() in wtf-bindings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Debug/ASAN builds fail with `use of undeclared identifier "assert"` in `uv__tty_make_raw`. libc `` 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. --- src/jsc/bindings/wtf-bindings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jsc/bindings/wtf-bindings.cpp b/src/jsc/bindings/wtf-bindings.cpp index 0d4968f805f..8b09b5a8665 100644 --- a/src/jsc/bindings/wtf-bindings.cpp +++ b/src/jsc/bindings/wtf-bindings.cpp @@ -61,7 +61,7 @@ extern "C" int uv_tty_reset_mode(void) static void uv__tty_make_raw(struct termios* tio) { - assert(tio != NULL); + ASSERT(tio != NULL); #if defined __sun || defined __MVS__ /*