diff --git a/mordant-baseline.toml b/mordant-baseline.toml index 2e6f3cb583f..54b11b7d93b 100644 --- a/mordant-baseline.toml +++ b/mordant-baseline.toml @@ -42,7 +42,6 @@ "error_collapsed_to_bool:src/runtime/ffi/ffi_body.rs" = 1 "field_valid_only_when:src/runtime/shell/builtin/rm.rs" = 1 "reimplemented_helper:src/runtime/api/bun/Terminal.rs" = 1 -"reimplemented_helper:src/runtime/hw_exports.rs" = 1 "same_match_twice:src/runtime/api/bun/h2_frame_parser.rs" = 1 "same_match_twice:src/runtime/cli/pack_command.rs" = 1 "same_match_twice:src/runtime/cli/update_interactive_command.rs" = 2 diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index ca0613b6d85..e8a1c103b55 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -3993,8 +3993,8 @@ JSC::JSValue EvalGlobalObject::moduleLoaderEvaluate(JSGlobalObject* lexicalGloba // // Instead, when the module yielded, capture the async capability's // promise. Its resolution value is the module's final completion - // value; the --print loop in run_command.rs already unwraps promises - // via asAnyPromise + Bun__onResolveEntryPointResult. + // value; the --print path in run_command.rs unwraps a promise result + // once the event loop has drained. JSC::JSValue valueToStore = result; if (auto* moduleRecord = dynamicDowncast(moduleRecordValue)) { JSC::JSValue state = moduleRecord->internalField(JSC::AbstractModuleRecord::Field::State).get(); @@ -4127,10 +4127,6 @@ GlobalObject::PromiseFunctions GlobalObject::promiseHandlerID(Zig::FFIFunction h return GlobalObject::PromiseFunctions::Bun__HTMLRewriter__onHandlerResolve; } else if (handler == Bun__HTMLRewriter__onHandlerReject) { return GlobalObject::PromiseFunctions::Bun__HTMLRewriter__onHandlerReject; - } else if (handler == Bun__onResolveEntryPointResult) { - return GlobalObject::PromiseFunctions::Bun__onResolveEntryPointResult; - } else if (handler == Bun__onRejectEntryPointResult) { - return GlobalObject::PromiseFunctions::Bun__onRejectEntryPointResult; } else if (handler == Bun__NodeHTTPRequest__onResolve) { return GlobalObject::PromiseFunctions::Bun__NodeHTTPRequest__onResolve; } else if (handler == Bun__NodeHTTPRequest__onReject) { diff --git a/src/jsc/bindings/ZigGlobalObject.h b/src/jsc/bindings/ZigGlobalObject.h index 35a8a4f69ec..441465aa4a7 100644 --- a/src/jsc/bindings/ZigGlobalObject.h +++ b/src/jsc/bindings/ZigGlobalObject.h @@ -413,8 +413,6 @@ class GlobalObject : public Bun::GlobalScope { Bun__TestScope__Describe2__bunTestCatch, Bun__HTMLRewriter__onHandlerResolve, Bun__HTMLRewriter__onHandlerReject, - Bun__onResolveEntryPointResult, - Bun__onRejectEntryPointResult, Bun__NodeHTTPRequest__onResolve, Bun__NodeHTTPRequest__onReject, Bun__FileStreamWrapper__onRejectRequestStream, @@ -438,7 +436,7 @@ class GlobalObject : public Bun::GlobalScope { Bun__HTMLRewriter__onResolveInputStream, Bun__HTMLRewriter__onRejectInputStream, }; - static constexpr size_t promiseFunctionsSize = 48; + static constexpr size_t promiseFunctionsSize = 46; static PromiseFunctions promiseHandlerID(SYSV_ABI EncodedJSValue (*handler)(JSC::JSGlobalObject* arg0, JSC::CallFrame* arg1)); diff --git a/src/jsc/bindings/headers.h b/src/jsc/bindings/headers.h index 8c1f1f6a1ff..8e5d32aa8d1 100644 --- a/src/jsc/bindings/headers.h +++ b/src/jsc/bindings/headers.h @@ -816,10 +816,6 @@ CPP_DECL bool JSC__CustomGetterSetter__isSetterNull(JSC::CustomGetterSetter *arg #ifdef __cplusplus -BUN_DECLARE_HOST_FUNCTION(Bun__onResolveEntryPointResult); -BUN_DECLARE_HOST_FUNCTION(Bun__onRejectEntryPointResult); - - BUN_DECLARE_HOST_FUNCTION(Bun__FileStreamWrapper__onResolveRequestStream); BUN_DECLARE_HOST_FUNCTION(Bun__FileStreamWrapper__onRejectRequestStream); diff --git a/src/runtime/cli/run_command.rs b/src/runtime/cli/run_command.rs index b0a92bc2ac4..80eb3f4e64a 100644 --- a/src/runtime/cli/run_command.rs +++ b/src/runtime/cli/run_command.rs @@ -1495,37 +1495,36 @@ impl Run<'_> { } 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, - ); + let result = vm + .entry_point_result + .value + .get() + .unwrap_or(JSValue::UNDEFINED); + let to_print: JSValue = match result.as_any_promise() { + Some(promise) => { + // Drained with the result unsettled (only unref'd work + // left): one more turn, then print whatever state it is + // in. Not when an unhandled error stopped the loop: as in + // `on_before_exit`, nothing of the script runs after that + // (the turn would only race its timers against the next + // internal wakeup). Exiting stays with the sequence + // below; a reaction on the promise would bypass it. + if promise.status() == PromiseStatus::Pending + && vm.unhandled_error_counter == 0 + { + vm.tick(); + vm.auto_tick_active(); + while vm.is_event_loop_alive() { 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()), + } + match promise.status() { + PromiseStatus::Pending => result, + _ => promise.result(vm.jsc_vm()), } } - result + None => result, }; // SAFETY: `vals[..1]` is the single stack `to_print`; null // `ctype` routes to the VM's stdout/stderr default. diff --git a/src/runtime/hw_exports.rs b/src/runtime/hw_exports.rs index c3158beb8a3..94ff03eb921 100644 --- a/src/runtime/hw_exports.rs +++ b/src/runtime/hw_exports.rs @@ -302,54 +302,6 @@ mod sql_hooks { }; } -// ─── entry-point promise reactions (used by `--print`) ─────────────────────── - -// HOST_EXPORT(Bun__onResolveEntryPointResult) -pub fn on_resolve_entry_point_result( - global: &JSGlobalObject, - callframe: &CallFrame, -) -> bun_jsc::JsResult { - let result = callframe.argument(0); - // SAFETY: `vals[..len]` is the single stack `result`; `ctype` is ignored by - // `message_with_type_and_level` (it always resolves the per-VM console via - // `vm_console(global)`), so null is fine. - unsafe { - bun_jsc::ConsoleObject::message_with_type_and_level( - core::ptr::null_mut(), - bun_jsc::ConsoleObject::MessageType::Log, - bun_jsc::ConsoleObject::MessageLevel::Log, - global, - &raw const result, - 1, - ); - } - // SAFETY: bun_vm() never null for a Bun-owned global. - bun_core::Global::exit(u32::from(global.bun_vm().as_mut().exit_handler.exit_code)); -} - -// HOST_EXPORT(Bun__onRejectEntryPointResult) -pub fn on_reject_entry_point_result( - global: &JSGlobalObject, - callframe: &CallFrame, -) -> bun_jsc::JsResult { - let result = callframe.argument(0); - // SAFETY: `vals[..len]` is the single stack `result`; `ctype` is ignored by - // `message_with_type_and_level` (it always resolves the per-VM console via - // `vm_console(global)`), so null is fine. - unsafe { - bun_jsc::ConsoleObject::message_with_type_and_level( - core::ptr::null_mut(), - bun_jsc::ConsoleObject::MessageType::Log, - bun_jsc::ConsoleObject::MessageLevel::Log, - global, - &raw const result, - 1, - ); - } - // SAFETY: bun_vm() never null for a Bun-owned global. - bun_core::Global::exit(u32::from(global.bun_vm().as_mut().exit_handler.exit_code)); -} - // ─── bindgenv2 dispatch shims (`bindgen_*_dispatch*`) ──────────────────────── // // These satisfy the `extern "C"` refs C++ emits from diff --git a/test/cli/run/run-eval.test.ts b/test/cli/run/run-eval.test.ts index 0f928a7ce6a..83cdd76b1b8 100644 --- a/test/cli/run/run-eval.test.ts +++ b/test/cli/run/run-eval.test.ts @@ -175,6 +175,98 @@ describe("--print for cjs/esm", () => { }); }); +// `--print` looks at the result once the event loop is done. A result promise +// that is still pending when the loop drained on its own gets one more turn of +// the loop; one left pending because an unhandled error stopped the loop does +// not (nothing of the script runs after such an error, as with `-e`). Either +// way the process then leaves through the regular exit path ('exit' listeners, +// exit code), not from inside a reaction on the promise. +// +// Every script below arms the timer that would settle the result and then +// blocks in Bun.sleepSync, so the timer is overdue by the time the loop is +// looked at: it fires for sure in the extra turn of the unref cases, and its +// callback not having run in the error cases shows that nothing ran after the +// error, whatever the timing. +describe.concurrent("--print with a result promise still pending when the event loop is done", () => { + const exitListener = `process.on("exit", () => console.log("exit listener ran"));`; + + async function print(script: string) { + await using proc = Bun.spawn({ + cmd: [bunExe(), "--print", script], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + return { stdout, stderr, exitCode }; + } + + test("fulfilled by an unref'd timer: prints the value, then runs 'exit' listeners", async () => { + const { stdout, stderr, exitCode } = await print( + `${exitListener} + new Promise(resolve => { setTimeout(() => resolve("settled late"), 1).unref(); Bun.sleepSync(10); })`, + ); + expect(stderr).toBe(""); + expect(stdout).toBe("settled late\nexit listener ran\n"); + expect(exitCode).toBe(0); + }); + + test("rejected by an unref'd timer: reported as unhandled and exits 1, like a rejection during the loop", async () => { + const { stdout, stderr, exitCode } = await print( + `${exitListener} + new Promise((_, reject) => { setTimeout(() => reject("rejected late"), 1).unref(); Bun.sleepSync(10); })`, + ); + expect(stderr).toContain("error: rejected late"); + // What a rejected result is printed as is not this block's subject. + expect(stdout).toEndWith("exit listener ran\n"); + expect(exitCode).toBe(1); + }); + + test.each(["resolve", "reject"])( + "unhandled rejection stopped the loop: the %s() timer does not run, exits 1 after 'exit' listeners", + async settle => { + const { stdout, stderr, exitCode } = await print( + `${exitListener} + Promise.reject(new Error("early rejection")); + new Promise((resolve, reject) => { + setTimeout(() => { console.log("timer ran"); ${settle}("late"); }, 1); + Bun.sleepSync(10); + })`, + ); + expect(stderr).toContain("early rejection"); + expect(stdout).toBe("Promise { }\nexit listener ran\n"); + expect(exitCode).toBe(1); + }, + ); + + test("uncaught exception from a timer stopped the loop: 'exit' listeners see code 1", async () => { + // The settling timer is armed by the throwing callback itself. Whether the + // turn of the loop that ran the throw still fires it differs by platform + // (libuv runs timers again after its poll), so what gets printed is not + // asserted, only that the process left through the exit listeners. + const { stdout, stderr, exitCode } = await print( + `process.on("exit", code => console.log("exit listener ran with", code)); + new Promise(resolve => { + setTimeout(() => { + setTimeout(() => resolve("settled late"), 1); + Bun.sleepSync(10); + throw new Error("thrown in timer"); + }, 1); + })`, + ); + expect(stderr).toContain("thrown in timer"); + expect(stdout).toEndWith("exit listener ran with 1\n"); + expect(exitCode).toBe(1); + }); + + test("never settles: prints the promise and exits normally", async () => { + const { stdout, stderr, exitCode } = await print(`${exitListener} new Promise(() => {})`); + expect(stderr).toBe(""); + expect(stdout).toBe("Promise { }\nexit listener ran\n"); + expect(exitCode).toBe(0); + }); +}); + function group(run: (code: string) => SyncSubprocess<"pipe", "inherit">) { test("it works", async () => { const { stdout } = run('console.log("hello world")');