test runner: dispatch pretty_format::print_as on a runtime Tag
Problem
- The debug (ASAN) build fails the repo's own
test/js/bun/test/pretty-format-overflow.test.tson Linux with the default 8 MB stack. The childbun testexits 139 (stack overflow) instead of 1 while it formats the 500-level object for thetoEqualdiff. The test is red on main underbun bd testand green here. Formatter::print_asinsrc/runtime/test_runner/pretty_format.rsis generic overconst FORMAT: Tagand holds all 24 arms in one closure. rustc emits the whole body per instantiation, so everyprint_asframe carries the locals of all 24 arms: 10 to 13 KB under ASAN, per nesting level. The body also compiles 48 times: 480K lines of LLVM IR, 15% ofbun_runtime.
Fix
formatis a runtime parameter. Each arm is its own#[inline(never)]method, the shapeconsole_object::Formatter::print_asinsrc/jsc/ConsoleObject.rsalready has.- No new dispatch:
Formatter::formatalready matched on the tag at runtime. Arm bodies moved verbatim (hide whitespace in the diff view). - Per nesting level the debug build spends about 6 KB of stack instead of about 16 KB. The fixture passes at 1200 levels and overflows at 1400. Main overflows at 500.
bun_runtimeIR: 3,149,105 to 2,687,005 lines. - Verified:
pretty-format-overflow.test.tsnow asserts the innermost property is printed and the child exits 1 with no signal. It fails on main underbun bd(exit 139) and passes here. Also the snapshot, printing, expect and mock suites undertest/js/bun/test.
Background
pretty_format.rsis the serializer behindtoEqualdiffs andtoMatchSnapshot.print_asrenders one value and recurses throughformatfor nested values, so its frame size sets the depth the formatter survives.- A
constgeneric parameter does not prune amatchbefore codegen. LLVM deletes the dead arms later. At opt-level 0 their locals stay in the frame. - #34884 adds a stack guard to this formatter. The guard bounds the recursion. This change decides where the bound lands. They are independent.
Notes
Depth ceiling, measured with the fixture from pretty-format-overflow.test.ts at DEPTH levels, ulimit -s 8192, debug ASAN build (bun bd): main exits 1 at 400 and segfaults at 500. This branch exits 1 at 1200 and segfaults at 1400. The CI ASAN lane is a release build (ci-build profile), so CI does not see the debug frames. Local bun bd does.
Frame sizes (sub rsp in the prologue, debug ASAN build). Before: every print_as::<.., Tag::X, ..>::{closure#0} was 0x2860 to 0x32c0 bytes (10,336 to 12,992), Tag::Object 0x2a80 (10,880). After: print_as 0x180 (384), print_object 0x280 (640), print_array 0x320 (800), print_typed_array 0xb80 (2944, the largest). The rest of the per-level cost is in PropertyIterator::for_each, the C++ forEachPropertyOrdered frame and format. A follow-up can port the for_each_prelude plus #[inline(never)] property-key split that ConsoleObject.rs uses into PropertyIterator::for_each for another 1 to 2 KB per level.
IR measured with cargo llvm-lines on every crate (release profile, -Cno-prepopulate-passes, LTO off). Whole workspace before: 8,305,084 lines in 151,486 function copies. print_as and its closures: 480,240 lines in 576 copies. After: 20,620 lines in 70 copies.
Compile time moves little, because LLVM deletes the dead arms in its first passes. bun_runtime alone, release profile with LTO off, codegen-units=1: 136.0 s before vs 128.4 s after in one pair of runs, equal within noise in another pair. -Ztime-passes: codegen_to_LLVM_IR 9.7 s to 8.6 s, LLVM_passes 113.8 s to 107.2 s. Peak rustc RSS 3763 MB to 3634 MB. Dev profile with ASAN: 17.3 s vs 17.2 s. The release libbun_runtime.a is 11 KB smaller.
The transformation was mechanical: arm bodies dedented into methods, self.print_as::<W, { Tag::X }, C>( rewritten to self.print_as::<W, C>(Tag::X, , the two arms that ended in return converted to tail expressions, &mut writer passed as writer. Only value and js_type are passed to the arms that read them. write_with_formatting (the %s/%d/%o path) calls print_as with its runtime token instead of matching over four instantiations. git diff -w shows about 260 added and 150 removed lines.
Because the arm bodies move, open PRs that edit them need a rebase: #40235 (unsafe removal in the test runner, touches the Private, Map, Set, Object and TypedArray arms), #34884, and the snapshot formatting PRs (#38935, #38939, #36674, #36645, #35712, #37043, #31292, #40319, #29709). The bodies are unchanged, so the rebase is a re-indent. If #40235 should land first, this transform can be re-applied on top of it.
[review] gate passed · iteration 2 · 2 files touched
fails on main (without fix)
ASAN without fix: 1 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/test/pretty-format-overflow.test.ts
bun test v1.4.1 (adc354d99)
test/js/bun/test/pretty-format-overflow.test.ts:
45 | // The diff goes to stderr. Drain stdout too so the child never blocks on it.
46 | const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited, proc.stdout.text()]);
47 |
48 | // The diff header proves the formatter ran; the innermost property proves
49 | // it walked all 500 levels instead of dying part-way down.
50 | expect(stderr).toContain("expect(received).toEqual(expected)");
^
error: expect(received).toContain(expected)
Expected to contain: "expect(received).toEqual(expected)"
Received: "\nnested.test.ts:\n"
at <anonymous> (/workspace/bun/test/js/bun/test/pretty-format-overflow.test.ts:50:20)
(fail) pretty_format should handle deeply nested objects without crashing > deeply nested object with many properties [961.68ms]
0 pass
1 fail
1 expect() calls
Ran 1 test across 1 file. [3.00s]
error: script "bd" exited with code 1
__F:1:S:0
release without fix: all passed
bun test v1.4.1-canary.1 (d99fc726f)
test/js/bun/test/pretty-format-overflow.test.ts:
(pass) pretty_format should handle deeply nested objects without crashing > deeply nested object with many properties [12.81ms]
1 pass
0 fail
4 expect() calls
Ran 1 test across 1 file. [91.00ms]
__F:0:S:0
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/test/pretty-format-overflow.test.ts
bun test v1.4.1 (adc354d99)
test/js/bun/test/pretty-format-overflow.test.ts:
(pass) pretty_format should handle deeply nested objects without crashing > deeply nested object with many properties [893.74ms]
1 pass
0 fail
4 expect() calls
Ran 1 test across 1 file. [2.85s]
__F:0:S:0
release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 611ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/5] gen generated_host_exports.rs
generated_host_exports.rs: 116 exports (host=5, lazy=10, generic=101, rust=0); 243 extern-C blocks audited
[1/5] cargo bun_runtime → libbun_runtime.a (--target x86_64-unknown-linux-gnu)
nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)
[1m[92m Compiling[0m bun_js_parser v0.0.0 (/workspace/bun/src/js_parser)
[1m[92m Compiling[0m bun_js_printer v0.0.0 (/workspace/bun/src/js_printer)
[1m[92m Compiling[0m bun_resolver v0.0.0 (/workspace/bun/src/resolver)
[1m[92m Compiling[0m bun_ini v0.0.0 (/workspace/bun/src/ini)
[1m[92m Compiling[0m bun_bundler v0.0.0 (/workspace/bun/src/bundler)
[1m[92m Compiling[0m bun_router v0.0.0 (/workspace/bun/src/router)
[1m[92m Compiling[0m bun_standalone_graph v0.0.0 (/workspace/bun/src/standalone_graph)
[1m[92m Compiling[0m bun_transpiler v0.0.0 (/workspace/bun/src/transpiler)
[1m[92m Compiling[0m bun_bunfig v0.0.0 (/workspace/bun/src/bunfig)
[1
... (truncated)
diff hotspot
src/runtime/test_runner/pretty_format.rs | 2814 ++++++++++++-----------
test/js/bun/test/pretty-format-overflow.test.ts | 29 +-
2 files changed, 1476 insertions(+), 1367 deletions(-)
gate history · 2 passed · 2 rejected · iteration 2
evidence per changed file
file reads edits tests
src/runtime/test_runner/pretty_format.rs 11 2 0
test/js/bun/test/pretty-format-overflow.test.ts 3 3 0