From 35cf7efe551f8d07fec0620db12620abe5d70be7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 13 Oct 2026 07:29:48 -0500 Subject: [PATCH] Make short async calls and awaits cheap --- packages/compiler/src/backend/llvm/emitter.ts | 144 ++++++++- .../compiler/src/backend/llvm/expr-async.ts | 12 + .../compiler/src/backend/llvm/expr-calls.ts | 20 ++ packages/compiler/src/backend/mangle.ts | 5 + .../compiler/test/await-call-emission.test.ts | 73 +++++ .../corpus/4880-await-call-turn-order.ts.json | 6 + .../corpus/4881-await-call-exceptions.ts.json | 6 + .../4882-await-call-request-chain.ts.json | 6 + .../corpus/4883-await-values-and-hops.ts.json | 6 + .../4884-await-call-unhandled-sibling.ts.json | 6 + .../4885-await-call-checkpoints.ts.json | 6 + packages/runtime/src/scr_async.c | 282 +++++++++++++++--- packages/runtime/src/scr_runtime.h | 11 + tests/corpus/4880-await-call-turn-order.ts | 88 ++++++ tests/corpus/4881-await-call-exceptions.ts | 82 +++++ tests/corpus/4882-await-call-request-chain.ts | 53 ++++ tests/corpus/4883-await-values-and-hops.ts | 32 ++ .../4884-await-call-unhandled-sibling.ts | 22 ++ tests/corpus/4885-await-call-checkpoints.ts | 40 +++ 19 files changed, 847 insertions(+), 53 deletions(-) create mode 100644 packages/compiler/test/await-call-emission.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4880-await-call-turn-order.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4881-await-call-exceptions.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4882-await-call-request-chain.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4883-await-values-and-hops.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4884-await-call-unhandled-sibling.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4885-await-call-checkpoints.ts.json create mode 100644 tests/corpus/4880-await-call-turn-order.ts create mode 100644 tests/corpus/4881-await-call-exceptions.ts create mode 100644 tests/corpus/4882-await-call-request-chain.ts create mode 100644 tests/corpus/4883-await-values-and-hops.ts create mode 100644 tests/corpus/4884-await-call-unhandled-sibling.ts create mode 100644 tests/corpus/4885-await-call-checkpoints.ts diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index b01d12c29..526cc9f78 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -95,6 +95,7 @@ import { RUNTIME_ABI_MARKER } from "../runtime-abi.js"; import { computeMayThrow } from "../may-throw.js"; import { mangleArgPack, + mangleAsyncAwaitCall, mangleAsyncSpawn, mangleBorrowedFunction, mangleClassObj, @@ -659,6 +660,9 @@ export class LlEmitter { /** Method names with at least one may-throw implementation — the * virtualCall pending check's key. */ readonly mayThrowMethods = new Set(); + /** Async functions awaited directly at some `await f(...)` site; each + * gets an await-call entry (emitAsyncAwaitCall). */ + readonly awaitCallees = new Set(); /** setTimeout and friends appeared somewhere: main must run the event * loop even in programs with no async functions. */ usesTimers = false; @@ -2996,8 +3000,14 @@ export class LlEmitter { const definitions = [`%${pack} = type { ${fieldTys.join(", ") || "i8"} } ; ${fn.name} args`]; const sizeOf = `ptrtoint (ptr getelementptr (%${pack}, ptr null, i32 1) to ${this.sizeType})`; - this.declare(`declare void @free(ptr)`); - this.declare(`declare ptr @malloc(${this.sizeType})`); + // Async argument packs live from the spawn to the trampoline's first + // instructions, so they take the runtime's small-block allocator. + // Generator packs keep malloc: the runtime frees never-started ones. + const smallPack = fn.generator === undefined; + const packAlloc = smallPack ? "scr_rt_calloc" : "malloc"; + const packFree = smallPack ? "scr_rt_free" : "free"; + this.declare(`declare void @${packFree}(ptr)`); + this.declare(`declare ptr @${packAlloc}(${this.sizeType})`); const tr: string[] = [ `define internal void @${mangleTrampoline(fn.name)}(ptr %self, ptr %ap) ${FN_ATTRS} {`, @@ -3011,7 +3021,7 @@ export class LlEmitter { ); loads.push(`${ty} %a${i}`); }); - tr.push(` call void @free(ptr %ap)`); + tr.push(` call void @${packFree}(ptr %ap)`); const ret = fn.returnType; const retTy = this.llType(ret); const bodyCall = `call ${retTy} @${mangleFunction(fn.name)}(${loads.join(", ")})`; @@ -3022,7 +3032,7 @@ export class LlEmitter { const spawnParams = fieldTys.map((ty, i) => `${ty} %a${i}`); const argPackLines = [ - ` %ap = call ptr @malloc(${this.sizeType} ${sizeOf})`, + ` %ap = call ptr @${packAlloc}(${this.sizeType} ${sizeOf})`, ` %isnull = icmp eq ptr %ap, null`, ` br i1 %isnull, label %oom, label %ok`, `oom:`, @@ -3094,7 +3104,7 @@ export class LlEmitter { const eligible = new Set(); if (this.wasi || this.mod.workers === true) return eligible; const suspends = - /@scr_(?:await_|module_await\b|promise_await_settled\b|test_sub\b|gen_yield_|async_gen_|fiber_)/; + /@(?:scr_(?:await_|module_await\b|promise_await_settled\b|test_sub\b|gen_yield_|async_gen_|fiber_)|sc_aw_)/; for (const decl of this.decls) if (/@scr_test_sub\b/.test(decl)) return eligible; for (const text of this.fnDefText.values()) if (/@scr_test_sub\b/.test(text)) return eligible; const byMangled = new Map(); @@ -3180,6 +3190,118 @@ export class LlEmitter { return out; } + /** Whether `await f(...)` may enter f through its await-call entry. The + * entry's result is the awaited value of f's own body type; direct calls + * never carry a lifted environment. */ + awaitCallEligible(fnName: string): boolean { + if (this.wasi || this.mod.workers === true) return false; + const fn = this.fnByName.get(fnName); + if (fn === undefined || fn.async !== true || fn.generator !== undefined) return false; + if (fn.captures !== undefined || fn.returnType.kind === "dyn") return false; + return fn.asyncCacheGlobal === undefined && fn.asyncCycleCacheGlobal === undefined; + } + + /** The await-call entry of an async function (`sc_aw_*`): one direct + * `await f(...)` as a call returning the awaited value, so the call site + * needs no promise of its own. A fiberless body runs on the caller's + * stack as in its spawn wrapper; a clean result then takes the await's + * one microtask turn (scr_await_hop) and is returned without ever + * materializing the promise, which nothing else could observe. A throw + * builds the rejected promise exactly as the spawn wrapper would and + * awaits it, so the rejection reaches the awaiter on the same turn. Other + * bodies pack their arguments as the spawn wrapper does and enter + * scr_async_spawn_await_*, which parks the caller on the promise as soon + * as the body first suspends. */ + private emitAsyncAwaitCall(fn: IrFunction, argPackLines: string[] | null): string[] { + const ret = fn.returnType; + const retTy = this.llType(ret); + const params = fn.params.map((p, i) => `${this.llType(p.type)} %a${i}`); + const awaitLine = (dest: string, promise: string): string => { + switch (ret.kind) { + case "void": + this.declare(`declare void @scr_await_void(ptr)`); + return ` call void @scr_await_void(ptr ${promise})`; + case "f64": + case "date": + this.declare(`declare double @scr_await_f64(ptr)`); + return ` ${dest} = call double @scr_await_f64(ptr ${promise})`; + case "bool": + this.declare(`declare zeroext i1 @scr_await_bool(ptr)`); + return ` ${dest} = call zeroext i1 @scr_await_bool(ptr ${promise})`; + case "string": + this.declare(`declare ptr @scr_await_str(ptr)`); + return ` ${dest} = call ptr @scr_await_str(ptr ${promise})`; + default: + this.declare(`declare ptr @scr_await_ref(ptr)`); + return ` ${dest} = call ptr @scr_await_ref(ptr ${promise})`; + } + }; + const retLine = (v: string): string => + retTy === "void" ? ` ret void` : ` ret ${retTy} ${v}`; + const head = `define internal ${retTy} @${mangleAsyncAwaitCall(fn.name)}(${params.join(", ")}) ${FN_ATTRS} { ; await ${fn.name}`; + const args = params.join(", "); + if (argPackLines !== null) { + const kind = + ret.kind === "void" + ? "void" + : ret.kind === "f64" || ret.kind === "date" + ? "f64" + : ret.kind === "bool" + ? "bool" + : ret.kind === "string" + ? "str" + : "ref"; + const cret = kind === "bool" ? "zeroext i1" : retTy; + this.declare(`declare ${cret} @scr_async_spawn_await_${kind}(ptr, ptr)`); + this.needOom(); + const call = `call ${cret} @scr_async_spawn_await_${kind}(ptr @${mangleTrampoline(fn.name)}, ptr %ap)`; + return [ + head, + `entry:`, + ...argPackLines, + kind === "void" ? ` ${call}` : ` %v = ${call}`, + retLine("%v"), + `}`, + ``, + ]; + } + this.declare(`declare void @scr_async_inline_enter(ptr)`); + this.declare(`declare void @scr_async_inline_exit(ptr)`); + this.declare(`declare void @scr_await_hop()`); + this.declare(`declare ptr @scr_promise_new()`); + this.declare(`declare void @scr_promise_reject_pending(ptr)`); + this.declare(`declare void @scr_promise_release(ptr)`); + const bodyCall = `call ${retTy} @${mangleFunction(fn.name)}(${args})`; + const out = [ + head, + `entry:`, + ` %frame = alloca { ptr, ptr }`, + ` call void @scr_async_inline_enter(ptr %frame)`, + retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`, + ...this.pendingTestLines("%pend"), + ` call void @scr_async_inline_exit(ptr %frame)`, + ` br i1 %pend, label %thrown, label %clean`, + `clean:`, + ` call void @scr_await_hop()`, + retLine("%r"), + `thrown:`, + ]; + if (ret.kind !== "void" && isRefCounted(ret)) { + // An escaping throw means %r is the never-read dummy (NULL). + out.push(` call void ${releaseSym(this.shapeHost, ret)}(ptr %r)`); + } + out.push( + ` %p = call ptr @scr_promise_new()`, + ` call void @scr_promise_reject_pending(ptr %p)`, + awaitLine("%v", "%p"), + ` call void @scr_promise_release(ptr %p)`, + retLine("%v"), + `}`, + ``, + ); + return out; + } + /** Per-async-function machinery — async.ts's scaffolding, .ll * flavored: an argument-pack struct type, a fiber trampoline (unpacks, * frees the pack, runs the ordinary compiled body, settles the @@ -3209,6 +3331,9 @@ export class LlEmitter { const fiberless = this.fiberlessAsyncFunctions(); for (const fn of this.mod.functions) { if (fn.async !== true || fn.generator !== undefined) continue; + if (fiberless.has(fn.name) && this.awaitCallees.has(fn.name)) { + for (const line of this.emitAsyncAwaitCall(fn, null)) out.push(line); + } if (fiberless.has(fn.name)) { const fieldTys = [ ...(fn.captures !== undefined ? ["ptr"] : []), @@ -3221,6 +3346,9 @@ export class LlEmitter { const { definitions, ret, tr, spawnParams, argPackLines } = this.emitArgPackAndTrampolinePrologue(fn); for (const line of definitions) out.push(line); + if (this.awaitCallees.has(fn.name)) { + for (const line of this.emitAsyncAwaitCall(fn, argPackLines)) out.push(line); + } this.declare(`declare ptr @scr_fiber_promise(ptr)`); this.declare(`declare ptr @scr_async_spawn(ptr, ptr)`); this.needOom(); @@ -7443,6 +7571,12 @@ export class LlEmitter { return emitCallExpr(this, e); } + /** A direct async call an enclosing `await` consumes whole: emitted + * through the callee's await-call entry, yielding the awaited value. */ + emitAwaitedCall(e: ExprOf<"call">): LlValue { + return emitCallExpr(this, e, true); + } + emitRecordExpr( e: ExprOf< | "fieldGet" diff --git a/packages/compiler/src/backend/llvm/expr-async.ts b/packages/compiler/src/backend/llvm/expr-async.ts index 15e272796..004f8ae24 100644 --- a/packages/compiler/src/backend/llvm/expr-async.ts +++ b/packages/compiler/src/backend/llvm/expr-async.ts @@ -626,6 +626,18 @@ export function emitAsyncExpr( // re-throw here (hence the pending check). Promise temp borrowed; // refcounted results arrive +1 and join the frame pre-check so an // unwind releases the dummy (NULL) harmlessly. + // `await f(...)` of a direct async call: the call and the await go + // through f's await-call entry, which returns the awaited value + // (owned, with the pending check done by the call emission). + if (e.value.kind === "call" && host.awaitCallEligible(e.value.callee)) { + const callee = host.fnByName.get(e.value.callee)!; + if ( + typeEquals(callee.returnType, e.type) && + !host.callLifetimes.borrowed.has(e.value.callee) + ) { + return host.emitAwaitedCall(e.value); + } + } const pr = host.emitExpr(e.value); if (host.wasi) host.emitWasiSuspend(pr.name); if (e.type.kind === "void") { diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index 00b766795..62a00103a 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -9,6 +9,7 @@ import { } from "../../ir/ir.js"; import { collectFfiRetainedOps, parseFfiCallbackKey } from "../ffi-callbacks.js"; import { + mangleAsyncAwaitCall, mangleBorrowedFunction, mangleClassNew, mangleClassRetain, @@ -51,6 +52,7 @@ export function emitCallExpr( | "instanceOf" | "virtualCall" >, + awaitEntry = false, ): LlValue { const B = host.B; switch (e.kind) { @@ -120,6 +122,24 @@ export function emitCallExpr( const argList = args .map((a, i) => `${host.llType(callee.params[i]!.type)} ${a.name}`) .join(", "); + if (awaitEntry) { + // The enclosing await routes this call through the callee's + // await-call entry: the result is the awaited value, and the + // await's rejection re-throw is the pending check. + host.awaitCallees.add(e.callee); + const awaited = callee.returnType; + const entry = `@${mangleAsyncAwaitCall(e.callee)}`; + if (awaited.kind === "void") { + B.line(`call void ${entry}(${argList})`); + host.emitPendingCheck(); + return { name: "", type: awaited }; + } + const t = B.tmp(); + B.line(`${t} = call ${host.llType(awaited)} ${entry}(${argList})`); + const out = host.own({ name: t, type: awaited }); + host.emitPendingCheck(); + return out; + } // Async callee: the spawn wrapper runs the body eagerly to its // first suspension and returns the promise (+1). The call itself // never unwinds — rejections surface at await (computeMayThrow's diff --git a/packages/compiler/src/backend/mangle.ts b/packages/compiler/src/backend/mangle.ts index d23e4efd5..e2fc58146 100644 --- a/packages/compiler/src/backend/mangle.ts +++ b/packages/compiler/src/backend/mangle.ts @@ -166,6 +166,11 @@ export function mangleGlobal(globalId: string): string { export function mangleAsyncSpawn(fnName: string): string { return `sc_as_${sanitize(fnName)}`; } +/** `await f(...)` of a direct async call: the call and its await as one + * entry that returns the awaited value. */ +export function mangleAsyncAwaitCall(fnName: string): string { + return `sc_aw_${sanitize(fnName)}`; +} export function mangleTrampoline(fnName: string): string { return `sc_tr_${sanitize(fnName)}`; } diff --git a/packages/compiler/test/await-call-emission.test.ts b/packages/compiler/test/await-call-emission.test.ts new file mode 100644 index 000000000..4051f6d5c --- /dev/null +++ b/packages/compiler/test/await-call-emission.test.ts @@ -0,0 +1,73 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +const directories: string[] = []; +afterEach(async () => { + await Promise.all(directories.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +async function emit(source: string): Promise { + const outDir = await mkdtemp(join(tmpdir(), "scriptc-await-call-")); + directories.push(outDir); + const entry = join(outDir, "main.ts"); + await writeFile(entry, source); + const outPath = join(outDir, "program.ll"); + const result = await compile(entry, { entry, outDir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result)).toBe(true); + return readFile(outPath, "utf8"); +} + +function definition(llvm: string, symbol: string): string { + const match = new RegExp(`^define internal [^\\n]*@${symbol}\\([^]*?^}`, "m").exec(llvm); + expect(match, symbol).not.toBeNull(); + return match![0]; +} + +const source = ` +const cache = new Map(); +async function leaf(x: number): Promise { + const hit = cache.get(x); + return hit ?? x + 1; +} +async function middle(x: number): Promise { + const y = await leaf(x); + return y * 2; +} +async function main(): Promise { + const pending = leaf(1); + let sum = await middle(2); + sum += await pending; + console.log(sum); +} +void main(); +`; + +test("awaiting a direct call to a body that never suspends skips the promise", async () => { + const llvm = await emit(source); + expect(definition(llvm, "sc_f_middle")).toContain("call double @sc_aw_leaf(double"); + const entry = definition(llvm, "sc_aw_leaf"); + expect(entry).toContain("call double @sc_f_leaf(double %a0)"); + expect(entry).toContain("call void @scr_async_inline_exit(ptr %frame)"); + // The clean result takes the await's microtask turn and returns directly; + // only the throw path builds the rejected promise. + const clean = entry.slice(entry.indexOf("clean:"), entry.indexOf("thrown:")); + expect(clean).toContain("call void @scr_await_hop()"); + expect(clean).not.toContain("@scr_promise_new"); + expect(entry.slice(entry.indexOf("thrown:"))).toContain("call void @scr_promise_reject_pending"); +}); + +test("awaiting a direct call to a suspending body spawns and awaits in one call", async () => { + const llvm = await emit(source); + const main = definition(llvm, "sc_f_main"); + expect(main).toContain("call double @sc_aw_middle(double"); + // A call whose promise is kept keeps the promise-returning entry. + expect(main).toContain("call ptr @sc_as_leaf(double"); + expect(definition(llvm, "sc_aw_middle")).toContain( + "call double @scr_async_spawn_await_f64(ptr @sc_tr_middle, ptr %ap)", + ); + // middle awaits, so it keeps its fiber. + expect(definition(llvm, "sc_as_middle")).toContain("@scr_async_spawn("); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4880-await-call-turn-order.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4880-await-call-turn-order.ts.json new file mode 100644 index 000000000..f1ce33b19 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4880-await-call-turn-order.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4880-await-call-turn-order.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4881-await-call-exceptions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4881-await-call-exceptions.ts.json new file mode 100644 index 000000000..38a16af52 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4881-await-call-exceptions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4881-await-call-exceptions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4882-await-call-request-chain.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4882-await-call-request-chain.ts.json new file mode 100644 index 000000000..b9888c118 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4882-await-call-request-chain.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4882-await-call-request-chain.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4883-await-values-and-hops.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4883-await-values-and-hops.ts.json new file mode 100644 index 000000000..f9a318b5c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4883-await-values-and-hops.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4883-await-values-and-hops.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4884-await-call-unhandled-sibling.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4884-await-call-unhandled-sibling.ts.json new file mode 100644 index 000000000..91f046058 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4884-await-call-unhandled-sibling.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4884-await-call-unhandled-sibling.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4885-await-call-checkpoints.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4885-await-call-checkpoints.ts.json new file mode 100644 index 000000000..c9614b2bb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4885-await-call-checkpoints.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4885-await-call-checkpoints.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index e1606a2e7..c75877fd3 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -163,6 +163,8 @@ typedef struct { size_t rc; } ScrPromiseIdentity; struct ScrPromise { size_t rc; /* The cycle collector reads the reference count at offset zero. */ + /* Shared identity cell for promises adopted across inner types (NULL + * until scr_promise_share_identity first links this promise). */ ScrPromiseIdentity *identity; int state; bool top_level_handled; /* uncaughtException handled entry evaluation */ @@ -178,8 +180,9 @@ struct ScrPromise { void (*release_fn)(void *); ScrTraceFn trace_fn; /* Fibers parked on this promise. */ - ScrFiber **waiters; + ScrFiber **waiters; /* waiter_inline until a second fiber parks */ size_t nwaiters, waiters_cap; + ScrFiber *waiter_inline; /* Combinator callbacks (Promise.race / Promise.all): at settle, * fulfilled promises run adapt(dst, this) — an emitted adapter * converting the payload into the destination's inner type — and @@ -235,6 +238,37 @@ static void scr_oom(void) { abort(); } +static void scr_promise_free_side(ScrPromise *p) { + if (p->identity && --p->identity->rc == 0) free(p->identity); + if (p->waiters != &p->waiter_inline) free(p->waiters); +} + +static void scr_promise_add_waiter(ScrPromise *p, ScrFiber *f) { + if (p->nwaiters == p->waiters_cap) { + if (p->waiters_cap == 0) { + p->waiters = &p->waiter_inline; + p->waiters_cap = 1; + } else { + size_t cap = p->waiters_cap < 4 ? 4 : p->waiters_cap * 2; + ScrFiber **grown = p->waiters == &p->waiter_inline + ? malloc(cap * sizeof *grown) + : realloc(p->waiters, cap * sizeof *grown); + if (!grown) scr_oom(); + if (p->waiters == &p->waiter_inline) grown[0] = p->waiter_inline; + p->waiters = grown; + p->waiters_cap = cap; + } + } + p->waiters[p->nwaiters++] = f; +} + +static bool scr_promise_has_traced_children(const ScrPromise *p) { + return p->ncbs != 0 || + ((p->payload_kind == SCR_EXC_REF || p->payload_kind == SCR_EXC_OBJ || + p->payload_kind == SCR_EXC_PRIMITIVE_REF) && + p->trace_fn && p->payload); +} + static void scr_promise_trace(void *o, ScrTraceVisit visit, void *ctx) { ScrPromise *p = (ScrPromise *)o; /* Waiters are fibers (not refcounted objects) — the settled payload and @@ -266,8 +300,7 @@ static void scr_promise_gcfree(void *o) { for (size_t i = 0; i < p->ncbs; i++) { if (p->cbs[i].all) scr_promise_all_state_release(p->cbs[i].all); } - if (--p->identity->rc == 0) free(p->identity); - free(p->waiters); + scr_promise_free_side(p); free(p->cbs); #ifdef SCR_RC_AUDIT scr_live_promises--; @@ -279,9 +312,6 @@ ScrPromise *scr_promise_new(void) { static const ScrCycType cyc_type = { &scr_promise_trace, &scr_promise_gcfree, SCR_CYC_KIND_PROMISE }; ScrPromise *p = scr_cyc_alloc(sizeof *p, &cyc_type); p->rc = 1; - p->identity = malloc(sizeof *p->identity); - if (!p->identity) scr_oom(); - p->identity->rc = 1; #ifdef SCR_RC_AUDIT scr_live_promises++; #endif @@ -289,14 +319,20 @@ ScrPromise *scr_promise_new(void) { } void scr_promise_share_identity(ScrPromise *dst, ScrPromise *src) { + if (dst == src) return; + if (!src->identity) { + src->identity = malloc(sizeof *src->identity); + if (!src->identity) scr_oom(); + src->identity->rc = 1; + } if (dst->identity == src->identity) return; - if (--dst->identity->rc == 0) free(dst->identity); + if (dst->identity && --dst->identity->rc == 0) free(dst->identity); dst->identity = src->identity; dst->identity->rc++; } bool scr_promise_identity_equal(ScrPromise *a, ScrPromise *b) { - return a->identity == b->identity; + return a == b || (a->identity != NULL && a->identity == b->identity); } /* The 'rejectionHandled' hook (scr_async_dyn.c installs it at listener @@ -352,8 +388,7 @@ void scr_promise_release(ScrPromise *p) { if (--p->rc == 0) { scr_cyc_dead(p); scr_promise_release_payload(p); - if (--p->identity->rc == 0) free(p->identity); - free(p->waiters); + scr_promise_free_side(p); for (size_t i = 0; i < p->ncbs; i++) { scr_promise_release(p->cbs[i].dst); if (p->cbs[i].all) scr_promise_all_state_release(p->cbs[i].all); @@ -363,9 +398,14 @@ void scr_promise_release(ScrPromise *p) { scr_live_promises--; #endif scr_cyc_free(p); - } else { + } else if (scr_promise_has_traced_children(p)) { scr_cyc_released(p); /* possible cycle root; may collect — p is done */ } + /* A promise the trace walks nothing from cannot be on a cycle at this + * decrement, so it is not a candidate root. If it later gains traced + * children (a cycle-capable payload or a combinator destination) while + * still referenced, the decrement that could strand a cycle through it + * sees them and buffers it then. */ } void *scr_promise_retain_v(void *p) { return scr_promise_retain((ScrPromise *)p); } @@ -557,6 +597,14 @@ struct ScrFiber { /* Async-generator settlement jobs: stackless READY-queue envelopes. * The retained generator is pumped on the main stack. */ ScrGen *gen_job; + /* Set during the eager prefix of a fiber spawned by an await-call entry + * (scr_async_spawn_await_*): the spawner that awaits this fiber's promise + * as soon as the spawn returns. The first suspension parks it on the + * promise right away (scr_fiber_park_awaiters) instead of switching back + * just so it can park itself. */ + struct ScrFiber *awaiter; + /* This fiber was parked by such a child; set before the switch away. */ + bool await_parked; #ifdef SCR_ASAN_FIBERS void *fake_stack; #endif @@ -599,6 +647,8 @@ void scr_als_ctx_release(ScrAlsCtx *c) { static SCR_TL ScrCtx scr_loop_ctx; /* the main stack (scheduler home) */ static SCR_TL ScrFiber *scr_current = NULL; +/* A finished fiber whose last switch went to the loop (scr_resume_fiber). */ +static SCR_TL ScrFiber *scr_loop_reap = NULL; static SCR_TL long scr_fibers_live = 0; static SCR_TL long scr_fibers_abandoned = 0; @@ -1548,6 +1598,10 @@ static void scr_trampoline(void) { ScrFiber *self = scr_current; self->entry(self, self->argpack); scr_fiber_finish(self); + /* The loop reaps the fiber that comes back to it finished, which need + * not be the one it resumed: a fiber that continued in place for the + * awaiters it parked (scr_await_yield) returns to the loop too. */ + if (self->gen == NULL && self->return_to == &scr_loop_ctx) scr_loop_reap = self; /* Dead fiber: hop back to whoever resumed us. The loop frees us. */ scr_switch(&self->ctx, self->return_to, NULL); /* unreachable */ @@ -1626,13 +1680,24 @@ static void scr_fiber_destroy(ScrFiber *f) { #else free(f->stack); #endif - free(f); + scr_mem_free(f); } +#if !defined(_WIN32) && !defined(__wasi__) && !defined(SCR_WORKERS) +#define SCR_SPAWN_AWAIT 1 +#endif + /* Spawns and EAGERLY runs an async body until its first suspension (JS's - * synchronous-prefix rule). Returns the fiber's promise, +1. */ -ScrPromise *scr_async_spawn(void (*entry)(ScrFiber *, void *), void *argpack) { - ScrFiber *f = calloc(1, sizeof *f); + * synchronous-prefix rule). Returns the fiber's promise, +1. A non-NULL + * `awaiter` (the current fiber, SCR_SPAWN_AWAIT only) awaits that promise + * immediately: the child saves into and returns to the awaiter's own + * context, so a child suspension can park the awaiter and switch past + * it (scr_fiber_park_awaiters). The promise is retained before the child + * runs, as the child may then finish and be reaped by the loop before the + * awaiter resumes. */ +static ScrPromise *scr_async_spawn_impl(void (*entry)(ScrFiber *, void *), void *argpack, + ScrFiber *awaiter) { + ScrFiber *f = scr_mem_calloc(sizeof *f); if (!f) scr_oom(); scr_fiber_track(f); f->promise = scr_promise_new(); @@ -1651,6 +1716,7 @@ ScrPromise *scr_async_spawn(void (*entry)(ScrFiber *, void *), void *argpack) { f->ctx = CreateFiberEx(4096, 0, 0, scr_trampoline, NULL); if (f->ctx == NULL) scr_oom(); #elif defined(__wasi__) + (void)awaiter; ScrFiber *spawner = scr_current; scr_current = f; scr_exc_swap_cell(&f->exc); @@ -1672,9 +1738,21 @@ ScrPromise *scr_async_spawn(void (*entry)(ScrFiber *, void *), void *argpack) { ScrCtx here; #endif #ifndef __wasi__ - f->return_to = &here; + ScrCtx *home = &here; + ScrPromise *awaited = NULL; +#ifdef SCR_SPAWN_AWAIT + if (awaiter != NULL) { + f->awaiter = awaiter; + awaiter->await_parked = false; + home = &awaiter->ctx; + awaited = scr_promise_retain(f->promise); + } +#else + (void)awaiter; +#endif + f->return_to = home; ScrFiber *spawner = scr_current; - scr_switch(&here, &f->ctx, f); + scr_switch(home, &f->ctx, f); /* back: the fiber suspended or finished. The switch back targeted NULL * (the child doesn't know who spawned it), which pointed both the current * fiber AND the exception machinery at main — restore the spawner's cell @@ -1684,7 +1762,12 @@ ScrPromise *scr_async_spawn(void (*entry)(ScrFiber *, void *), void *argpack) { scr_current = spawner; scr_exc_swap_cell(spawner ? &spawner->exc : NULL); scr_als_active = spawner ? &spawner->als : &scr_als_main_slot; - ScrPromise *result = scr_promise_retain(f->promise); + if (awaited != NULL) { + /* Parked by the child: the loop owns (and may have reaped) it. */ + if (awaiter->await_parked) return awaited; + f->awaiter = NULL; + } + ScrPromise *result = awaited != NULL ? awaited : scr_promise_retain(f->promise); if (f->done) { scr_promise_release(f->promise); scr_fiber_destroy(f); @@ -1694,6 +1777,81 @@ ScrPromise *scr_async_spawn(void (*entry)(ScrFiber *, void *), void *argpack) { #endif } +ScrPromise *scr_async_spawn(void (*entry)(ScrFiber *, void *), void *argpack) { + return scr_async_spawn_impl(entry, argpack, NULL); +} + +static void scr_await_park(ScrPromise *p); +static void scr_promise_rethrow(ScrPromise *p); + +/* `await f(...)` of a fiber-backed async f in one call (the compiler's + * await-call entry): spawn, then await the promise, with the same turns + * as the two steps. When the child suspends in its eager prefix, the + * awaiter is already parked on the pending promise, so the settle wake + * resumes it with the result and no further hop; a child that finished + * eagerly leaves a settled promise, which the ordinary await hops on. + * Returns the promise +1; `ok` false means the rejection was re-thrown. */ + +static ScrPromise *scr_async_spawn_await(void (*entry)(ScrFiber *, void *), void *argpack, + bool *ok) { +#ifdef SCR_SPAWN_AWAIT + ScrFiber *self = scr_current; + if (self != NULL && self->gen == NULL && self->inline_depth == 0) { + ScrPromise *p = scr_async_spawn_impl(entry, argpack, self); + if (self->await_parked) { + self->await_parked = false; + while (p->state == SCR_PROM_PENDING) scr_await_park(p); + scr_prom_observe(p); + if (p->state == SCR_PROM_REJECTED) { + scr_promise_rethrow(p); + *ok = false; + } else { + *ok = true; + } + return p; + } + *ok = scr_await_settled(p); + return p; + } +#endif + ScrPromise *p = scr_async_spawn(entry, argpack); + *ok = scr_await_settled(p); + return p; +} + +void scr_async_spawn_await_void(void (*entry)(ScrFiber *, void *), void *argpack) { + bool ok; + scr_promise_release(scr_async_spawn_await(entry, argpack, &ok)); +} +double scr_async_spawn_await_f64(void (*entry)(ScrFiber *, void *), void *argpack) { + bool ok; + ScrPromise *p = scr_async_spawn_await(entry, argpack, &ok); + double v = ok ? p->f64 : 0; + scr_promise_release(p); + return v; +} +bool scr_async_spawn_await_bool(void (*entry)(ScrFiber *, void *), void *argpack) { + bool ok; + ScrPromise *p = scr_async_spawn_await(entry, argpack, &ok); + bool v = ok ? p->b : false; + scr_promise_release(p); + return v; +} +ScrStr *scr_async_spawn_await_str(void (*entry)(ScrFiber *, void *), void *argpack) { + bool ok; + ScrPromise *p = scr_async_spawn_await(entry, argpack, &ok); + ScrStr *v = ok && p->payload ? scr_str_retain((ScrStr *)p->payload) : NULL; + scr_promise_release(p); + return v; +} +void *scr_async_spawn_await_ref(void (*entry)(ScrFiber *, void *), void *argpack) { + bool ok; + ScrPromise *p = scr_async_spawn_await(entry, argpack, &ok); + void *v = ok && p->payload ? p->retain_fn(p->payload) : NULL; + scr_promise_release(p); + return v; +} + /* ── fiberless async calls ───────────────────────────────────────────── * An async function whose body the compiler proved can never park its * execution context (no await, no other suspending runtime call, directly @@ -1721,7 +1879,7 @@ void scr_async_inline_enter(ScrAsyncInline *frame) { if (scr_current != NULL) scr_current->inline_depth++; } -ScrPromise *scr_async_inline_leave(ScrAsyncInline *frame) { +void scr_async_inline_exit(ScrAsyncInline *frame) { if (scr_current != NULL) scr_current->inline_depth--; scr_exc_current_cell()->stack = (ScrStackFrame *)frame->stack; ScrAlsCtx *saved = (ScrAlsCtx *)frame->als; @@ -1732,6 +1890,10 @@ ScrPromise *scr_async_inline_leave(ScrAsyncInline *frame) { } else { scr_als_ctx_release(saved); } +} + +ScrPromise *scr_async_inline_leave(ScrAsyncInline *frame) { + scr_async_inline_exit(frame); return scr_promise_new(); } @@ -1747,7 +1909,7 @@ ScrPromise *scr_async_spawn_after(ScrPromise *dependency, (void)dependency; return scr_async_spawn(entry, argpack); #else - ScrFiber *f = calloc(1, sizeof *f); + ScrFiber *f = scr_mem_calloc(sizeof *f); if (!f) scr_oom(); f->promise = scr_promise_new(); f->entry = entry; @@ -1756,13 +1918,7 @@ ScrPromise *scr_async_spawn_after(ScrPromise *dependency, scr_fibers_live++; if (dependency->state == SCR_PROM_PENDING) { - if (dependency->nwaiters == dependency->waiters_cap) { - dependency->waiters_cap = dependency->waiters_cap ? dependency->waiters_cap * 2 : 4; - dependency->waiters = realloc( - dependency->waiters, dependency->waiters_cap * sizeof *dependency->waiters); - if (!dependency->waiters) scr_oom(); - } - dependency->waiters[dependency->nwaiters++] = f; + scr_promise_add_waiter(dependency, f); } else { scr_ready_push(f); } @@ -1770,6 +1926,29 @@ ScrPromise *scr_async_spawn_after(ScrPromise *dependency, #endif } +/* Where a suspending async fiber switches to. During the eager prefix + * under an await-call entry (awaiter set), control would return to the + * awaiter only for it to park on this fiber's still-pending promise and + * switch to its own resumer: park it here, for each such awaiter up the + * chain, and switch there directly. Nothing runs in between either way. */ +static ScrCtx *scr_fiber_park_awaiters(ScrFiber *self) { + ScrCtx *to = self->return_to; +#ifdef SCR_SPAWN_AWAIT + for (ScrFiber *cur = self; cur->awaiter != NULL;) { + ScrFiber *aw = cur->awaiter; + cur->awaiter = NULL; + scr_promise_add_waiter(cur->promise, aw); + aw->await_parked = true; + to = aw->return_to; + cur = aw; + } + /* Whoever resumes this fiber next (the loop) becomes its return target; + * also right when the caller continues in place without switching. */ + self->return_to = to; +#endif + return to; +} + /* Parks the current fiber on `p` until it settles. Only fibers await. */ static void scr_await_park(ScrPromise *p) { ScrFiber *self = scr_current; @@ -1778,16 +1957,11 @@ static void scr_await_park(ScrPromise *p) { abort(); } if (self->inline_depth != 0) scr_inline_park_trap(); - if (p->nwaiters == p->waiters_cap) { - p->waiters_cap = p->waiters_cap ? p->waiters_cap * 2 : 4; - p->waiters = realloc(p->waiters, p->waiters_cap * sizeof *p->waiters); - if (!p->waiters) scr_oom(); - } - p->waiters[p->nwaiters++] = self; + scr_promise_add_waiter(p, self); #ifdef SCR_WORKERS self->waiting = p; #endif - scr_switch(&self->ctx, self->return_to, NULL); + scr_switch(&self->ctx, scr_fiber_park_awaiters(self), NULL); #ifdef SCR_WORKERS /* Shutdown resumes suspended stacks without settling their dependencies. * Remove the borrowed waiter before the stack releases its promise. */ @@ -1809,12 +1983,7 @@ static void scr_await_park(ScrPromise *p) { * the continuation through their waiter list. */ void scr_wasi_await_prepare(ScrFiber *self, ScrPromise *p) { if (p->state == SCR_PROM_PENDING) { - if (p->nwaiters == p->waiters_cap) { - p->waiters_cap = p->waiters_cap ? p->waiters_cap * 2 : 4; - p->waiters = realloc(p->waiters, p->waiters_cap * sizeof *p->waiters); - if (!p->waiters) scr_oom(); - } - p->waiters[p->nwaiters++] = self; + scr_promise_add_waiter(p, self); } else { scr_ready_push(self); } @@ -1851,8 +2020,22 @@ static void scr_await_yield(void) { abort(); } if (self->inline_depth != 0) scr_inline_park_trap(); + if (self->gen != NULL) { + scr_ready_push(self); + scr_switch(&self->ctx, self->return_to, NULL); + return; + } + /* The hop would return control to the loop's microtask drain with + * nothing else queued (this async fiber, or the awaiters parked for it, + * were resumed by the drain), so this fiber would be the very next job: + * the drain runs to exhaustion with no checkpoint in between. Continuing + * in place is the same turn order without the two switches. A fiber + * resumed by its spawner (the eager prefix) must still hop: the spawner + * has code to run first. */ + ScrCtx *to = scr_fiber_park_awaiters(self); + if (to == &scr_loop_ctx && scr_ready_len == 0) return; scr_ready_push(self); - scr_switch(&self->ctx, self->return_to, NULL); + scr_switch(&self->ctx, to, NULL); } /* The emitted promise-or-absent await's unit arm (`await u` where u holds @@ -2782,7 +2965,7 @@ ScrDyn *scr_set_immediate_dyn_value(void) { * main stack; a throw is an uncaught exception (Node's queueMicrotask), * never a rejection. */ void scr_queue_microtask(ScrClosure *cb) { - ScrFiber *f = calloc(1, sizeof *f); + ScrFiber *f = scr_mem_calloc(sizeof *f); if (!f) scr_oom(); f->micro_cb = cb; /* ownership moves in */ scr_ready_push(f); @@ -2840,7 +3023,7 @@ void scr_loop_set_events(bool (*pending)(void), bool (*watching)(void), static void scr_resume_fiber(ScrFiber *f) { if (f->gen_job != NULL) { ScrGen *g = f->gen_job; - free(f); + scr_mem_free(f); scr_async_gen_job(g); scr_gen_release(g); return; @@ -2851,7 +3034,7 @@ static void scr_resume_fiber(ScrFiber *f) { * semantics (uncaughtException, never an unhandled rejection). */ if (f->micro_cb != NULL) { ScrClosure *cb = f->micro_cb; - free(f); + scr_mem_free(f); ((void (*)(ScrClosure *))cb->fn)(cb); scr_closure_release(cb); return; @@ -2884,6 +3067,9 @@ static void scr_resume_fiber(ScrFiber *f) { scr_als_active = &scr_als_main_slot; #else scr_switch(&scr_loop_ctx, &f->ctx, f); + f = scr_loop_reap; + if (f == NULL) return; + scr_loop_reap = NULL; #endif if (f->done) { scr_promise_release(f->promise); @@ -3868,7 +4054,7 @@ static ScrGen *scr_gen_new_common(void (*entry)(ScrFiber *, void *), void *argpa g->settle_async = settle_async; scr_obj_alloc_note(); - ScrFiber *f = calloc(1, sizeof *f); + ScrFiber *f = scr_mem_calloc(sizeof *f); if (!f) scr_oom(); f->gen = g; scr_fiber_track(f); @@ -4273,7 +4459,7 @@ static void scr_async_gen_start_queued(ScrGen *g); static void scr_async_gen_schedule_settle(ScrGen *g) { if (g->settle_queued) return; - ScrFiber *job = calloc(1, sizeof *job); + ScrFiber *job = scr_mem_calloc(sizeof *job); if (!job) scr_oom(); job->gen_job = scr_gen_retain(g); g->settle_queued = true; @@ -4485,12 +4671,12 @@ static void scr_context_ready_discard(void) { scr_ready_len--; if (fiber->micro_cb) { scr_closure_release(fiber->micro_cb); - free(fiber); + scr_mem_free(fiber); } else if (fiber->gen_job) { ScrGen *generator = fiber->gen_job; generator->settle_queued = false; scr_gen_release(generator); - free(fiber); + scr_mem_free(fiber); } /* Real fibers belong to the context registry, including queued ones. */ } diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 176c2be65..695dd2c5f 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -5351,6 +5351,13 @@ void scr_promise_release_v(void *p); typedef struct ScrFiber ScrFiber; /* Spawn + run eagerly to the first suspension; returns the promise, +1. */ ScrPromise *scr_async_spawn(void (*entry)(ScrFiber *, void *), void *argpack); +/* `await f(...)` of a fiber-backed async f: spawn and await in one call, + * returning the awaited value (a rejection re-throws into the caller). */ +void scr_async_spawn_await_void(void (*entry)(ScrFiber *, void *), void *argpack); +double scr_async_spawn_await_f64(void (*entry)(ScrFiber *, void *), void *argpack); +bool scr_async_spawn_await_bool(void (*entry)(ScrFiber *, void *), void *argpack); +ScrStr *scr_async_spawn_await_str(void (*entry)(ScrFiber *, void *), void *argpack); +void *scr_async_spawn_await_ref(void (*entry)(ScrFiber *, void *), void *argpack); /* Runtime-authored C waiters cannot themselves become LLVM switched * coroutines on wasm32. Spawn after `dependency` settles so their first * (and only) await is extraction from an already-settled promise, while @@ -5370,6 +5377,10 @@ typedef struct ScrAsyncInline { } ScrAsyncInline; void scr_async_inline_enter(ScrAsyncInline *frame); ScrPromise *scr_async_inline_leave(ScrAsyncInline *frame); +/* Leaves the frame without creating the call's promise: `await f()` of a + * fiberless f consumes the result directly (the emitted await-call entry + * still takes the await's microtask turn). */ +void scr_async_inline_exit(ScrAsyncInline *frame); /* wasm32-wasi lowers async functions and generators through LLVM switched * coroutines instead of the native stack-switching implementations. The diff --git a/tests/corpus/4880-await-call-turn-order.ts b/tests/corpus/4880-await-call-turn-order.ts new file mode 100644 index 000000000..e183c4596 --- /dev/null +++ b/tests/corpus/4880-await-call-turn-order.ts @@ -0,0 +1,88 @@ +// Directly awaited async calls (bodies that never suspend, bodies that do, +// and chains of them) keep Node's microtask order against then-callbacks, +// queueMicrotask, and settled and pending promises. +const log: string[] = []; +const cache = new Map(); + +async function hit(key: number): Promise { + const v = cache.get(key); + if (v !== undefined) return v; + cache.set(key, key * 10); + return key * 10; +} +async function flag(n: number): Promise { + return n % 2 === 0; +} +async function label(n: number): Promise { + return "L" + n; +} +async function nothing(n: number): Promise { + log.push("nothing " + n); +} +async function suspends(n: number): Promise { + log.push("suspends enter " + n); + const a = await hit(n); + log.push("suspends mid " + n); + const b = await hit(n + 1); + return a + b; +} +async function chain(n: number): Promise { + log.push("chain enter " + n); + const v = await suspends(n); + log.push("chain got " + n + " " + v); + return v + 1; +} +async function deep(n: number): Promise { + return (await chain(n)) * 2; +} + +async function worker(name: string, n: number): Promise { + log.push(name + " start"); + const a = await hit(n); + log.push(name + " a=" + a); + const f = await flag(n); + log.push(name + " flag=" + f); + const s = await label(n); + log.push(name + " label=" + s); + await nothing(n); + log.push(name + " after nothing"); + const d = await deep(n); + log.push(name + " deep=" + d); + return a + d; +} + +async function main(): Promise { + const settled = Promise.resolve(7); + const pending = new Promise((resolve) => { + setTimeout(() => { + log.push("timer resolves"); + resolve(9); + }, 0); + }); + settled.then((v) => log.push("then settled " + v)); + queueMicrotask(() => log.push("microtask 1")); + const w1 = worker("w1", 1); + const w2 = worker("w2", 2); + queueMicrotask(() => log.push("microtask 2")); + Promise.resolve() + .then(() => log.push("then A1")) + .then(() => log.push("then A2")) + .then(() => log.push("then A3")) + .then(() => log.push("then A4")); + log.push("main sync end"); + const x = await settled; + log.push("main settled " + x); + const t = await (async (): Promise => await hit(5))(); + log.push("main arrow " + t); + const both: number[] = await Promise.all([w1, w2]); + log.push("workers " + both[0] + " " + both[1]); + const p = await pending; + log.push("main pending " + p); + const r = await deep(3); + log.push("main deep " + r); +} + +main().then(() => { + for (const line of log) console.log(line); +}); +console.log("top-level sync end"); diff --git a/tests/corpus/4881-await-call-exceptions.ts b/tests/corpus/4881-await-call-exceptions.ts new file mode 100644 index 000000000..52574b97f --- /dev/null +++ b/tests/corpus/4881-await-call-exceptions.ts @@ -0,0 +1,82 @@ +// Exceptions thrown by directly awaited async calls, before and after the +// callee's first await, reach the awaiter's catch on Node's turn, and +// finally blocks and rethrows keep their order against other microtasks. +const log: string[] = []; + +async function failsNow(n: number): Promise { + if (n > 0) throw new Error("now " + n); + return n; +} +async function failsString(n: number): Promise { + if (n > 0) throw "plain " + n; + return "ok"; +} +async function failsLater(n: number): Promise { + log.push("later enter " + n); + await failsNow(0); + log.push("later resumed " + n); + throw new Error("later " + n); +} +async function failsVoid(): Promise { + throw new RangeError("void failure"); +} +async function rethrows(n: number): Promise { + try { + return await failsLater(n); + } catch (e) { + log.push("rethrows caught " + (e as Error).message); + throw new Error("wrapped " + (e as Error).message); + } finally { + log.push("rethrows finally " + n); + } +} + +async function probe(name: string): Promise { + try { + await failsNow(1); + } catch (e) { + log.push(name + " caught " + (e as Error).message); + } + try { + await failsString(2); + } catch (e) { + log.push(name + " caught " + String(e)); + } + try { + await failsVoid(); + } catch (e) { + log.push(name + " caught " + (e as Error).name + " " + (e as Error).message); + } + try { + await rethrows(3); + } catch (e) { + log.push(name + " caught " + (e as Error).message); + } + const ok = await failsNow(0); + log.push(name + " ok " + ok); +} + +async function main(): Promise { + queueMicrotask(() => log.push("microtask before")); + const a = probe("a"); + const b = probe("b"); + Promise.resolve() + .then(() => log.push("tick 1")) + .then(() => log.push("tick 2")) + .then(() => log.push("tick 3")); + await a; + await b; + let caught = ""; + try { + await (async (): Promise => { + throw new Error("inline arrow"); + })(); + } catch (e) { + caught = (e as Error).message; + } + log.push("arrow " + caught); +} + +main().then(() => { + for (const line of log) console.log(line); +}); diff --git a/tests/corpus/4882-await-call-request-chain.ts b/tests/corpus/4882-await-call-request-chain.ts new file mode 100644 index 000000000..4148c0935 --- /dev/null +++ b/tests/corpus/4882-await-call-request-chain.ts @@ -0,0 +1,53 @@ +// A request handler chain of small async helpers that usually complete +// without suspending (cache hits), awaited directly, with concurrent +// requests interleaving on the microtask queue. +interface Session { + user: string; + role: string; +} +const sessions = new Map(); +const quotas = new Map(); +const order: string[] = []; + +async function lookupSession(token: string): Promise { + return sessions.get(token); +} +async function loadSession(token: string): Promise { + const hit = await lookupSession(token); + if (hit !== undefined) return hit; + await new Promise((resolve) => setTimeout(resolve, 0)); + const fresh = { user: "u-" + token, role: token.length % 2 === 0 ? "admin" : "user" }; + sessions.set(token, fresh); + return fresh; +} +async function canWrite(session: Session): Promise { + return session.role === "admin"; +} +async function quota(user: string): Promise { + const q = quotas.get(user) ?? 3; + quotas.set(user, q - 1); + return q; +} +async function render(status: number, body: string): Promise { + return status + " " + body; +} +async function handle(id: number, token: string): Promise { + order.push("start " + id); + const session = await loadSession(token); + if (!(await canWrite(session))) return await render(403, session.user); + const left = await quota(session.user); + if (left <= 0) return await render(429, session.user); + order.push("write " + id); + return await render(200, session.user + " left " + left); +} + +async function main(): Promise { + const tokens = ["ab", "abc", "ab", "abcd", "ab", "abc", "abcd", "ab", "ab", "ab"]; + const first = await Promise.all(tokens.map((t, i) => handle(i, t))); + for (const r of first) console.log(r); + const second: string[] = []; + for (let i = 0; i < tokens.length; i++) second.push(await handle(100 + i, tokens[i]!)); + for (const r of second) console.log(r); + console.log(order.join(",")); +} +void main(); diff --git a/tests/corpus/4883-await-values-and-hops.ts b/tests/corpus/4883-await-values-and-hops.ts new file mode 100644 index 000000000..4d12ccbd5 --- /dev/null +++ b/tests/corpus/4883-await-values-and-hops.ts @@ -0,0 +1,32 @@ +// await of non-promise values and of directly called async functions each +// take exactly one microtask turn, interleaved with two counters. +const log: string[] = []; +async function one(): Promise { + return 1; +} +async function ticker(name: string, n: number): Promise { + for (let i = 0; i < n; i++) { + log.push(name + i); + await null; + } +} +async function mixed(): Promise { + log.push("m0"); + const a = await 5; + log.push("m1 " + a); + const b = await one(); + log.push("m2 " + b); + const c = await "text"; + log.push("m3 " + c); + const d = await undefined; + log.push("m4 " + d); + const e = await one(); + log.push("m5 " + e); + const f = await Promise.resolve(await one()); + log.push("m6 " + f); +} +async function main(): Promise { + await Promise.all([ticker("t", 10), mixed(), ticker("s", 4)]); + console.log(log.join(" ")); +} +void main(); diff --git a/tests/corpus/4884-await-call-unhandled-sibling.ts b/tests/corpus/4884-await-call-unhandled-sibling.ts new file mode 100644 index 000000000..330d3ce24 --- /dev/null +++ b/tests/corpus/4884-await-call-unhandled-sibling.ts @@ -0,0 +1,22 @@ +// @exit: 1 +// A rejection from a directly awaited async call is handled by that await, +// while the same function's rejected promise left unawaited still reports +// as an unhandled rejection after the microtask queue drains. +async function fails(n: number): Promise { + if (n > 0) throw new Error("fail " + n); + return n; +} +async function main(): Promise { + try { + await fails(1); + } catch (e) { + console.log("handled", (e as Error).message); + } + process.on("unhandledRejection", (reason) => { + console.log("unhandled", (reason as Error).message); + process.exitCode = 1; + }); + void fails(2); + console.log("after", await fails(0)); +} +void main(); diff --git a/tests/corpus/4885-await-call-checkpoints.ts b/tests/corpus/4885-await-call-checkpoints.ts new file mode 100644 index 000000000..269dfcd91 --- /dev/null +++ b/tests/corpus/4885-await-call-checkpoints.ts @@ -0,0 +1,40 @@ +// Directly awaited async calls against process.nextTick, setImmediate, +// a timer and queueMicrotask: ticks run before promise jobs at each +// checkpoint, and every await still waits for the jobs queued before it. +const log: string[] = []; +async function quick(n: number): Promise { + return n * 3; +} +async function once(n: number): Promise { + const v = await quick(n); + process.nextTick(() => log.push("tick from once " + n)); + return v + 1; +} +async function runner(name: string): Promise { + log.push(name + " begin"); + for (let i = 0; i < 3; i++) { + const v = await once(i); + log.push(name + " step " + i + " = " + v); + if (i === 1) { + queueMicrotask(() => log.push(name + " microtask")); + process.nextTick(() => log.push(name + " tick")); + } + } + await new Promise((resolve) => setImmediate(resolve)); + log.push(name + " after immediate " + (await quick(7))); + await new Promise((resolve) => setTimeout(resolve, 1)); + log.push(name + " after timer " + (await once(9))); +} +async function main(): Promise { + process.nextTick(() => log.push("first tick")); + setImmediate(() => log.push("immediate")); + const a = runner("a"); + const b = runner("b"); + log.push("main sync"); + await Promise.all([a, b]); + const p = quick(1); + const q = Promise.resolve(p); + log.push("same promise " + (p === q)); + log.push("value " + (await q)); +} +main().then(() => console.log(log.join("\n")));