Skip to content
Open
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
1 change: 0 additions & 1 deletion mordant-baseline.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 2 additions & 6 deletions src/jsc/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<JSC::AbstractModuleRecord>(moduleRecordValue)) {
JSC::JSValue state = moduleRecord->internalField(JSC::AbstractModuleRecord::Field::State).get();
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 1 addition & 3 deletions src/jsc/bindings/ZigGlobalObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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));

Expand Down
4 changes: 0 additions & 4 deletions src/jsc/bindings/headers.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 25 additions & 26 deletions src/runtime/cli/run_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
48 changes: 0 additions & 48 deletions src/runtime/hw_exports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<JSValue> {
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<JSValue> {
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
Expand Down
92 changes: 92 additions & 0 deletions test/cli/run/run-eval.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { <pending> }\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 { <pending> }\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")');
Expand Down
Loading