Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
0b48c8a6c0 Fix stack overflow with deeply nested if-else chains
Fixes #24548

When processing JavaScript files with deeply nested if-else chains
(2000+ levels), Bun would crash with a stack overflow during AST
traversal. This occurred because the `s_if` visitor in visitStmt.zig
used recursive calls to process else-if chains, creating a call stack
proportional to the nesting depth.

This was particularly problematic for code generated by compilers like
Gleam, which can generate long if-else-if chains for entity resolution
and similar tasks.

The fix converts the else-if chain processing from recursive to
iterative. When the else branch of an if statement is another if
statement (forming an else-if chain), we now process it iteratively
in a loop rather than through recursive calls. This bounds the stack
depth regardless of chain length.

Test plan:
- Added regression test with 2500-level deep if-else chain
- Verified the original reproduction case now works
- Both `bun run` and `bun build` handle deep nesting correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 03:52:03 +00:00
2 changed files with 154 additions and 11 deletions

View File

@@ -1000,6 +1000,9 @@ pub fn VisitStmt(
try stmts.append(stmt.*);
}
pub fn s_if(noalias p: *P, noalias stmts: *ListManaged(Stmt), noalias stmt: *Stmt, noalias data: *S.If) !void {
// For deeply nested if-else chains, we need to avoid stack overflow.
// We do this by handling the "else if" chain iteratively rather than recursively.
// Process the test expression and yes branch for the first if statement
data.test_ = p.visitExpr(data.test_);
if (p.options.features.minify_syntax) {
@@ -1017,20 +1020,87 @@ pub fn VisitStmt(
}
// The "else" clause is optional
// For else-if chains, process iteratively to avoid stack overflow
if (data.no) |no| {
if (effects.ok and effects.value) {
const old = p.is_control_flow_dead;
p.is_control_flow_dead = true;
defer p.is_control_flow_dead = old;
data.no = p.visitSingleStmt(no, .none);
} else {
data.no = p.visitSingleStmt(no, .none);
// Check if this is the start of an else-if chain
var current_no = no;
var parent_data: ?*S.If = data;
while (current_no.data == .s_if) {
var current_if_data: *S.If = current_no.data.s_if;
// Process this if statement in the chain
current_if_data.test_ = p.visitExpr(current_if_data.test_);
if (p.options.features.minify_syntax) {
current_if_data.test_ = SideEffects.simplifyBoolean(p, current_if_data.test_);
}
const chain_effects = SideEffects.toBoolean(p, current_if_data.test_.data);
if (chain_effects.ok and !chain_effects.value) {
const old = p.is_control_flow_dead;
p.is_control_flow_dead = true;
current_if_data.yes = p.visitSingleStmt(current_if_data.yes, StmtsKind.none);
p.is_control_flow_dead = old;
} else {
current_if_data.yes = p.visitSingleStmt(current_if_data.yes, StmtsKind.none);
}
// Move to the next else clause
if (current_if_data.no) |next_no| {
// Trim unnecessary "else" clauses
if (p.options.features.minify_syntax) {
if (@as(Stmt.Tag, next_no.data) == .s_empty) {
current_if_data.no = null;
break;
}
}
// Check if the next else is another if (continuing the chain)
if (next_no.data == .s_if) {
current_no = next_no;
parent_data = current_if_data;
continue; // Continue processing the chain
} else {
// Not an else-if, process the final else branch normally
if (chain_effects.ok and chain_effects.value) {
const old = p.is_control_flow_dead;
p.is_control_flow_dead = true;
defer p.is_control_flow_dead = old;
current_if_data.no = p.visitSingleStmt(next_no, .none);
} else {
current_if_data.no = p.visitSingleStmt(next_no, .none);
}
// Trim unnecessary "else" clauses
if (p.options.features.minify_syntax) {
if (current_if_data.no != null and @as(Stmt.Tag, current_if_data.no.?.data) == .s_empty) {
current_if_data.no = null;
}
}
break;
}
} else {
break; // No more else clauses
}
}
// Trim unnecessary "else" clauses
if (p.options.features.minify_syntax) {
if (data.no != null and @as(Stmt.Tag, data.no.?.data) == .s_empty) {
data.no = null;
// If we didn't enter the loop (no was not an if statement), process normally
if (parent_data == data and current_no.data != .s_if) {
if (effects.ok and effects.value) {
const old = p.is_control_flow_dead;
p.is_control_flow_dead = true;
defer p.is_control_flow_dead = old;
data.no = p.visitSingleStmt(current_no, .none);
} else {
data.no = p.visitSingleStmt(current_no, .none);
}
// Trim unnecessary "else" clauses
if (p.options.features.minify_syntax) {
if (data.no != null and @as(Stmt.Tag, data.no.?.data) == .s_empty) {
data.no = null;
}
}
}
}

View File

@@ -0,0 +1,73 @@
// https://github.com/oven-sh/bun/issues/24548
// Test that Bun can handle deeply nested if-else chains without stack overflow
import { expect, test } from "bun:test";
import { bunEnv, bunExe, normalizeBunSnapshot, tempDir } from "harness";
test("deeply nested if-else chains should not cause stack overflow", async () => {
// Generate a deeply nested if-else chain (similar to Gleam's entity resolver)
const depth = 2500; // More than the 2124 in the original issue
let code = "export function test(x) {\n";
for (let i = 0; i < depth; i++) {
if (i > 0) code += " else ";
code += `if (x === ${i}) {\n`;
code += ` return ${i};\n`;
code += " }";
}
code += " else {\n return -1;\n }\n}\n";
using dir = tempDir("issue-24548", {
"deep-if-else.js": code,
"index.js": `import { test } from "./deep-if-else.js";\nconsole.log(test(42));`,
});
// Test that bun can run the file
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(normalizeBunSnapshot(stdout)).toMatchInlineSnapshot(`"42"`);
expect(exitCode).toBe(0);
});
test("bun build should handle deeply nested if-else chains", async () => {
const depth = 2500;
let code = "export function test(x) {\n";
for (let i = 0; i < depth; i++) {
if (i > 0) code += " else ";
code += `if (x === ${i}) {\n`;
code += ` return ${i};\n`;
code += " }";
}
code += " else {\n return -1;\n }\n}\n";
using dir = tempDir("issue-24548-build", {
"deep-if-else.js": code,
});
// Test that bun build can bundle the file
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "deep-if-else.js", "--outfile=bundle.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
// Verify the bundle was created
const bundlePath = `${dir}/bundle.js`;
expect(await Bun.file(bundlePath).exists()).toBe(true);
});