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/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__ /* 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); +});