diff --git a/packages/compiler/src/backend/llvm/emit-journal.ts b/packages/compiler/src/backend/llvm/emit-journal.ts index b2b3fc4cc..f290f8df8 100644 --- a/packages/compiler/src/backend/llvm/emit-journal.ts +++ b/packages/compiler/src/backend/llvm/emit-journal.ts @@ -103,6 +103,7 @@ export interface ChunkResult { readonly flags: { readonly usesCoroutines: boolean; readonly usesTimers: boolean; + readonly usesPromises: boolean; readonly usesGenSlots: boolean; readonly needsUnionNone: boolean; readonly needsNullableAbsent: boolean; diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 549f5d748..caf402e9d 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -603,14 +603,16 @@ export function emitModuleParts(emitter: LlEmitter): string[] { const globalReleases = exitReleaseLines("g"); const asyncEntry = emitter.fnByName.get(emitter.mod.entry)?.async === true; const entryMayThrow = emitter.mayThrow.has(emitter.mod.entry); - // The event loop runs when timers appeared OR any async/generator - // function exists (the C main's hasAsync || hasGenerators || - // usesTimers gate). Generator programs run + // The event loop runs when timers appeared, a promise is made (the loop's + // end reports an unhandled rejection, Node's exit 1), OR any + // async/generator function exists (the C main's hasAsync || + // hasGenerators || usesTimers gate). Generator programs run // the loop too: its exit accounting notes still-suspended generator // fibers as abandoned, so the RC audit downgrades exactly like the // async loop-exhaustion story. const runsLoop = emitter.usesTimers || + emitter.usesPromises || emitter.ffiHasForeignCallback || emitter.mod.functions.some((f) => f.async === true || f.generator !== undefined); const uncaughtReleases = entryMayThrow && !asyncEntry ? exitReleaseLines("gu") : []; diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 9c2d3bfe6..ae533b31f 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1635,6 +1635,10 @@ export class LlEmitter { /** setTimeout and friends appeared somewhere: main must run the event * loop even in programs with no async functions. */ usesTimers = false; + /** A promise is made somewhere (Promise statics, `new Promise`, + * Promise.withResolvers): main runs the event loop, whose end reports an + * unhandled rejection as Node does, even without async functions. */ + usesPromises = false; /** The program calls mock.timers members (moduleUsesMockTimers): timer * calls emit scr_test.c's mock-aware entry points. */ readonly mockTimers: boolean; @@ -5717,6 +5721,7 @@ export class LlEmitter { flags: { usesCoroutines: this.usesCoroutines, usesTimers: this.usesTimers, + usesPromises: this.usesPromises, usesGenSlots: this.usesGenSlots, needsUnionNone: this.needsUnionNone, needsNullableAbsent: this.needsNullableAbsent, @@ -5754,6 +5759,7 @@ export class LlEmitter { const flags = chunk.flags; this.usesCoroutines ||= flags.usesCoroutines; this.usesTimers ||= flags.usesTimers; + this.usesPromises ||= flags.usesPromises; this.usesGenSlots ||= flags.usesGenSlots; this.needsUnionNone ||= flags.needsUnionNone; this.needsNullableAbsent ||= flags.needsNullableAbsent; diff --git a/packages/compiler/src/backend/llvm/expr-dispatch.ts b/packages/compiler/src/backend/llvm/expr-dispatch.ts index e53f47d47..aaddd47c8 100644 --- a/packages/compiler/src/backend/llvm/expr-dispatch.ts +++ b/packages/compiler/src/backend/llvm/expr-dispatch.ts @@ -100,6 +100,7 @@ export function emitExpr(host: LlvmEmitterContext, e: IrExpr): LlValue { case "caughtToDyn": return host.emitDynamicExpr(e); case "intrinsic": + if (e.name.startsWith("promise.")) host.usesPromises = true; return host.emitIntrinsicExpr(e); case "jsonStringify": case "dynCheck": @@ -108,8 +109,10 @@ export function emitExpr(host: LlvmEmitterContext, e: IrExpr): LlValue { case "genResume": case "awaitExpr": case "awaitUnionExpr": + return host.emitAsyncExpr(e); case "newPromise": case "promiseWithResolvers": + host.usesPromises = true; return host.emitAsyncExpr(e); case "libCall": return host.emitLibCall(e); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/unhandled-rejection-without-async.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/unhandled-rejection-without-async.cjs.json new file mode 100644 index 000000000..e07772e93 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/unhandled-rejection-without-async.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/unhandled-rejection-without-async.cjs" + ], + "diags": [] +} diff --git a/tests/corpus/unhandled-rejection-without-async.cjs b/tests/corpus/unhandled-rejection-without-async.cjs new file mode 100644 index 000000000..3990cd402 --- /dev/null +++ b/tests/corpus/unhandled-rejection-without-async.cjs @@ -0,0 +1,10 @@ +// @exit: 1 +'use strict'; +// A program with no async functions or timers still ends Node's way when a +// promise rejection goes unhandled: after the script, the 'exit' listeners +// see code 1 and the process exits 1. Settled promises alone do not fail. +process.on('exit', (code) => console.log('exit listener', code)); +const settled = Promise.resolve(1); +new Promise((resolve) => resolve(2)); +console.log('end of script', typeof settled); +new Promise((resolve, reject) => reject(new TypeError('rejected in the executor'))); diff --git a/tests/harness/corpus-costs/unhandled-rejection-without-async.cjs.json b/tests/harness/corpus-costs/unhandled-rejection-without-async.cjs.json new file mode 100644 index 000000000..7da124d86 --- /dev/null +++ b/tests/harness/corpus-costs/unhandled-rejection-without-async.cjs.json @@ -0,0 +1 @@ +{ "plain": 2511, "san": 3503 }