mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Await a suspending stackless callee without its promise (#1754)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,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 () => {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7104-direct-await-order.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -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,
|
||||
|
||||
@@ -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--;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) ──────
|
||||
|
||||
@@ -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();
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user