Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
b71539e
jsc: seat StackCheck on the uncaught-exception printer's Formatter
robobun Jul 21, 2026
9710a9d
Move StackCheck::init() into Formatter::new() instead of per-call-site
robobun Jul 21, 2026
4b3439a
test: assert uncaught printer completes (Bun version trailer), drop h…
robobun Jul 21, 2026
7b3da13
Extend stack guard to pretty_format::Formatter; ignore stderr in test
robobun Jul 21, 2026
a5e7897
print_as_prelude: check stack before the !can_circ early-return
robobun Jul 21, 2026
0f832d9
test: include stmt in describe.each title so variants are distinguish…
robobun Jul 21, 2026
be64e5b
Drop now-unused StackCheck::update and its last caller in format_value
robobun Aug 17, 2026
143d380
Fold in the pretty_format test matrix from #34885 and make snapshot s…
robobun Aug 17, 2026
8e893e5
Trim field and branch comments that restate the code
robobun Aug 17, 2026
62ce631
Drop remaining field doc comment; rationale lives in the two construc…
robobun Aug 17, 2026
e7f6acc
pretty_format: throw the stack overflow after the walk unwinds, not a…
robobun Aug 18, 2026
ee2036e
Trim comments on the deferred stack-overflow throw
robobun Aug 18, 2026
6f71143
Test the throwing branch through a React element chain and pin a dept…
robobun Aug 18, 2026
ee06954
pretty_format: pass the writer through the asymmetric matcher bridge …
robobun Aug 18, 2026
5a24ce6
Trim comments on the matcher bridge
robobun Aug 18, 2026
6773705
Merge remote-tracking branch 'origin/main' into push/34884
robobun Sep 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions src/bun_core/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2172,16 +2172,6 @@ unsafe extern "C" {
#[cfg(windows)]
safe fn clock_gettime_monotonic(sec: &mut i64, nsec: &mut i64);
}
impl Default for StackCheck {
/// `cached_stack_end` defaults to `0`, so
/// `is_safe_to_recurse()` always reports true until `init`/`update`.
#[inline]
fn default() -> Self {
Self {
cached_stack_end: 0,
}
}
}
impl StackCheck {
#[inline]
pub fn configure_thread() {
Expand All @@ -2193,10 +2183,6 @@ impl StackCheck {
cached_stack_end: Bun__StackCheck__getMaxStack() as usize,
}
}
#[inline]
pub fn update(&mut self) {
self.cached_stack_end = Bun__StackCheck__getMaxStack() as usize;
}
/// Stack reserved for the work a frame does before the next check. One
/// `WTF::StringBuilder` growth reallocates through libpas, a ~35 frame
/// path that measures ~160 KB under a sanitizer and a few KB without one.
Expand Down
17 changes: 4 additions & 13 deletions src/jsc/ConsoleObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,6 @@ impl<'a> TablePrinter<'a> {
f.single_line = true;
f.max_depth = 5;
f.can_throw_stack_overflow = true;
f.stack_check = StackCheck::init();
f
},
values_col_width: None,
Expand Down Expand Up @@ -1338,7 +1337,6 @@ pub fn format2(
fmt.max_depth = options.max_depth;
fmt.single_line = options.single_line;
fmt.indent = u32::from(options.default_indent);
fmt.stack_check = StackCheck::init();
fmt.can_throw_stack_overflow = true;
fmt.error_display_level = options.error_display_level;
let tag = formatter::Tag::get(vals[0], global)?;
Expand Down Expand Up @@ -1401,7 +1399,6 @@ pub fn format2(
fmt.max_depth = options.max_depth;
fmt.single_line = options.single_line;
fmt.indent = u32::from(options.default_indent);
fmt.stack_check = StackCheck::init();
fmt.can_throw_stack_overflow = true;
fmt.error_display_level = options.error_display_level;
let mut tag: formatter::TagResult;
Expand Down Expand Up @@ -1631,10 +1628,7 @@ pub mod formatter {
ordered_properties: false,
custom_formatted_object: CustomFormattedObject::default(),
disable_inspect_custom: false,
// `StackCheck::default()` has `cached_stack_end = 0` ⇒ the
// check always passes; callers that want a real bound
// overwrite with `StackCheck::init()` explicitly.
stack_check: StackCheck::default(),
stack_check: StackCheck::init(),
Comment thread
robobun marked this conversation as resolved.
Resolved
Comment thread
robobun marked this conversation as resolved.
Resolved
can_throw_stack_overflow: false,
error_display_level: ErrorDisplayLevel::Full,
format_buffer_as_text: false,
Expand Down Expand Up @@ -3219,17 +3213,16 @@ pub mod formatter {
if self.global_this.has_exception() {
return Err(jsc::JsError::Thrown);
}
if !can_circ {
return Ok(true);
}

if !self.stack_check.is_safe_to_recurse() {
self.failed = true;
if self.can_throw_stack_overflow {
return Err(self.global_this.throw_stack_overflow());
}
return Ok(false);
}
if !can_circ {
return Ok(true);
}

if self.map_node.is_none() {
let mut node = core::ptr::NonNull::new(visited::Pool::get_node())
Expand Down Expand Up @@ -5624,7 +5617,6 @@ pub mod formatter {
value: JSValue,
writer: &mut dyn bun_io::Write,
) -> JsResult<()> {
self.stack_check.update();
let one = [value];
self.remaining_values = bun_ptr::RawSlice::new(&one);
let global = self.global_this;
Expand Down Expand Up @@ -5848,7 +5840,6 @@ pub(crate) extern "C" fn Bun__ConsoleObject__timeLog(
fmt.max_depth = bun_options_types::context::try_get()
.and_then(|ctx| ctx.runtime_options.console_depth)
.unwrap_or(DEFAULT_CONSOLE_LOG_DEPTH);
fmt.stack_check = StackCheck::init();
fmt.can_throw_stack_overflow = true;
let console = vm_console(global);
// SAFETY: see [`vm_console`] — points at the live boxed `ConsoleObject` for
Expand Down
1 change: 0 additions & 1 deletion src/jsc/JSValue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2655,7 +2655,6 @@ impl JSValue {
formatter: &'a mut crate::console_object::Formatter<'b>,
) -> crate::console_object::formatter::ZigFormatter<'a, 'b> {
formatter.remaining_values = bun_ptr::RawSlice::EMPTY;
formatter.stack_check.update();
crate::console_object::formatter::ZigFormatter::new(formatter, self)
}

Expand Down
3 changes: 1 addition & 2 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6120,8 +6120,7 @@ impl VirtualMachine {
// re-check here with an extra `MAX_PATH_BYTES * 3` of slack on Windows
// to cover the transpiler's nested path buffers — same parity-level
// protection the Object path gets from C++ `forEachProperty`'s
// `vm.isSafeToRecurse()`. The formatter's `stack_check` was seated by
// the caller (`format2` / `Bun.inspect`).
// `vm.isSafeToRecurse()`.
let extra_headroom: usize = if cfg!(windows) {
// 3× PathBuffer ≈ 288 KB — empirically enough for the
// `remap_zig_exception` → `transpile_source_code` chain on the
Expand Down
1 change: 1 addition & 0 deletions src/runtime/test_runner/diff_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ impl<'a> DiffFormatter<'a> {
add_newline: false,
flush: false,
quote_strings: true,
can_throw_stack_overflow: false,
};
let mut received_buf: Vec<u8> = Vec::new();
JestPrettyFormat::format(
Expand Down
1 change: 1 addition & 0 deletions src/runtime/test_runner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ pub mod expect {
add_newline: false,
flush: false,
quote_strings: true,
can_throw_stack_overflow: true,
};
JestPrettyFormat::format(
MessageLevel::Debug,
Expand Down
86 changes: 52 additions & 34 deletions src/runtime/test_runner/pretty_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::test_runner::expect::JSValueTestExt;
use core::ffi::c_void;

use bun_collections::HashMap;
use bun_core::fmt as bun_fmt;
use bun_core::{fmt as bun_fmt, StackCheck};
use bun_jsc::{
self as jsc, ComptimeStringMapExt as _, JSGlobalObject, JSObject,
JSPropertyIterator, JSType, JSValue, JsError, JsResult, VM,
Expand Down Expand Up @@ -101,6 +101,7 @@ pub struct FormatOptions {
pub(crate) add_newline: bool,
pub flush: bool,
pub(crate) quote_strings: bool,
pub(crate) can_throw_stack_overflow: bool,
}

impl JestPrettyFormat {
Expand Down Expand Up @@ -184,7 +185,7 @@ impl JestPrettyFormat {
}

let _ = writer.flush();
return Ok(());
return fmt.finish(options);
}

// The flush must fire on every exit including `?` propagation from
Expand Down Expand Up @@ -255,7 +256,8 @@ impl JestPrettyFormat {
}

// map_node release handled by `impl Drop for Formatter`.
result
result?;
fmt.finish(options)
}
}

Expand Down Expand Up @@ -318,6 +320,8 @@ pub struct Formatter<'a> {
pub(crate) failed: bool,
pub(crate) estimated_line_length: usize,
pub(crate) always_newline_scope: bool,
stack_check: StackCheck,
hit_stack_limit: bool,
}

impl<'a> Formatter<'a> {
Expand All @@ -332,7 +336,17 @@ impl<'a> Formatter<'a> {
failed: false,
estimated_line_length: 0,
always_newline_scope: false,
stack_check: StackCheck::init(),
hit_stack_limit: false,
}
}

/// Deferred to here because the C++ property walks drop exceptions thrown from their callbacks.
fn finish(&self, options: FormatOptions) -> JsResult<()> {
if self.hit_stack_limit && options.can_throw_stack_overflow {
return Err(self.global_this.throw_stack_overflow());
}
Ok(())
}

pub(crate) fn good_time_for_a_new_line(&mut self) -> bool {
Expand Down Expand Up @@ -1041,6 +1055,11 @@ impl<'a> Formatter<'a> {
if self.failed {
return Ok(());
}
if !self.stack_check.is_safe_to_recurse() {
self.failed = true;
self.hit_stack_limit = true;
return Ok(());
}
// reshaped for borrowck — `WrappedWriter` borrows both writer_
// and &mut self.estimated_line_length; we use a local wrapper and sync
// `failed` at scope exit. estimated_line_length is unused by WrappedWriter
Expand Down Expand Up @@ -2583,8 +2602,7 @@ impl<'a> Formatter<'a> {
/// for nested values. The trait is the layering seam — it lives in `bun_jsc`,
/// the webcore types call through it generically, and this file supplies the
/// test-runner-specific dispatch. Mirrors the `bun_jsc::console_object`
/// `Formatter` impl (`src/jsc/lib.rs`) one-for-one, mapping the
/// `bun_jsc::FormatTag` enum onto this file's smaller `Tag` set.
/// `Formatter` impl (`src/jsc/lib.rs`) one-for-one.
impl bun_jsc::ConsoleFormatter for Formatter<'_> {
#[inline]
fn global_this(&self) -> &JSGlobalObject { self.global_this }
Expand Down Expand Up @@ -2613,12 +2631,22 @@ impl bun_jsc::ConsoleFormatter for Formatter<'_> {
value: JSValue,
cell: JSType,
) -> JsResult<()> {
let mut sink = bun_io::FmtAdapter::new(writer);
let global = self.global_this;
self.format::<_, ENABLE_ANSI_COLORS>(
TagResult { tag: tag.into(), cell },
&mut sink,
value,
global,
)
}
}

/// Variants this file has no arm for collapse onto `Object`.
impl From<bun_jsc::FormatTag> for Tag {
fn from(tag: bun_jsc::FormatTag) -> Self {
use bun_jsc::FormatTag as Ft;
// Map the wider `console_object::Tag` onto this file's `Tag`. Only the
// variants the `write_format` hooks actually emit are reachable
// (Boolean / Double / Object / Private / String); the rest collapse
// onto `Object` so any future caller still renders something useful.
let local = match tag {
match tag {
Ft::StringPossiblyFormatted => Tag::StringPossiblyFormatted,
Ft::String => Tag::String,
Ft::Undefined => Tag::Undefined,
Expand Down Expand Up @@ -2652,15 +2680,7 @@ impl bun_jsc::ConsoleFormatter for Formatter<'_> {
| Ft::CustomGetterSetter
| Ft::Proxy
| Ft::RevokedProxy => Tag::Object,
};
let mut sink = bun_io::FmtAdapter::new(writer);
let global = self.global_this;
self.format::<_, ENABLE_ANSI_COLORS>(
TagResult { tag: local, cell },
&mut sink,
value,
global,
)
}
}
}

Expand All @@ -2676,10 +2696,11 @@ pub trait AsymmetricMatcherFormatter {
fn amf_quote_strings(&mut self) -> &mut bool;
/// `printAs(tag, …)` routed through the formatter's own runtime
/// dispatcher. Only `Object` / `String` / `Array` are reached.
fn amf_print_as<const C: bool>(
/// Generic rather than `dyn` so nested matchers do not stack one writer adapter per level.
fn amf_print_as<W: bun_io::Write, const C: bool>(
&mut self,
tag: bun_jsc::FormatTag,
w: &mut dyn bun_io::Write,
w: &mut W,
v: JSValue,
cell: JSType,
) -> JsResult<()>;
Expand All @@ -2692,18 +2713,15 @@ impl AsymmetricMatcherFormatter for Formatter<'_> {
fn amf_global_this(&self) -> &JSGlobalObject { self.global_this }
#[inline]
fn amf_quote_strings(&mut self) -> &mut bool { &mut self.quote_strings }
fn amf_print_as<const C: bool>(
fn amf_print_as<W: bun_io::Write, const C: bool>(
&mut self,
tag: bun_jsc::FormatTag,
w: &mut dyn bun_io::Write,
w: &mut W,
v: JSValue,
cell: JSType,
) -> JsResult<()> {
// Reuse the `ConsoleFormatter` bridge above (FormatTag → local `Tag`
// mapping + `format` dispatch). `AsFmt` adapts `dyn bun_io::Write` →
// `core::fmt::Write` for the trait method's signature.
let mut bridge = AsFmt::new(w);
<Self as bun_jsc::ConsoleFormatter>::print_as::<_, C>(self, tag, &mut bridge, v, cell)
let global = self.global_this;
self.format::<W, C>(TagResult { tag: tag.into(), cell }, w, v, global)
}
}

Expand All @@ -2714,10 +2732,10 @@ impl AsymmetricMatcherFormatter for bun_jsc::console_object::Formatter<'_> {
fn amf_global_this(&self) -> &JSGlobalObject { self.global_this }
#[inline]
fn amf_quote_strings(&mut self) -> &mut bool { &mut self.quote_strings }
fn amf_print_as<const C: bool>(
fn amf_print_as<W: bun_io::Write, const C: bool>(
&mut self,
tag: bun_jsc::FormatTag,
w: &mut dyn bun_io::Write,
w: &mut W,
v: JSValue,
cell: JSType,
) -> JsResult<()> {
Expand Down Expand Up @@ -2849,7 +2867,7 @@ impl JestPrettyFormat {
this.amf_add_for_new_line(b"ObjectContaining ".len());
writer.write_all(b"ObjectContaining ");
}
this.amf_print_as::<ENABLE_ANSI_COLORS>(
this.amf_print_as::<_, ENABLE_ANSI_COLORS>(
bun_jsc::FormatTag::Object, &mut *writer.ctx, object_value, JSType::Object,
)?;
} else if let Some(matcher) = value.as_class_ref::<expect::ExpectStringContaining>() {
Expand All @@ -2868,7 +2886,7 @@ impl JestPrettyFormat {
this.amf_add_for_new_line(b"StringContaining ".len());
writer.write_all(b"StringContaining ");
}
this.amf_print_as::<ENABLE_ANSI_COLORS>(
this.amf_print_as::<_, ENABLE_ANSI_COLORS>(
bun_jsc::FormatTag::String, &mut *writer.ctx, substring_value, JSType::String,
)?;
} else if let Some(matcher) = value.as_class_ref::<expect::ExpectStringMatching>() {
Expand All @@ -2891,7 +2909,7 @@ impl JestPrettyFormat {
if test_value.is_reg_exp() {
*this.amf_quote_strings() = false;
}
this.amf_print_as::<ENABLE_ANSI_COLORS>(
this.amf_print_as::<_, ENABLE_ANSI_COLORS>(
bun_jsc::FormatTag::String, &mut *writer.ctx, test_value, JSType::String,
)?;
*this.amf_quote_strings() = original_quote_strings;
Expand Down Expand Up @@ -2923,7 +2941,7 @@ impl JestPrettyFormat {
this.amf_add_for_new_line(matcher_name.length() + 1);
writer.print(format_args!("{}", matcher_name));
writer.write_all(b" ");
this.amf_print_as::<ENABLE_ANSI_COLORS>(
this.amf_print_as::<_, ENABLE_ANSI_COLORS>(
bun_jsc::FormatTag::Array, &mut *writer.ctx, args_value, JSType::Array,
)?;
}
Expand Down
Loading
Loading