Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
93 changes: 92 additions & 1 deletion src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,24 @@ impl VirtualMachine {
|| !el.next_immediate_tasks.is_empty()
}

/// Whether anything could still wake the loop and settle a pending module
/// promise. Unlike `is_event_loop_alive`, ignores `unhandled_error_counter`
/// so ref'd work (e.g. a timer that will resolve the await) isn't miscounted.
pub fn has_pending_loop_work(&self) -> bool {
let el = self.event_loop_shared();
let active = self
.platform_loop_opt()
.map(|h| h.is_active())
.unwrap_or(false);
active
|| self.active_tasks > 0
|| el.tasks.readable_length() > 0
|| el.has_pending_refs()
|| !el.concurrent_tasks.is_empty()
|| !el.immediate_tasks.is_empty()
|| !el.next_immediate_tasks.is_empty()
}

pub fn wakeup(&mut self) {
self.event_loop_mut().wakeup();
}
Expand Down Expand Up @@ -2232,6 +2250,77 @@ impl VirtualMachine {
self.event_loop_mut().wait_for_promise(promise);
}

/// Like [`wait_for_promise`](Self::wait_for_promise) but returns (promise
/// possibly still `Pending`) once nothing could settle it
/// (`!has_pending_loop_work`), instead of spinning on an unsettled TLA.
pub fn wait_for_module_promise(&mut self, promise: *mut JSInternalPromise) {
// Read as a raw ptr (Copy) so it doesn't borrow `self` across the
// `&mut self` calls (`tick`, `auto_tick`) below.
let jsc_vm = self.jsc_vm;
// SAFETY: `promise` is a live JSC heap cell tracked by the VM (caller
// just obtained it from `reload_entry_point`).
while crate::JSPromise::status_ptr(promise) == crate::js_promise::Status::Pending {
// SAFETY: `jsc_vm` is the live per-thread JSC VM (set in `init`).
if unsafe { &*jsc_vm }.execution_forbidden() {
return;
}
self.event_loop_mut().tick();
if crate::JSPromise::status_ptr(promise) != crate::js_promise::Status::Pending {
return;
}
// Nothing left that could settle the promise: return instead of
// busy-spinning (see `has_pending_loop_work`).
if !self.has_pending_loop_work() {
return;
}
self.auto_tick();
}
}

/// True when the entry module's evaluation promise is still pending, i.e.
/// (once the loop has drained) an unsettled top-level await.
pub fn entry_point_evaluation_is_pending(&self) -> bool {
match self.pending_internal_promise {
Some(p) => crate::JSPromise::status_ptr(p) == crate::js_promise::Status::Pending,
None => false,
}
}

/// Print Node's "Detected unsettled top-level await" warning to stderr,
/// naming the stalled module(s) from the JSC module registry (falling back
/// to the entry path in eval mode).
pub fn report_unsettled_top_level_await(&self) {
unsafe extern "C" {
fn Bun__findStalledTopLevelAwait(global: *mut JSGlobalObject) -> bun_core::String;
}
// SAFETY: `self.global` is the live per-thread global object.
let stalled = unsafe { Bun__findStalledTopLevelAwait(self.global) };
let stalled_utf8 = stalled.to_utf8();
let slice = stalled_utf8.slice();
let warn = |module: &[u8]| {
bun_core::pretty_errorln!(
"<r><yellow>Warning<r><d>:<r> Detected unsettled top-level await at <b>{}<r>",
bstr::BStr::new(module),
);
};
if !slice.is_empty() {
// The C++ helper NUL-joins multiple stalled specifiers (NUL can't
// appear in a path); print one warning per module, matching Node.
for module in slice.split(|&b| b == b'\0') {
warn(module);
}
} else if !self.main().is_empty() {
warn(self.main());
} else {
bun_core::pretty_errorln!(
"<r><yellow>Warning<r><d>:<r> Detected unsettled top-level await"
);
}
bun_core::Output::flush();
drop(stalled_utf8);
stalled.deref();
}

/// `eventLoop().autoTick()` — dispatched through the runtime hook
/// (needs `Timer::All` for the poll timeout).
#[inline]
Expand Down Expand Up @@ -2434,7 +2523,9 @@ impl VirtualMachine {
return Ok(promise);
}
self.event_loop_mut().perform_gc();
self.wait_for_promise(jsc::AnyPromise::Internal(promise));
// Returns with the promise still pending if the loop drains, so the
// caller can detect an unsettled top-level await (warn + exit 13).
self.wait_for_module_promise(promise);
Comment thread
robobun marked this conversation as resolved.
}

