Skip to content
Open
100 changes: 91 additions & 9 deletions src/jsc/ConsoleObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,32 @@ fn message_with_type_and_level_(
Ok(())
}

/// Steps the formatters give an iterable that has no element count of its own, such as a generator.
const UNSIZED_ITERABLE_BUDGET: u32 = 1000;

/// A bounded [`JSValue::for_each`] for formatters. True when elements were left. `Bun__ConsoleObject__forEachLimited` has the rules.
pub(crate) fn for_each_limited(
iterable: JSValue,
global: &JSGlobalObject,
budget: u32,
ctx: *mut c_void,
callback: jsc::ForEachCallback,
) -> JsResult<bool> {
unsafe extern "C" {
// safe: `ctx` is an opaque round-trip pointer that C++ only forwards to `callback`.
safe fn Bun__ConsoleObject__forEachLimited(
iterable: JSValue,
global: &JSGlobalObject,
budget: u32,
ctx: *mut c_void,
callback: jsc::ForEachCallback,
) -> bool;
}
jsc::host_fn::from_js_host_call_generic(global, || {
Bun__ConsoleObject__forEachLimited(iterable, global, budget, ctx, callback)
})
}

// ───────────────────────────────────────────────────────────────────────────
// TablePrinter
// ───────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -898,6 +924,7 @@ impl<'a> TablePrinter<'a> {
// ranges, so no property is re-read and no value is re-formatted.
let mut cell_text: Vec<u8> = Vec::new();
let mut rows: Vec<CollectedRow> = Vec::new();
let mut rows_truncated = false;
{
if self.is_iterable {
struct Ctx<'c, 'a> {
Expand Down Expand Up @@ -943,8 +970,10 @@ impl<'a> TablePrinter<'a> {
}
ctx.idx += 1;
}
tabular_data.for_each_with_context(
rows_truncated = for_each_limited(
tabular_data,
global_object,
UNSIZED_ITERABLE_BUDGET,
(&raw mut ctx).cast::<c_void>(),
callback::<ENABLE_ANSI_COLORS>,
)?;
Expand Down Expand Up @@ -1063,6 +1092,11 @@ impl<'a> TablePrinter<'a> {
let _ = writer.write_all("┘\n".as_bytes());
}

if rows_truncated {
let _ =
writer.write_all(pfmt!("<r><d>... more rows<r>\n", ENABLE_ANSI_COLORS).as_bytes());
}

Ok(())
}
}
Expand Down Expand Up @@ -2745,6 +2779,24 @@ pub mod formatter {
self.estimated_line_length += 1;
Ok(())
}

/// Ends a preview whose iterator had more to give than the walk allowed.
fn print_more_entries<const C: bool>(
&mut self,
writer: &mut dyn bun_io::Write,
wrote_entry: bool,
) {
if !self.single_line {
let _ = self.write_indent(writer);
} else if wrote_entry {
let _ = self.print_comma::<C>(writer);
let _ = writer.write_all(b" ");
}
let _ = writer.write_all(pfmt!("<r><d>... more items<r>", C).as_bytes());
if !self.single_line {
let _ = writer.write_all(b"\n");
}
}
}

