diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index 57c60b1bc..f5ff2642e 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 23, marker: "scr_runtime_abi_v23" }, + runtime_abi: { version: 24, marker: "scr_runtime_abi_v24" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { () => null, (failure: { stderr?: string }) => failure, ); - expect(error?.stderr).toContain("scr_runtime_abi_v23"); + expect(error?.stderr).toContain("scr_runtime_abi_v24"); }); }); diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 7bbd3d453..8c6264e6c 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -35,6 +35,7 @@ import { mangleArgPack, mangleAsyncAdoptCall, mangleAsyncAwaitCall, + mangleAsyncDirectSpawn, mangleAsyncSpawn, mangleBorrowedFunction, mangleClassStruct, @@ -910,6 +911,20 @@ export function emitAsyncScaffolding(emitter: LlEmitter): string[] { for (const line of emitter.emitAsyncAwaitCall(fn, argPackLines, true)) out.push(line); } const stackless = emitter.stacklessAsync.has(fn.name); + if (stackless && emitter.directAwaitCallees.has(fn.name)) { + // A coroutine's direct await: the same prefix, with the promise kept + // inside the returned fiber record. + emitter.declare(`declare ptr @scr_async_spawn_direct(ptr, ptr)`); + out.push( + `define internal ptr @${mangleAsyncDirectSpawn(fn.name)}(${spawnParams.join(", ")}) ${FN_ATTRS} { ; direct spawn ${fn.name}`, + `entry:`, + ...argPackLines, + ` %f = call ptr @scr_async_spawn_direct(ptr @${mangleTrampoline(fn.name)}, ptr %ap)`, + ` ret ptr %f`, + `}`, + ``, + ); + } const spawn = stackless ? "scr_async_spawn_stackless" : "scr_async_spawn"; emitter.declare(`declare ptr @scr_fiber_promise(ptr)`); emitter.declare(`declare ptr @${spawn}(ptr, ptr)`); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index b306674fc..eeb89d953 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -889,6 +889,9 @@ export class LlEmitter { /** Fiberless async functions a stackless coroutine body awaits directly; * each gets an inline await entry (emitCoroInlineAwait). */ readonly inlineAwaitCallees = new Set(); + /** Stackless async functions some coroutine body awaits directly: each + * gets a direct spawn entry (emitCoroDirectAwait). */ + readonly directAwaitCallees = new Set(); /** The async functions whose bodies never suspend and that a direct * await may enter in place (selectStacklessAsync). */ private inlineAwaitable = new Set(); @@ -1913,6 +1916,69 @@ export class LlEmitter { return fn.captures === undefined; } + /** Whether a stackless coroutine body's `await f(...)` may await f's + * fiber record instead of its promise (emitCoroDirectAwait): f is a + * stackless async function whose direct call an await-call entry could + * take. The await consumes the promise, so nothing else can observe it. + * Worker programs keep the promise for their stop checkpoint. */ + directAwaitEligible(fnName: string): boolean { + if (this.wasi || this.mod.workers === true || this.currentCoro?.kind !== "async") return false; + const fn = this.fnByName.get(fnName); + if (fn === undefined || fn.async !== true || fn.generator !== undefined) return false; + if (!this.stacklessAsync.has(fnName) || fn.captures !== undefined) return false; + if (fn.returnType.kind === "dyn") return false; + return fn.asyncCacheGlobal === undefined && fn.asyncCycleCacheGlobal === undefined; + } + + /** A stackless coroutine body's `await f(...)` of a stackless f, without + * f's promise object: f's prefix runs as the spawn of a fiber record that + * keeps the promise inside. A body still running wakes this coroutine + * when it finishes, where its promise's settlement would wake its one + * waiter; a finished one takes the settled await's turn. The result (or + * the rejection, re-thrown) comes out of the record, which goes. */ + emitCoroDirectAwait(call: ExprOf<"call">, type: IrType): LlValue { + const coro = this.currentCoro; + if (coro === null) + throw new InternalCompilerError("llvm emitter bug: direct await outside a coroutine"); + const B = this.B; + const record = emitCallExpr(this, call, "direct").name; + this.declare(`declare zeroext i1 @scr_coro_direct_prepare(ptr, ptr)`); + const suspend = B.tmp(); + B.line( + `${suspend} = call zeroext i1 @scr_coro_direct_prepare(ptr ${coro.self}, ptr ${record})`, + ); + const park = B.newLabel("await.suspend"); + const ready = B.newLabel("await.ready"); + B.condBr(suspend, park, ready); + B.startBlock(park); + this.emitCoroSuspendPrepared(); + B.br(ready); + B.startBlock(ready); + if (type.kind === "void") { + this.declare(`declare void @scr_coro_direct_void(ptr)`); + B.line(`call void @scr_coro_direct_void(ptr ${record})`); + this.emitPendingCheck(); + return { name: "", type }; + } + const t = B.tmp(); + if (type.kind === "f64" || type.kind === "date") { + this.declare(`declare double @scr_coro_direct_f64(ptr)`); + B.line(`${t} = call double @scr_coro_direct_f64(ptr ${record})`); + } else if (type.kind === "bool") { + this.declare(`declare zeroext i1 @scr_coro_direct_bool(ptr)`); + B.line(`${t} = call zeroext i1 @scr_coro_direct_bool(ptr ${record})`); + } else if (type.kind === "string") { + this.declare(`declare ptr @scr_coro_direct_str(ptr)`); + B.line(`${t} = call ptr @scr_coro_direct_str(ptr ${record})`); + } else { + this.declare(`declare ptr @scr_coro_direct_ref(ptr)`); + B.line(`${t} = call ptr @scr_coro_direct_ref(ptr ${record})`); + } + const out = this.own({ name: this.adoptHeap(t, type), type }); + this.emitPendingCheck(); + return out; + } + /** The inline await entry of a fiberless async function (`sc_ai_*`): its * body on the caller's stack (between scr_async_inline_enter/exit when * fiberlessFrames), as in its spawn wrapper, returning the result and diff --git a/packages/compiler/src/backend/llvm/expr-async.ts b/packages/compiler/src/backend/llvm/expr-async.ts index 4d73d0904..b5d765b16 100644 --- a/packages/compiler/src/backend/llvm/expr-async.ts +++ b/packages/compiler/src/backend/llvm/expr-async.ts @@ -918,6 +918,22 @@ export function emitAsyncExpr( return host.emitCoroInlineAwait(e.value, e.adopt === true); } } + // A stackless coroutine body awaits a stackless callee's fiber record + // instead of its promise. + if ( + adopted === null && + e.adopt !== true && + e.value.kind === "call" && + host.directAwaitEligible(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.emitCoroDirectAwait(e.value, e.type); + } + } if (adopted === null && e.value.kind === "call" && host.awaitCallEligible(e.value.callee)) { const callee = host.fnByName.get(e.value.callee)!; if ( diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index 81c44d5a6..f0aa3b276 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -31,6 +31,7 @@ import { mangleFunction, mangleWrapper, mangleVtStruct, + mangleAsyncDirectSpawn, } from "../mangle.js"; import { classEnvironmentIndex, @@ -74,7 +75,7 @@ export function emitCallExpr( | "instanceOf" | "virtualCall" >, - awaitEntry: false | "await" | "adopt" | "inline" = false, + awaitEntry: false | "await" | "adopt" | "inline" | "direct" = false, /** A projection consumes a scalar union result (Int32Abi.union): answer * a private stack box instead of a heap box. */ stackResult = false, @@ -184,6 +185,14 @@ export function emitCallExpr( : `${host.llType(callee.params[i]!.type)} ${a.name}`, ) .join(", "); + if (awaitEntry === "direct") { + // A stackless coroutine's await of a stackless callee: the spawn + // returns the callee's fiber record (emitCoroDirectAwait). + host.directAwaitCallees.add(e.callee); + const record = B.tmp(); + B.line(`${record} = call ptr @${mangleAsyncDirectSpawn(e.callee)}(${argList})`); + return { name: record, type: callee.returnType }; + } if (awaitEntry === "inline") { // A stackless coroutine's await of a fiberless callee: the entry // runs the body in place and leaves a throw pending; the enclosing diff --git a/packages/compiler/src/backend/mangle.ts b/packages/compiler/src/backend/mangle.ts index da6cbe0f5..14736ead7 100644 --- a/packages/compiler/src/backend/mangle.ts +++ b/packages/compiler/src/backend/mangle.ts @@ -195,6 +195,11 @@ export function mangleAsyncAdoptCall(fnName: string): string { export function mangleAsyncInlineAwaitCall(fnName: string): string { return `sc_ai_${sanitize(fnName)}`; } +/** A stackless coroutine body's `await f(...)` of a stackless async f: the + * spawn of f's fiber record, which keeps its promise inside (direct await). */ +export function mangleAsyncDirectSpawn(fnName: string): string { + return `sc_dw_${sanitize(fnName)}`; +} export function mangleTrampoline(fnName: string): string { return `sc_tr_${sanitize(fnName)}`; } diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 67be86571..5eb24482b 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 23 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v23" as const; +export const RUNTIME_ABI_VERSION = 24 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v24" as const; /** Object consumption is useful today, but the complete scr_* surface may * still change before 1.0. The versioned marker prevents accidental mixing; diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index 19519e1f2..fc7ed9efa 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 23, marker: "scr_runtime_abi_v23" }, + runtime_abi: { version: 24, marker: "scr_runtime_abi_v24" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index ee2301172..5ffb4797d 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -124,7 +124,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 23, marker: "scr_runtime_abi_v23" }, + runtime_abi: { version: 24, marker: "scr_runtime_abi_v24" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/test/await-call-emission.test.ts b/packages/compiler/test/await-call-emission.test.ts index 5d2b614c3..96c039155 100644 --- a/packages/compiler/test/await-call-emission.test.ts +++ b/packages/compiler/test/await-call-emission.test.ts @@ -183,10 +183,16 @@ test("bodies that suspend only at their own awaits compile to stackless coroutin const middle = definition(llvm, "sc_f_middle"); expect(middle).toMatch(/^define internal double @sc_f_middle\(double [^)]*\) #1 /); // Each await prepares its continuation and suspends the coroutine, then - // extracts the settled result; nothing parks the stack it runs on. - expect(middle).toContain("call zeroext i1 @scr_coro_await_prepare(ptr"); - expect(middle).toContain("call double @scr_coro_await_f64(ptr"); - expect(middle).toContain("call ptr @sc_as_slow(double"); + // extracts the settled result; nothing parks the stack it runs on. A + // stackless callee is awaited through its fiber record, without its + // promise (emitCoroDirectAwait). + expect(middle).toContain("call ptr @sc_dw_slow(double"); + expect(middle).toContain("call zeroext i1 @scr_coro_direct_prepare(ptr"); + expect(middle).toContain("call double @scr_coro_direct_f64(ptr"); + expect(middle).not.toContain("@sc_as_slow("); + expect(definition(llvm, "sc_dw_slow")).toContain( + "call ptr @scr_async_spawn_direct(ptr @sc_tr_slow", + ); // A callee that never suspends runs in place; the coroutine takes the // await's turn itself and awaits a rejected promise only on a throw. expect(middle).toContain("call double @sc_ai_leaf(double"); @@ -307,3 +313,41 @@ test("a stackless body runs Promise.all of never-suspending calls in place", asy expect(reused).toContain("call ptr @sc_as_leaf(double"); expect(reused).toContain("@scr_promise_all("); }); + +const directSource = ` +async function slow(x: number): Promise { + await null; + return x + 1; +} +async function direct(x: number): Promise { + return (await slow(x)) * 2; +} +async function adopting(x: number): Promise { + return slow(x); +} +async function kept(x: number): Promise { + const pending = slow(x); + await null; + return await pending; +} +async function main(): Promise { + console.log((await direct(1)) + (await adopting(2)) + (await kept(3))); +} +void main(); +`; + +test("a stackless body awaits a stackless callee's fiber record instead of its promise", async () => { + const llvm = await emit(directSource); + const direct = definition(llvm, "sc_f_direct"); + expect(direct).toContain("call ptr @sc_dw_slow(double"); + expect(direct).toContain("call zeroext i1 @scr_coro_direct_prepare(ptr"); + expect(direct).toContain("call double @scr_coro_direct_f64(ptr"); + expect(direct).not.toContain("@scr_coro_await_prepare("); + // A returned call adopts the callee's promise, and a promise held in a + // local is a value: both keep the promise. + for (const symbol of ["sc_f_adopting", "sc_f_kept"]) { + const body = definition(llvm, symbol); + expect(body, symbol).toContain("call ptr @sc_as_slow(double"); + expect(body, symbol).not.toContain("@sc_dw_slow("); + } +}); diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index c0f2cfe84..d8f158ebc 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -378,7 +378,7 @@ done: expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"])); expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v23"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v24"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -712,7 +712,7 @@ done: .split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v23"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v24"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7104-direct-await-order.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7104-direct-await-order.ts.json new file mode 100644 index 000000000..238ee8fb1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7104-direct-await-order.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7104-direct-await-order.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7105-direct-await-context.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7105-direct-await-context.ts.json new file mode 100644 index 000000000..2ed3b6aec --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7105-direct-await-context.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7105-direct-await-context.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 856de4b86..85c5ffb00 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -337,7 +337,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 23, marker: "scr_runtime_abi_v23" }, + runtime_abi: { version: 24, marker: "scr_runtime_abi_v24" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index fa84e9c4d..b242b78aa 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -434,7 +434,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 23, marker: "scr_runtime_abi_v23" }, + runtime_abi: { version: 24, marker: "scr_runtime_abi_v24" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 12cf81f77..db1e54295 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -616,6 +616,13 @@ struct ScrFiber { struct ScrFiber *awaiter; /* This fiber was parked by such a child; set before the switch away. */ bool await_parked; + /* A stackless body spawned for a direct await (scr_async_spawn_direct): + * its promise lives in the same block (ScrDirectFiber) and nothing else + * sees it. Once the awaiting coroutine suspends it is direct_awaiter, the + * finished body wakes it in place of the promise's settlement, and the + * awaiter destroys the record when it takes the result. */ + bool direct; + struct ScrFiber *direct_awaiter; #ifdef SCR_ASAN_FIBERS void *fake_stack; /* Live stackless async fibers (scr_async_spawn_stackless) form a list @@ -2138,11 +2145,31 @@ static void scr_prefix_on_fresh_stack(ScrFiber *f, void (*entry)(ScrFiber *, voi } #endif -ScrPromise *scr_async_spawn_stackless(void (*entry)(ScrFiber *, void *), void *argpack) { - ScrFiber *f = scr_mem_calloc(sizeof *f); - if (!f) scr_oom(); +/* A directly awaited body's fiber record and its promise, one block. */ +typedef struct { + ScrFiber fiber; + ScrPromise promise; +} ScrDirectFiber; + +/* Creates a stackless body's fiber and runs its eager prefix, which + * returns at the first suspension or the end. A direct fiber's promise is + * embedded and immortal (retains and releases skip it). */ +static ScrFiber *scr_spawn_stackless_fiber(void (*entry)(ScrFiber *, void *), void *argpack, + bool direct) { + ScrFiber *f; + if (direct) { + ScrDirectFiber *block = scr_mem_calloc(sizeof *block); + if (!block) scr_oom(); + f = &block->fiber; + f->promise = &block->promise; + f->promise->rc = SIZE_MAX; + f->direct = true; + } else { + f = scr_mem_calloc(sizeof *f); + if (!f) scr_oom(); + f->promise = scr_promise_new(); + } scr_fiber_track(f); - f->promise = scr_promise_new(); f->stackless = true; f->als = scr_als_ctx_retain(*SCR_ALS_SLOT()); /* The lifetime region the eager prefix runs in, which every later step @@ -2171,6 +2198,11 @@ ScrPromise *scr_async_spawn_stackless(void (*entry)(ScrFiber *, void *), void *a scr_current = spawner; scr_exc_swap_cell(spawner ? &spawner->exc : NULL); scr_als_active = spawner ? &spawner->als : &scr_als_main_slot; + return f; +} + +ScrPromise *scr_async_spawn_stackless(void (*entry)(ScrFiber *, void *), void *argpack) { + ScrFiber *f = scr_spawn_stackless_fiber(entry, argpack, false); ScrPromise *result = scr_promise_retain(f->promise); if (f->done) { scr_promise_release(f->promise); @@ -2179,6 +2211,14 @@ ScrPromise *scr_async_spawn_stackless(void (*entry)(ScrFiber *, void *), void *a } return result; } + +/* A stackless body that a stackless coroutine awaits right away (the + * compiler's direct await): its promise never escapes, so it is embedded + * in the fiber record, which the awaiter takes the result from + * (scr_coro_direct_*) and then destroys. */ +ScrFiber *scr_async_spawn_direct(void (*entry)(ScrFiber *, void *), void *argpack) { + return scr_spawn_stackless_fiber(entry, argpack, true); +} #endif static void scr_await_park(ScrPromise *p); @@ -2447,7 +2487,28 @@ bool scr_coro_await_prepare(ScrFiber *self, ScrPromise *p) { bool scr_coro_hop_prepare(ScrFiber *self) { return scr_coro_hop_needed(self); } -void scr_coro_async_finish(ScrFiber *self) { scr_fiber_finish(self); } +/* A directly awaited body's end: its awaiter is the promise's only waiter, + * and it observes a rejection when it takes the result, so the body wakes + * it where the settlement would, without the unhandled-rejection ledger. */ +static void scr_direct_finish(ScrFiber *self) { + ScrPromise *p = self->promise; + bool stopped = false; +#ifdef SCR_WORKERS + /* A stop request rejects like the fiber epilogue does (scr_fiber_finish). */ + stopped = scr_context_checkpoint(); +#endif + if (stopped || scr_exc_pending()) scr_promise_reject_from_cell(p, &self->exc); + if (stopped) p->rejection_observed = true; + if (p->state == SCR_PROM_PENDING) + scr_trap("scriptc: internal error: directly awaited body finished unsettled\n"); + self->done = true; + if (self->direct_awaiter != NULL) scr_promise_wake_one(self->direct_awaiter); +} + +void scr_coro_async_finish(ScrFiber *self) { + if (self->direct) scr_direct_finish(self); + else scr_fiber_finish(self); +} /* Extraction after the await's suspension: the promise has settled. */ static bool scr_coro_await_settled(ScrPromise *p) { @@ -2470,6 +2531,66 @@ ScrStr *scr_coro_await_str(ScrPromise *p) { void *scr_coro_await_ref(ScrPromise *p) { return scr_coro_await_settled(p) && p->payload ? p->retain_fn(p->payload) : NULL; } + +/* A direct await of a stackless body (scr_async_spawn_direct): a body + * still running gets the coroutine as its only awaiter, which its finish + * wakes as a settlement wakes a promise's waiter; a finished body is a + * settled promise, which takes one microtask turn. */ +bool scr_coro_direct_prepare(ScrFiber *self, ScrFiber *callee) { + if (!callee->done) { + callee->direct_awaiter = self; + return true; + } + return scr_coro_hop_needed(self); +} + +/* The result of a finished direct body: its promise's settlement, read as + * scr_coro_await_* reads it (a rejection re-throws), then the record goes. */ +static inline void scr_direct_destroy(ScrFiber *f) { + /* Only a rejection reason is left: values moved out, and nothing waits + * on or shares the promise. */ + if (f->promise->payload_kind != SCR_EXC_NONE) scr_promise_release_payload(f->promise); + scr_fiber_destroy(f); + scr_fibers_live--; +} + +void scr_coro_direct_void(ScrFiber *f) { + scr_coro_await_settled(f->promise); + scr_direct_destroy(f); +} +double scr_coro_direct_f64(ScrFiber *f) { + double v = scr_coro_await_settled(f->promise) ? f->promise->f64 : 0; + scr_direct_destroy(f); + return v; +} +bool scr_coro_direct_bool(ScrFiber *f) { + bool v = scr_coro_await_settled(f->promise) ? f->promise->b : false; + scr_direct_destroy(f); + return v; +} +/* String and reference results move out of the record (+1). */ +ScrStr *scr_coro_direct_str(ScrFiber *f) { + ScrPromise *p = f->promise; + ScrStr *v = NULL; + if (scr_coro_await_settled(p)) { + v = (ScrStr *)p->payload; + p->payload = NULL; + p->payload_kind = SCR_EXC_NONE; + } + scr_direct_destroy(f); + return v; +} +void *scr_coro_direct_ref(ScrFiber *f) { + ScrPromise *p = f->promise; + void *v = NULL; + if (scr_coro_await_settled(p)) { + v = p->payload; + p->payload = NULL; + p->payload_kind = SCR_EXC_NONE; + } + scr_direct_destroy(f); + return v; +} #endif /* One microtask hop: park the current fiber on the READY queue itself and @@ -3982,7 +4103,7 @@ static void scr_resume_fiber(ScrFiber *f) { scr_loop_reap = NULL; } #endif - if (f->done) { + if (f->done && !f->direct) { scr_promise_release(f->promise); scr_fiber_destroy(f); scr_fibers_live--; diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index 041d2ea03..827c303f9 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -109,6 +109,15 @@ double scr_coro_await_f64(ScrPromise *p); bool scr_coro_await_bool(ScrPromise *p); ScrStr *scr_coro_await_str(ScrPromise *p); /* +1 */ void *scr_coro_await_ref(ScrPromise *p); /* +1 via the stored retain */ +/* A direct await of a stackless body: its promise stays inside the fiber + * record the spawn returns, and the awaiter takes the result from it. */ +ScrFiber *scr_async_spawn_direct(void (*entry)(ScrFiber *, void *), void *argpack); +bool scr_coro_direct_prepare(ScrFiber *self, ScrFiber *callee); +void scr_coro_direct_void(ScrFiber *f); +double scr_coro_direct_f64(ScrFiber *f); +bool scr_coro_direct_bool(ScrFiber *f); +ScrStr *scr_coro_direct_str(ScrFiber *f); /* +1 */ +void *scr_coro_direct_ref(ScrFiber *f); /* +1 */ double scr_await_f64(ScrPromise *p); /* rejected promises re-throw */ bool scr_await_bool(ScrPromise *p); diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index 0afb8981a..c29126a4a 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v23(void) {} +void scr_runtime_abi_v24(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index cdf1061d9..94936a8aa 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -81,7 +81,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v23(void); +void scr_runtime_abi_v24(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/tests/corpus/7104-direct-await-order.ts b/tests/corpus/7104-direct-await-order.ts new file mode 100644 index 000000000..ab4f438c4 --- /dev/null +++ b/tests/corpus/7104-direct-await-order.ts @@ -0,0 +1,96 @@ +// A stackless async body awaiting a stackless callee that suspends: +// results of each kind, callees that finish without suspending, nested +// chains, throws before and after the first suspension, and an uncaught +// rejection, against a ticker that logs every microtask turn. +interface Point { + x: number; + y: number; +} +const log: string[] = []; + +async function slowNumber(n: number, turns: number): Promise { + for (let i = 0; i < turns; i++) await null; + log.push(`slowNumber ${n} done`); + return n * 2; +} +async function maybeSuspend(n: number): Promise { + if (n > 0) await null; + log.push(`maybeSuspend ${n}`); + return n + 100; +} +async function slowFlag(n: number): Promise { + await null; + return n % 2 === 0; +} +async function slowLabel(s: string): Promise { + await null; + return `<${s}>`; +} +async function slowPoint(n: number): Promise { + await null; + return { x: n, y: n * n }; +} +async function slowVoid(tag: string): Promise { + await null; + log.push(`slowVoid ${tag}`); +} +async function throwsEarly(n: number): Promise { + if (n > 0) throw new RangeError(`early ${n}`); + await null; + return n; +} +async function throwsLate(n: number): Promise { + await null; + await null; + throw new TypeError(`late ${n}`); +} +async function chain(depth: number): Promise { + await null; + if (depth === 0) return 1; + return 1 + (await chain(depth - 1)); +} +function ticker(name: string, turns: number): Promise { + let p = Promise.resolve(); + for (let i = 0; i < turns; i++) p = p.then(() => void log.push(`${name} tick ${i}`)); + return p; +} + +async function values(): Promise { + const a = await slowNumber(1, 2); + const b = await maybeSuspend(0); + const c = await maybeSuspend(1); + const f = await slowFlag(4); + const s = await slowLabel("x"); + const p = await slowPoint(3); + await slowVoid("v"); + log.push(`values ${a} ${b} ${c} ${f} ${s} ${p.x + p.y}`); +} +async function errors(): Promise { + try { + await throwsEarly(1); + } catch (e) { + log.push(`errors caught ${(e as Error).name} ${(e as Error).message}`); + } + try { + await throwsLate(2); + } catch (e) { + log.push(`errors caught ${(e as Error).name} ${(e as Error).message}`); + } + log.push(`errors ok ${await throwsEarly(0)}`); +} +async function nested(): Promise { + log.push(`nested ${await chain(4)}`); +} +async function uncaught(): Promise { + await slowNumber(9, 1); + await throwsLate(9); + log.push("uncaught resolved"); +} + +process.on("unhandledRejection", (reason) => console.log(`unhandled ${(reason as Error).message}`)); +async function main(): Promise { + void uncaught(); + await Promise.all([ticker("t", 10), values(), ticker("u", 3), errors(), nested()]); + for (const line of log) console.log(line); +} +void main(); diff --git a/tests/corpus/7105-direct-await-context.ts b/tests/corpus/7105-direct-await-context.ts new file mode 100644 index 000000000..c5a6bd40e --- /dev/null +++ b/tests/corpus/7105-direct-await-context.ts @@ -0,0 +1,34 @@ +// AsyncLocalStorage across a stackless body's awaits of stackless callees: +// each callee inherits its caller's store at the call, a store a callee +// enters after its first await stays inside it, and run() scopes survive +// the awaits. +import { AsyncLocalStorage } from "node:async_hooks"; + +const store = new AsyncLocalStorage(); +const log: string[] = []; + +async function read(tag: string): Promise { + await null; + return `${tag}=${store.getStore() ?? "none"}`; +} +async function enters(tag: string): Promise { + await null; + store.enterWith(`inner-${tag}`); + await null; + return `${tag}=${store.getStore() ?? "none"}`; +} +async function body(name: string): Promise { + log.push(await read(`${name}.first`)); + log.push(await enters(`${name}.enters`)); + log.push(await read(`${name}.after`)); +} + +async function main(): Promise { + await Promise.all([ + store.run("a", () => body("a")), + store.run("b", () => body("b")), + body("none"), + ]); + for (const line of log) console.log(line); +} +void main(); diff --git a/tests/harness/corpus-costs/7104-direct-await-order.ts.json b/tests/harness/corpus-costs/7104-direct-await-order.ts.json new file mode 100644 index 000000000..17c78c9c9 --- /dev/null +++ b/tests/harness/corpus-costs/7104-direct-await-order.ts.json @@ -0,0 +1 @@ +{ "plain": 3100, "san": 4100 } diff --git a/tests/harness/corpus-costs/7105-direct-await-context.ts.json b/tests/harness/corpus-costs/7105-direct-await-context.ts.json new file mode 100644 index 000000000..78ee9421a --- /dev/null +++ b/tests/harness/corpus-costs/7105-direct-await-context.ts.json @@ -0,0 +1 @@ +{ "plain": 2300, "san": 3100 }