Ok(self.pending_internal_promise.unwrap_or(promise))
Expand Down
30 changes: 30 additions & 0 deletions src/jsc/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
#include "JavaScriptCore/JSModuleLoader.h"
#include "JavaScriptCore/CyclicModuleRecord.h"
#include "JavaScriptCore/ModuleRegistryEntry.h"
#include <wtf/text/StringBuilder.h>
#include "JavaScriptCore/JSModuleNamespaceObject.h"
#include "JavaScriptCore/JSModuleNamespaceObjectInlines.h"
#include "JavaScriptCore/JSModuleRecord.h"
Expand Down Expand Up @@ -719,6 +720,35 @@ static bool isModuleEvaluated(JSC::AbstractModuleRecord* record)
return record->moduleEnvironmentMayBeNull() != nullptr;
}

// Module specifiers suspended on their own top-level await (EvaluatingAsync,
// syntactic TLA, no pending async dependency), NUL-joined, for the
// unsettled-TLA warning. Empty BunString when nothing is stalled.
extern "C" BunString Bun__findStalledTopLevelAwait(JSC::JSGlobalObject* globalObject)
{
WTF::StringBuilder builder;
for (auto& [key, entry] : globalObject->moduleLoader()->moduleMap()) {
if (!key.first || !entry)
continue;
auto* record = entry->record();
if (!record || !record->hasTLA())
continue;
auto* cyclic = dynamicDowncast<JSC::CyclicModuleRecord>(record);
if (!cyclic || cyclic->status() != JSC::CyclicModuleRecord::Status::EvaluatingAsync)
continue;
// EvaluatingAsync only because it awaits a dependency: that dependency
// is the real culprit, skip this one.
if (auto pending = record->pendingAsyncDependencies(); pending && *pending > 0)
continue;
// NUL separator: it cannot appear in a module specifier/path.
if (!builder.isEmpty())
builder.append('\0');
builder.append(String { key.first });
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
if (builder.isEmpty())
return BunStringEmpty;
return Bun::toStringRef(builder.toString());
}

JSC_DEFINE_HOST_FUNCTION(functionEsmNamespaceForCjs, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(globalObject);
Expand Down
165 changes: 122 additions & 43 deletions src/runtime/cli/run_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1581,54 +1581,78 @@ impl Run {
vm.auto_tick_active();
}

if ctx.runtime_options.eval.eval_and_print {
let to_print: JSValue = 'brk: {
let result = vm
.entry_point_result
.value
.get()
.unwrap_or(JSValue::UNDEFINED);
if let Some(promise) = result.as_any_promise() {
match promise.status() {
PromiseStatus::Pending => {
// C-ABI shims are emitted by
// `generate-host-exports.ts` into
// `crate::generated_host_exports` under their
// link name (`Bun__on…EntryPointResult`).
result.then2(
vm.global(),
JSValue::UNDEFINED,
crate::generated_host_exports::Bun__onResolveEntryPointResult,
crate::generated_host_exports::Bun__onRejectEntryPointResult,
);
vm.tick();
vm.auto_tick_active();
while vm.is_event_loop_alive() {
vm.tick();
vm.auto_tick_active();
}
break 'brk result;
}
_ => break 'brk promise.result(vm.jsc_vm()),
// A settled entry prints its `--print` value before `beforeExit`
// (Node's order: value then beforeExit output); a pending/rejected
// entry prints nothing (a bogus `Promise { <pending> }`; reported below).
let eval_and_print = ctx.runtime_options.eval.eval_and_print;
let printed = eval_and_print && entry_point_print_ok(vm);
if printed {
print_eval_result(vm);
}

vm.on_before_exit();
Comment thread
robobun marked this conversation as resolved.

// A `beforeExit` handler may resolve the entry's await (a bare
// `resolve()` queues a microtask `is_event_loop_alive()` ignores);
// drain it, and re-run the loop if the resumed body schedules work.
while vm.entry_point_evaluation_is_pending() {
vm.tick();
if !vm.is_event_loop_alive() {
break;
}
while vm.is_event_loop_alive() {
vm.tick();
vm.auto_tick_active();
}
vm.on_before_exit();
}
Comment thread
robobun marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.

// A `beforeExit` handler that just resolved the TLA prints its value
// now (a still-pending/rejected entry is reported below instead).
if eval_and_print && !printed && entry_point_print_ok(vm) {
print_eval_result(vm);
}

// The entry promise is pre-marked handled, so report it here:
// pending (unsettled TLA) → exit 13; late-rejected (resumed body
// threw) → exit 1, gated so an initial-load rejection isn't redone.
if let Some(p) = vm.pending_internal_promise {
// SAFETY: `p` is a live JSC heap cell tracked by the VM.
match bun_jsc::JSPromise::status_ptr(p) {
PromiseStatus::Pending => {
vm.report_unsettled_top_level_await();
if vm.exit_handler.exit_code == 0 {
vm.exit_handler.exit_code = 13;
}
}
result
};
// SAFETY: `vals[..1]` is the single stack `to_print`; null
// `ctype` routes to the VM's stdout/stderr default.
unsafe {
bun_jsc::ConsoleObject::message_with_type_and_level(
::core::ptr::null_mut(),
bun_jsc::ConsoleObject::MessageType::Log,
bun_jsc::ConsoleObject::MessageLevel::Log,
vm.global(),
&raw const to_print,
1,
);
PromiseStatus::Rejected
Comment thread
robobun marked this conversation as resolved.
if vm.pending_internal_promise_reported_at != vm.hot_reload_counter =>
{
vm.pending_internal_promise_reported_at = vm.hot_reload_counter;
// `on_before_exit` set `exit_on_uncaught_exception`, which
// hard-exits before a user `uncaughtException` handler;
// clear it so the throw reaches it (Node: handler -> 0).
vm.exit_on_uncaught_exception = false;
// SAFETY: `p` is a live JSC heap cell; `vm.jsc_vm` set in `init`.
let result = unsafe { &mut *p }.result(unsafe { &mut *vm.jsc_vm });
let global = vm.global;
// SAFETY: `global` valid for VM lifetime. `uncaught_exception`
// runs a user handler (exit 0) and sets exit_code = 1 itself
// when unhandled.
let _ = vm.uncaught_exception(unsafe { &*global }, result, true);
// SAFETY: `p` is a live JSC heap cell.
unsafe { &mut *p }.set_handled();
Comment thread
robobun marked this conversation as resolved.
}
_ => {}
}
}

vm.on_before_exit();
// An `uncaughtException` handler above may have scheduled async work
// (e.g. a timer); drain it, mirroring the initial-load path.
while vm.is_event_loop_alive() {
vm.tick();
vm.auto_tick_active();
}
}

if log_has_msgs(vm) {
Expand Down Expand Up @@ -1658,6 +1682,61 @@ impl Run {
}
}

/// Whether the entry module's `--print` value is ready to print: the entry
/// promise is absent (e.g. patched runMain) or fulfilled. A pending (unsettled
/// TLA) or rejected entry prints nothing; it is reported (exit 13 / 1) instead.
fn entry_point_print_ok(vm: &VirtualMachine) -> bool {
vm.pending_internal_promise
.is_none_or(|p| bun_jsc::JSPromise::status_ptr(p) == PromiseStatus::Fulfilled)
}

/// Print the `--print`/`-p` eval result: the entry module's completion value,
/// unwrapping the pipeline promise (draining the loop if it is still pending).
fn print_eval_result(vm: &mut VirtualMachine) {
let to_print: JSValue = 'brk: {
let result = vm
.entry_point_result
.value
.get()
.unwrap_or(JSValue::UNDEFINED);
if let Some(promise) = result.as_any_promise() {
match promise.status() {
PromiseStatus::Pending => {
// C-ABI shims are emitted by `generate-host-exports.ts` into
// `crate::generated_host_exports` under their link name.
result.then2(
vm.global(),
JSValue::UNDEFINED,
crate::generated_host_exports::Bun__onResolveEntryPointResult,
crate::generated_host_exports::Bun__onRejectEntryPointResult,
);
vm.tick();
vm.auto_tick_active();
while vm.is_event_loop_alive() {
vm.tick();
vm.auto_tick_active();
}
break 'brk result;
}
_ => break 'brk promise.result(vm.jsc_vm()),
}
}
result
};
// SAFETY: `vals[..1]` is the single stack `to_print`; null `ctype` routes
// to the VM's stdout/stderr default.
unsafe {
bun_jsc::ConsoleObject::message_with_type_and_level(
::core::ptr::null_mut(),
bun_jsc::ConsoleObject::MessageType::Log,
bun_jsc::ConsoleObject::MessageLevel::Log,
vm.global(),
&raw const to_print,
1,
);
}
}

#[inline]
fn log_has_msgs(vm: &VirtualMachine) -> bool {
match vm.log {
Expand Down
Loading
Loading