// ───────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -4601,16 +4653,21 @@ pub mod formatter {
writer: writer_,
count: 0,
};
value.for_each(
let truncated = for_each_limited(
value,
global_this,
UNSIZED_ITERABLE_BUDGET,
(&raw mut iter).cast::<c_void>(),
MapIteratorCtx::<C, false, true>::for_each,
)?;
let count = iter.count;
if iter.formatter.failed {
return Ok(());
}
if count > 0 {
if truncated {
self.print_more_entries::<C>(writer_, count > 0);
}
if count > 0 || truncated {
let _ = writer_.write_all(b" ");
}
} else {
Expand All @@ -4619,14 +4676,19 @@ pub mod formatter {
writer: writer_,
count: 0,
};
value.for_each(
let truncated = for_each_limited(
value,
global_this,
UNSIZED_ITERABLE_BUDGET,
(&raw mut iter).cast::<c_void>(),
MapIteratorCtx::<C, false, false>::for_each,
)?;
if iter.formatter.failed {
return Ok(());
}
if truncated {
self.print_more_entries::<C>(writer_, true);
}
}
}
if !self.single_line {
Expand Down Expand Up @@ -4660,15 +4722,20 @@ pub mod formatter {
writer: writer_,
count: 0,
};
value.for_each(
let truncated = for_each_limited(
value,
global_this,
UNSIZED_ITERABLE_BUDGET,
(&raw mut iter).cast::<c_void>(),
MapIteratorCtx::<C, true, true>::for_each,
)?;
let count = iter.count;
if iter.formatter.failed {
return Ok(());
}
if truncated {
self.print_more_entries::<C>(writer_, count > 0);
}
// Only the MapIterator case writes a trailing space.
if count > 0 && label == "MapIterator" {
let _ = writer_.write_all(b" ");
Expand All @@ -4679,8 +4746,10 @@ pub mod formatter {
writer: writer_,
count: 0,
};
value.for_each(
let truncated = for_each_limited(
value,
global_this,
UNSIZED_ITERABLE_BUDGET,
(&raw mut iter).cast::<c_void>(),
MapIteratorCtx::<C, true, false>::for_each,
)?;
Expand All @@ -4691,6 +4760,9 @@ pub mod formatter {
if count > 0 {
let _ = writer_.write_all(b"\n");
}
if truncated {
self.print_more_entries::<C>(writer_, count > 0);
}
}
}
if !self.single_line {
Expand Down Expand Up @@ -4743,16 +4815,21 @@ pub mod formatter {
writer: writer_,
is_first: true,
};
value.for_each(
let truncated = for_each_limited(
value,
global_this,
UNSIZED_ITERABLE_BUDGET,
(&raw mut iter).cast::<c_void>(),
SetIteratorCtx::<C, true>::for_each,
)?;
let is_first = iter.is_first;
if iter.formatter.failed {
return Ok(());
}
if !is_first {
if truncated {
self.print_more_entries::<C>(writer_, !is_first);
}
if !is_first || truncated {
let _ = writer_.write_all(b" ");
}
} else {
Expand All @@ -4761,14 +4838,19 @@ pub mod formatter {
writer: writer_,
is_first: true,
};
value.for_each(
let truncated = for_each_limited(
value,
global_this,
UNSIZED_ITERABLE_BUDGET,
(&raw mut iter).cast::<c_void>(),
SetIteratorCtx::<C, false>::for_each,
)?;
if iter.formatter.failed {
return Ok(());
}
if truncated {
self.print_more_entries::<C>(writer_, true);
}
}
}
if !self.single_line {
Expand Down
13 changes: 1 addition & 12 deletions src/jsc/JSValue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2210,7 +2210,7 @@ struct SerializedScriptValueExternal {
handle: *mut c_void,
}

/// Callback signature for [`JSValue::for_each`] / [`JSValue::for_each_with_context`].
/// Callback signature for [`JSValue::for_each`].
pub type ForEachCallback =
extern "C" fn(vm: *mut crate::VM, global: &JSGlobalObject, ctx: *mut c_void, next: JSValue);

Expand Down Expand Up @@ -2443,17 +2443,6 @@ impl JSValue {
JSC__JSValue__forEach(self, global, ctx, callback)
})
}
/// `JSValue.forEachWithContext` — typed-ctx wrapper (callers pass
/// `*mut c_void` directly).
#[inline]
pub(crate) fn for_each_with_context(
self,
global: &JSGlobalObject,
ctx: *mut c_void,
callback: ForEachCallback,
) -> JsResult<()> {
self.for_each(global, ctx, callback)
}
/// `JSValue.forEachProperty` — enumerate own props,
/// invoking `callback` per (key, value, is_symbol, is_private_symbol).
///
Expand Down
24 changes: 19 additions & 5 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5439,11 +5439,25 @@ impl VirtualMachine {
allow_side_effects,
printed_member: false,
};
if errors
.for_each(global_ref, (&raw mut ctx).cast(), agg_iter)
.is_err()
{
global_ref.clear_exception();
// `errors` is user-assigned, so its iterator can be endless.
const UNSIZED_ERRORS_BUDGET: u32 = 100;
match crate::console_object::for_each_limited(
errors,
global_ref,
UNSIZED_ERRORS_BUDGET,
(&raw mut ctx).cast(),
agg_iter,
) {
Ok(true) => {
let marker = if allow_ansi_color {
bun_core::pretty_fmt!("<r><d>... more errors<r>\n", true)
} else {
bun_core::pretty_fmt!("<r><d>... more errors<r>\n", false)
};
let _ = writer.write_all(marker.as_bytes());
}
Ok(false) => {}
Err(_) => global_ref.clear_exception(),
}
if ctx.printed_member {
return;
Expand Down
90 changes: 90 additions & 0 deletions src/jsc/bindings/ConsoleObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include <JavaScriptCore/ConsoleClient.h>
#include <JavaScriptCore/ConsoleMessage.h>
#include <JavaScriptCore/IteratorOperations.h>
#include <JavaScriptCore/JSString.h>
#include <JavaScriptCore/ScriptArguments.h>
#include <wtf/text/WTFString.h>
Expand Down Expand Up @@ -129,3 +130,92 @@ void ConsoleObject::profileEnd(JSC::JSGlobalObject* globalObject, const String&
}

}

// `JSC::forEachInIterable` with the iterator protocol bounded: by the collection's element count when it has one, else by `budget`. True when elements were left.
extern "C" bool Bun__ConsoleObject__forEachLimited(JSC::EncodedJSValue encodedIterable, JSC::JSGlobalObject* globalObject, uint32_t budget, void* ctx, void (*callback)(JSC::VM*, JSC::JSGlobalObject*, void* ctx, JSC::EncodedJSValue))
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSC::JSValue iterable = JSC::JSValue::decode(encodedIterable);
JSC::JSCell* sentinel = vm.orderedHashTableSentinel();

auto visit = [&](JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue value) {
callback(&vm, globalObject, ctx, JSC::JSValue::encode(value));
};

// The walks `forEachInIterable` takes when user code cannot observe the iteration. The collection itself bounds them.
if (JSC::getIterationMode(iterable) == JSC::IterationMode::FastArray) {
JSC::forEachInFastArray(globalObject, iterable, uncheckedDowncast<JSC::JSArray>(iterable), visit);
RETURN_IF_EXCEPTION(scope, false);
return false;
}
auto* map = dynamicDowncast<JSC::JSMap>(iterable);
if (map && map->isIteratorProtocolFastAndNonObservable()) {
JSC::JSCell* storage = map->storageOrSentinel(vm);
if (storage != sentinel) {
JSC::forEachInMapStorage(vm, globalObject, storage, 0, JSC::IterationKind::Entries, visit);
RETURN_IF_EXCEPTION(scope, false);
}
return false;
}
auto* set = dynamicDowncast<JSC::JSSet>(iterable);
if (set && set->isIteratorProtocolFastAndNonObservable()) {
JSC::JSCell* storage = set->storageOrSentinel(vm);
if (storage != sentinel) {
JSC::forEachInSetStorage(vm, globalObject, storage, 0, visit);
RETURN_IF_EXCEPTION(scope, false);
}
return false;
}

uint64_t bound = budget;
if (map || set) {
// Node's rule for a replaced iterator: it gets as many steps as the collection reports entries.
JSC::JSObject* collection = map ? static_cast<JSC::JSObject*>(map) : set;
JSC::JSValue sizeValue = collection->get(globalObject, vm.propertyNames->size);
Comment thread
robobun marked this conversation as resolved.
Resolved
RETURN_IF_EXCEPTION(scope, false);
double size = sizeValue.toNumber(globalObject);
RETURN_IF_EXCEPTION(scope, false);
// `size` is a user-controlled claim once it exceeds what the storage holds, so past that only the budget applies.
uint64_t trusted = std::max<uint64_t>(map ? map->size() : set->size(), budget);
if (size >= 0)
bound = size < static_cast<double>(trusted) ? static_cast<uint64_t>(size) : trusted;
Comment thread
robobun marked this conversation as resolved.
Resolved
} else if (auto* array = dynamicDowncast<JSC::JSArray>(iterable))
bound = array->length();
else if (auto* view = dynamicDowncast<JSC::JSArrayBufferView>(iterable))
bound = view->length();
else if (auto* mapIterator = dynamicDowncast<JSC::JSMapIterator>(iterable)) {
if (auto* iterated = dynamicDowncast<JSC::JSMap>(mapIterator->internalField(JSC::JSMapIterator::Field::IteratedObject).get()))
bound = iterated->size();
} else if (auto* setIterator = dynamicDowncast<JSC::JSSetIterator>(iterable)) {
if (auto* iterated = dynamicDowncast<JSC::JSSet>(setIterator->internalField(JSC::JSSetIterator::Field::IteratedObject).get()))
bound = iterated->size();
}

JSC::IterationRecord iterationRecord = JSC::iteratorForIterable(globalObject, iterable);
RETURN_IF_EXCEPTION(scope, false);

bool truncated = false;
for (uint64_t visited = 0;; visited++) {
JSC::JSValue next = JSC::iteratorStep(globalObject, iterationRecord);
RETURN_IF_EXCEPTION(scope, false);
if (next.isFalse())
return false;

if (visited >= bound) {
truncated = true;
break;
}

JSC::JSValue nextValue = JSC::iteratorValue(globalObject, next);
RETURN_IF_EXCEPTION(scope, false);

callback(&vm, globalObject, ctx, JSC::JSValue::encode(nextValue));
if (scope.exception()) [[unlikely]]
break;
}

scope.release();
JSC::iteratorClose(globalObject, iterationRecord.iterator);
return truncated;
}
Loading
Loading