Await a suspending stackless callee without its promise (#1754)

This commit is contained in:
Chris Tate
2026-10-22 00:01:22 -05:00
committed by GitHub
23 changed files with 452 additions and 23 deletions
+2 -2
View File
@@ -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");
});
});
@@ -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)`);
@@ -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<string>();
/** Stackless async functions some coroutine body awaits directly: each
* gets a direct spawn entry (emitCoroDirectAwait). */
readonly directAwaitCallees = new Set<string>();
/** The async functions whose bodies never suspend and that a direct
* await may enter in place (selectStacklessAsync). */
private inlineAwaitable = new Set<string>();
@@ -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
@@ -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 (
@@ -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
+5
View File
@@ -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)}`;
}
+2 -2
View File
@@ -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;
@@ -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: {
@@ -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",
@@ -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<number> {
await null;
return x + 1;
}
async function direct(x: number): Promise<number> {
return (await slow(x)) * 2;
}
async function adopting(x: number): Promise<number> {
return slow(x);
}
async function kept(x: number): Promise<number> {
const pending = slow(x);
await null;
return await pending;
}
async function main(): Promise<void> {
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(");
}
});
@@ -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 () => {
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7104-direct-await-order.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7105-direct-await-context.ts"
],
"diags": []
}
@@ -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,
@@ -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,
+127 -6
View File
@@ -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--;
+9
View File
@@ -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);
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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) ──────
+96
View File
@@ -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<number> {
for (let i = 0; i < turns; i++) await null;
log.push(`slowNumber ${n} done`);
return n * 2;
}
async function maybeSuspend(n: number): Promise<number> {
if (n > 0) await null;
log.push(`maybeSuspend ${n}`);
return n + 100;
}
async function slowFlag(n: number): Promise<boolean> {
await null;
return n % 2 === 0;
}
async function slowLabel(s: string): Promise<string> {
await null;
return `<${s}>`;
}
async function slowPoint(n: number): Promise<Point> {
await null;
return { x: n, y: n * n };
}
async function slowVoid(tag: string): Promise<void> {
await null;
log.push(`slowVoid ${tag}`);
}
async function throwsEarly(n: number): Promise<number> {
if (n > 0) throw new RangeError(`early ${n}`);
await null;
return n;
}
async function throwsLate(n: number): Promise<number> {
await null;
await null;
throw new TypeError(`late ${n}`);
}
async function chain(depth: number): Promise<number> {
await null;
if (depth === 0) return 1;
return 1 + (await chain(depth - 1));
}
function ticker(name: string, turns: number): Promise<void> {
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<void> {
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<void> {
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<void> {
log.push(`nested ${await chain(4)}`);
}
async function uncaught(): Promise<void> {
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> {
void uncaught();
await Promise.all([ticker("t", 10), values(), ticker("u", 3), errors(), nested()]);
for (const line of log) console.log(line);
}
void main();
+34
View File
@@ -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<string>();
const log: string[] = [];
async function read(tag: string): Promise<string> {
await null;
return `${tag}=${store.getStore() ?? "none"}`;
}
async function enters(tag: string): Promise<string> {
await null;
store.enterWith(`inner-${tag}`);
await null;
return `${tag}=${store.getStore() ?? "none"}`;
}
async function body(name: string): Promise<void> {
log.push(await read(`${name}.first`));
log.push(await enters(`${name}.enters`));
log.push(await read(`${name}.after`));
}
async function main(): Promise<void> {
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();
@@ -0,0 +1 @@
{ "plain": 3100, "san": 4100 }
@@ -0,0 +1 @@
{ "plain": 2300, "san": 3100 }