From 7eeca8eb43f70abbbc9f139cf48ddd001eea361a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 00:57:21 -0500 Subject: [PATCH] Add AsyncDisposableStack new AsyncDisposableStack() compiles to a native stack handle with use, adopt, defer, move, disposeAsync, disposed and [Symbol.asyncDispose], and works with `await using`. disposeAsync runs the resources in reverse order and awaits each result as Node does: the next resource runs one microtask turn after the result settles, a callback that throws is not awaited, and use() of null or undefined costs one await at the end when nothing else was awaited. Failures chain into a SuppressedError, and an invalid receiver rejects the returned promise. use() of a class instance calls its [Symbol.asyncDispose] method, or its [Symbol.dispose] method when it has no async one. The stack lives in a new runtime unit, scr_async_disposable.c, linked when a program uses both symbols and checked-dynamic promises; it shares DisposableStack's storage through scr_disposable.h. Library mode refuses it with the rest of the checked-dynamic async surface. --- .../language-features/resource-disposal.mdx | 2 +- .../compiler/src/backend/llvm/lib-shared.ts | 2 + .../compiler/src/backend/native/executable.ts | 2 + .../lowering/builtins/disposable-stack.ts | 90 ++++-- packages/compiler/src/frontend/type-mapper.ts | 8 +- .../compiler/src/ir/builtin-signatures.ts | 11 +- packages/compiler/src/ir/runtime-features.ts | 1 + .../5902-async-disposable-stack.ts.json | 6 + .../5903-async-disposable-stack-js.js.json | 6 + .../runtime-pack-matrix.mjs | 2 + .../runtime-pack-matrix.mjs | 2 + packages/runtime/src/scr_async_disposable.c | 276 ++++++++++++++++++ packages/runtime/src/scr_disposable.h | 51 ++++ packages/runtime/src/scr_dyn.h | 6 + packages/runtime/src/scr_json.c | 6 +- packages/runtime/src/scr_symbol.c | 111 ++++--- tests/corpus/5902-async-disposable-stack.ts | 156 ++++++++++ .../corpus/5903-async-disposable-stack-js.js | 48 +++ tests/harness/corpus-costs.json | 4 + 19 files changed, 692 insertions(+), 98 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5902-async-disposable-stack.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5903-async-disposable-stack-js.js.json create mode 100644 packages/runtime/src/scr_async_disposable.c create mode 100644 packages/runtime/src/scr_disposable.h create mode 100644 tests/corpus/5902-async-disposable-stack.ts create mode 100644 tests/corpus/5903-async-disposable-stack-js.js diff --git a/docs/content/limitations/language-features/resource-disposal.mdx b/docs/content/limitations/language-features/resource-disposal.mdx index d3df4138e..5b6fd33e9 100644 --- a/docs/content/limitations/language-features/resource-disposal.mdx +++ b/docs/content/limitations/language-features/resource-disposal.mdx @@ -1 +1 @@ -- **Resource disposal:** Top-level and switch-clause `using` declarations, and `AsyncDisposableStack`, are unsupported. Block- and function-scoped `using` and `await using` support compiled classes with zero-parameter disposal methods, object literals and other `Disposable`- or `AsyncDisposable`-typed values, and supported FileHandle, timer, immediate, child-process, and readline handles. A class instance reached through a `Disposable`- or `unknown`-typed value throws an SC2020 error when its declaration runs; declare the variable with the class type. Array `for (using ... of ...)` loops also compile. `SuppressedError` values expose `error` and `suppressed`; `extends SuppressedError` and casting `unknown` to `SuppressedError` are unsupported (narrow with `instanceof`). `DisposableStack` supports `use`, `adopt`, `defer`, `move`, `dispose`, `disposed` and `[Symbol.dispose]`, and works with `using`; `use` accepts class instances, object literals and other `Disposable`-typed values, and refuses unions that include class instances (narrow the value first). Disposal runs in reverse order and raises Node's `SuppressedError` chain when more than one callback throws. +- **Resource disposal:** Top-level and switch-clause `using` declarations are unsupported. Block- and function-scoped `using` and `await using` support compiled classes with zero-parameter disposal methods, object literals and other `Disposable`- or `AsyncDisposable`-typed values, and supported FileHandle, timer, immediate, child-process, and readline handles. A class instance reached through a `Disposable`- or `unknown`-typed value throws an SC2020 error when its declaration runs; declare the variable with the class type. Array `for (using ... of ...)` loops also compile. `SuppressedError` values expose `error` and `suppressed`; `extends SuppressedError` and casting `unknown` to `SuppressedError` are unsupported (narrow with `instanceof`). `DisposableStack` supports `use`, `adopt`, `defer`, `move`, `dispose`, `disposed` and `[Symbol.dispose]`, and works with `using`; `use` accepts class instances, object literals and other `Disposable`-typed values, and refuses unions that include class instances (narrow the value first). Disposal runs in reverse order and raises Node's `SuppressedError` chain when more than one callback throws. `AsyncDisposableStack` supports `use`, `adopt`, `defer`, `move`, `disposeAsync`, `disposed` and `[Symbol.asyncDispose]`, and works with `await using`; `disposeAsync` awaits each disposal's result on the microtask turns Node uses, and `use` of a class instance calls its `[Symbol.asyncDispose]` method, or its `[Symbol.dispose]` method when it has no async one. In library mode `AsyncDisposableStack` is unsupported. diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 47a825dc3..0e8b091ab 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1328,6 +1328,8 @@ export const LIB_FN_SYMS: Record = { "dyn.usingDispose": "scr_dyn_using_dispose", "disposable.stackNew": "scr_disposable_stack_new", "disposable.stackIs": "scr_disposable_stack_is", + "disposable.asyncStackNew": "scr_async_disposable_stack_new", + "disposable.asyncStackIs": "scr_async_disposable_stack_is", "disposable.stackUseCallback": "scr_disposable_stack_use_callback", "dyn.reflectSet": "scr_dyn_reflect_set", "dyn.reflectDefine": "scr_dyn_reflect_define", diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 549e9ebbf..ad7c9de4c 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -538,6 +538,8 @@ export async function compileCInternal( // their exact size class. ...(opts.emitter || opts.workers || net ? [rt(join(rtDir, "scr_dyn_handle.c"))] : []), ...(opts.symbol ? [rt(join(rtDir, "scr_symbol.c"))] : []), + // AsyncDisposableStack calls into both scr_symbol.c and scr_async_dyn.c. + ...(opts.symbol && opts.dynAsync ? [rt(join(rtDir, "scr_async_disposable.c"))] : []), ...(opts.assert && opts.bigint ? [rt(join(rtDir, "scr_bigint_assert.c"))] : []), ...(opts.qs ? [rt(join(rtDir, "scr_qs.c"))] : []), // The legacy url unit references the regex, querystring and dyn-async diff --git a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts index 469a66ace..1165bdaf4 100644 --- a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts +++ b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts @@ -5,44 +5,66 @@ import { newFnCtx } from "../function-context.js"; import { BOOL, DYN, VOID, type IrExpr, type IrType } from "../../../ir/ir.js"; import { varRef } from "../../../ir/build.js"; -/** Whether `node`'s static type is the standard library's DisposableStack. */ -function isStdlibDisposableStack(lowerer: Lowerer, node: ts.Expression): boolean { +type StackClass = "DisposableStack" | "AsyncDisposableStack"; + +const STACK_CLASSES: readonly StackClass[] = ["DisposableStack", "AsyncDisposableStack"]; + +/** The standard-library stack class `node`'s static type names, or null. */ +function stdlibStackClassOf(lowerer: Lowerer, node: ts.Expression): StackClass | null { const type = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(node)); for (const symbol of [type.getSymbol(), type.getAliasSymbol()]) { - if (symbol?.name === "DisposableStack" && lowerer.isStdlibSymbol(symbol)) return true; + const name = STACK_CLASSES.find((cls) => cls === symbol?.name); + if (name && symbol && lowerer.isStdlibSymbol(symbol)) return name; } - return false; + return null; } -/** `new DisposableStack()`: a native stack handle (scr_symbol.c). Its - * members are function values and getters of the handle, reached through - * the checked-dynamic method and property paths. Null for other - * constructors. */ +/** `new DisposableStack()` and `new AsyncDisposableStack()`: native stack + * handles (scr_symbol.c, scr_async_disposable.c). Their members are + * function values and getters of the handle, reached through the + * checked-dynamic method and property paths. Null for other constructors. */ export function lowerDisposableStackNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { - if (!ts.isIdentifier(expr.expression) || expr.expression.text !== "DisposableStack") return null; + if (!ts.isIdentifier(expr.expression)) return null; + const text = expr.expression.text; + const name = STACK_CLASSES.find((cls) => cls === text); + if (!name) return null; const symbol = lowerer.resolveValueSymbol(expr.expression); if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; if ((expr.arguments ?? []).length !== 0) return null; - return { kind: "libCall", fn: "disposable.stackNew", args: [], type: DYN, loc: locOf(expr) }; + return { + kind: "libCall", + fn: name === "DisposableStack" ? "disposable.stackNew" : "disposable.asyncStackNew", + args: [], + type: DYN, + loc: locOf(expr), + }; } -/** `x instanceof DisposableStack`, or null when the right side is not the - * standard library's DisposableStack. */ +/** `x instanceof DisposableStack` and `x instanceof AsyncDisposableStack`, + * or null when the right side is neither standard-library class. */ export function lowerDisposableStackInstanceOf( lowerer: Lowerer, expr: ts.BinaryExpression, ): IrExpr | null { - if (!lowerer.isStdlibGlobal(expr.right, "DisposableStack")) return null; + const name = STACK_CLASSES.find((cls) => lowerer.isStdlibGlobal(expr.right, cls)); + if (!name) return null; const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); - return { kind: "libCall", fn: "disposable.stackIs", args: [value], type: BOOL, loc: locOf(expr) }; + return { + kind: "libCall", + fn: name === "DisposableStack" ? "disposable.stackIs" : "disposable.asyncStackIs", + args: [value], + type: BOOL, + loc: locOf(expr), + }; } /** `stack.use(value)` where `value` is a class instance. The dyn view of a * class instance has no methods, so the stack receives a function that - * calls the class's [Symbol.dispose] method on the value, and the call - * answers the value at its static type. Other values take the - * checked-dynamic method path, which reads the value's [Symbol.dispose]. - * Null when the call is not that form. */ + * calls the class's [Symbol.dispose] method on the value (on an + * AsyncDisposableStack, its [Symbol.asyncDispose] method when it has one, + * whose promise the disposal awaits), and the call answers the value at + * its static type. Other values take the checked-dynamic method path, + * which reads the value's methods. Null when the call is not that form. */ export function lowerDisposableStackUseCall( lowerer: Lowerer, call: ts.CallExpression, @@ -52,7 +74,8 @@ export function lowerDisposableStackUseCall( if (access.name.text !== "use" || call.arguments.length !== 1) return null; const argNode = call.arguments[0]!; if (ts.isSpreadElement(argNode)) return null; - if (!isStdlibDisposableStack(lowerer, access.expression)) return null; + const stackClass = stdlibStackClassOf(lowerer, access.expression); + if (!stackClass) return null; const valueT = lowerer.mapTypeOf(lowerer.typeOf(argNode)); if (valueT?.kind === "union") { const arms = lowerer.unions.get(valueT.unionId)?.arms ?? []; @@ -69,34 +92,41 @@ export function lowerDisposableStackUseCall( const className = valueT.className; const info = lowerer.classes.get(className); if (!info) lowerer.flushDeferredClass(className); - const found = info ? lowerer.findMethodOn(info, "sym:dispose") : null; + const asyncFound = + stackClass === "AsyncDisposableStack" && info + ? lowerer.findMethodOn(info, "sym:asyncDispose") + : null; + const member = asyncFound ? "sym:asyncDispose" : "sym:dispose"; + const found = asyncFound ?? (info ? lowerer.findMethodOn(info, "sym:dispose") : null); if (!found) return null; + const memberText = asyncFound ? "[Symbol.asyncDispose]" : "[Symbol.dispose]"; if (found.sig.params.length !== 0) { - lowerer.unsupported("SC1090", argNode, "[Symbol.dispose] methods with parameters"); + lowerer.unsupported("SC1090", argNode, `${memberText} methods with parameters`); } + if (asyncFound && found.sig.ret.kind !== "promise") { + lowerer.unsupported("SC1090", argNode, `${memberText} methods whose result is not a Promise`); + } + const helperRet: IrType = asyncFound ? found.sig.ret : VOID; const stack = lowerer.lowerExpr(access.expression); const value = lowerer.lowerExpr(argNode); - const key = `disposableStack.use:${className}`; + const key = `disposableStack.use:${member}:${className}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { helper = `%disposableStack.use.${lowerer.valueHelpers.size}`; lowerer.valueHelpers.set(key, helper); - const context = newFnCtx(false, null, null, VOID); + const context = newFnCtx(false, null, null, helperRet); lowerer.fnStack.push(context); try { const param = lowerer.declareHiddenLocal("value", valueT); const self = varRef(param.id, valueT, loc); + const call = lowerer.accessorCall(className, member, self, [], found.sig.ret, loc); lowerer.liftedFns.push({ name: helper, params: [{ localId: param.id, name: "value", type: valueT }], - returnType: VOID, + returnType: helperRet, locals: context.locals, body: [ - { - kind: "exprStmt", - expr: lowerer.accessorCall(className, "sym:dispose", self, [], found.sig.ret, loc), - loc, - }, + asyncFound ? { kind: "return", value: call, loc } : { kind: "exprStmt", expr: call, loc }, ], loc, }); @@ -106,7 +136,7 @@ export function lowerDisposableStackUseCall( } const stackLocal = lowerer.declareHiddenLocal("%disposableStack", DYN); const valueLocal = lowerer.declareHiddenLocal("%disposableValue", valueT); - const fnT: IrType = { kind: "func", params: [valueT], ret: VOID }; + const fnT: IrType = { kind: "func", params: [valueT], ret: helperRet }; const dispose: IrExpr = { kind: "closure", fnName: helper, captures: [], type: fnT, loc }; return { kind: "seqExpr", diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index e78ae59cc..a379c5ceb 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -2377,13 +2377,15 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // representation and consumed by the console method lowerings. The // stdlib Disposable (events.addAbortListener's result) and // AsyncDisposable are checked objects carrying their Symbol.dispose or - // Symbol.asyncDispose method. A DisposableStack is a native handle whose - // methods are function values of its checked-dynamic view. + // Symbol.asyncDispose method. DisposableStack and AsyncDisposableStack + // are native handles whose methods are function values of their + // checked-dynamic views. if ( (psym?.name === "Console" || psym?.name === "Disposable" || psym?.name === "AsyncDisposable" || - psym?.name === "DisposableStack") && + psym?.name === "DisposableStack" || + psym?.name === "AsyncDisposableStack") && checker .declarationsOf(psym) .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 03e47dc72..372069242 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3214,12 +3214,15 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * `await using`. Both throw Node's TypeErrors or the method's error. */ "dyn.usingMethod": { argTypes: [DYN, BOOL], result: DYN }, "dyn.usingDispose": { argTypes: [DYN, DYN], result: DYN }, - /** DisposableStack (scr_symbol.c): a new stack handle; whether a value is - * one; and `stack.use(value)` for a class instance, registering the - * compiler's disposal function for it. The last throws Node's - * ReferenceError on a disposed stack. */ + /** DisposableStack (scr_symbol.c) and AsyncDisposableStack + * (scr_async_disposable.c): a new stack handle; whether a value is one; + * and `stack.use(value)` for a class instance on either stack, + * registering the compiler's disposal function for it. The last throws + * Node's ReferenceError on a disposed stack. */ "disposable.stackNew": { argTypes: [], result: DYN }, "disposable.stackIs": { argTypes: [DYN], result: BOOL }, + "disposable.asyncStackNew": { argTypes: [], result: DYN }, + "disposable.asyncStackIs": { argTypes: [DYN], result: BOOL }, "disposable.stackUseCallback": { argTypes: [DYN, DYN, DYN], result: BOOL }, "dyn.reflectSet": { argTypes: [DYN, DYN, DYN, DYN], result: BOOL }, "dyn.reflectDefine": { argTypes: [DYN, DYN, DYN], result: BOOL }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 94c3331ce..573f841b2 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -43,6 +43,7 @@ export interface RuntimeFeatures { } const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ + "disposable.asyncStackNew", "fs.callbackValue", "fs.callbackCall", "fs.callbackGlob", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5902-async-disposable-stack.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5902-async-disposable-stack.ts.json new file mode 100644 index 000000000..2e7640a20 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5902-async-disposable-stack.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5902-async-disposable-stack.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5903-async-disposable-stack-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5903-async-disposable-stack-js.js.json new file mode 100644 index 000000000..c4579368e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5903-async-disposable-stack-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5903-async-disposable-stack-js.js" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs index 54e789624..ca5caeb95 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -80,6 +80,8 @@ const optional = [ ["scr_events_emitter.c", "emitter"], ["scr_dyn_handle.c", any("emitter", "netEffective")], ["scr_symbol.c", "symbol"], + // AsyncDisposableStack calls into both scr_symbol.c and scr_async_dyn.c. + ["scr_async_disposable.c", all("symbol", "dynAsync")], ["scr_bigint_assert.c", all("assert", "bigint")], ["scr_qs.c", "qs"], // The legacy url unit lowercases and normalizes through the regex unit diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 6b5052482..8f9e22b92 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -70,6 +70,8 @@ const OPTIONAL = [ ["scr_events_emitter.c", "emitter"], ["scr_dyn_handle.c", any("emitter", "netEffective")], ["scr_symbol.c", "symbol"], + // AsyncDisposableStack calls into both scr_symbol.c and scr_async_dyn.c. + ["scr_async_disposable.c", all("symbol", "dynAsync")], ["scr_bigint_assert.c", all("assert", "bigint")], ["scr_qs.c", "qs"], // The legacy url unit lowercases and normalizes through the regex unit diff --git a/packages/runtime/src/scr_async_disposable.c b/packages/runtime/src/scr_async_disposable.c new file mode 100644 index 000000000..fd64b16f4 --- /dev/null +++ b/packages/runtime/src/scr_async_disposable.c @@ -0,0 +1,276 @@ +/* AsyncDisposableStack: the DisposableStack storage (scr_disposable.h) + * behind its own handle, with disposeAsync in place of dispose. + * disposeAsync runs the entries in reverse order and awaits each result as + * an async function would: the next entry runs one microtask turn after + * the result settles, a callback that throws is not awaited, and an absent + * entry (use() of null or undefined) costs one await at the end when no + * other entry was awaited. Failures chain into a SuppressedError as in + * DisposableStack, and the returned promise settles after the last entry. + * Linked with scr_symbol.c and scr_async_dyn.c, whose functions it calls. */ +#include "scr_runtime.h" +#include "scr_disposable.h" + +#include +#include + +typedef struct { + ScrDisposableStack *storage; /* the entries, moved out of the stack */ + size_t next; /* entries [0, next) are still to run */ + ScrCaught *error; /* the disposal error so far, or NULL */ + bool needs_await, has_awaited; + ScrPromise *result; /* the promise disposeAsync returned */ + ScrPromise *awaited; /* the result being awaited */ +} ScrAdsRun; + +static void scr_ads_step(ScrAdsRun *run); + +static void scr_ads_fail(ScrAdsRun *run) { + if (run->error) scr_exc_suppress(run->error); + run->error = scr_exc_take(); +} + +static void scr_ads_resume(ScrFiber *self, void *opaque) { + (void)self; + ScrAdsRun *run = opaque; + run->has_awaited = true; + ScrDyn *value = scr_await_dyn(run->awaited); + scr_promise_release(run->awaited); + run->awaited = NULL; + if (value) scr_dyn_release(value); + else scr_ads_fail(run); + scr_ads_step(run); +} + +/* Await `value` (moved in): a promise is awaited itself, any other value + * through a fulfilled promise, as `await` takes one turn for either. */ +static void scr_ads_await(ScrAdsRun *run, ScrDyn *value) { + ScrPromise *p; + if (value->kind == SCR_DYN_PROMISE) { + p = scr_promise_retain(value->v.promise); + } else { + p = scr_promise_new(); + scr_promise_fulfill_ref(p, scr_dyn_retain(value), scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + } + scr_dyn_release(value); + scr_promise_mark_handled(p); + run->awaited = p; + ScrPromise *waiter = scr_async_spawn_after(p, scr_ads_resume, run); + scr_promise_release(waiter); +} + +static void scr_ads_finish(ScrAdsRun *run) { + if (run->error) { + scr_rethrow(run->error); + scr_caught_release(run->error); + scr_promise_reject_pending(run->result); + } else { + scr_promise_resolve_dyn(run->result, scr_dyn_retain(scr_dyn_undefined())); + } + scr_promise_release(run->result); + scr_ds_release(run->storage); + free(run); +} + +static void scr_ads_step(ScrAdsRun *run) { + while (run->next > 0) { + ScrDisposableResource *r = &run->storage->items[--run->next]; + if (r->kind == SCR_DS_ABSENT) { + run->needs_await = true; + continue; + } + ScrDyn *result; + if (r->kind == SCR_DS_USE) { + result = scr_dyn_using_dispose(r->method, r->value); + } else { + scr_dyn_this_push_dyn(scr_dyn_undefined()); + result = r->kind == SCR_DS_ADOPT ? scr_dyn_call(r->method, &r->value, 1, "onDisposeAsync") + : scr_dyn_call(r->method, NULL, 0, "onDisposeAsync"); + scr_dyn_this_pop(); + } + scr_dyn_release(r->value); + scr_dyn_release(r->method); + r->value = r->method = NULL; + r->kind = SCR_DS_ABSENT; + if (!result) { + scr_ads_fail(run); + continue; + } + scr_ads_await(run, result); + return; + } + if (run->needs_await && !run->has_awaited) { + run->needs_await = false; + scr_ads_await(run, scr_dyn_retain(scr_dyn_undefined())); + return; + } + scr_ads_finish(run); +} + +static ScrDyn *scr_ads_dispose_async(const ScrDyn *self) { + ScrPromise *p = scr_promise_new(); + ScrDyn *boxed = scr_dyn_new_promise(p); + ScrDisposableStack *s = self->kind == SCR_DYN_HANDLE && self->v.handle.tag == SCR_DYNH_ASYNC_DISPOSABLE_STACK + ? self->v.handle.ptr : NULL; + if (!s) { + /* An invalid receiver rejects the promise instead of throwing. */ + static const char message[] = "Receiver is not an AsyncDisposableStack"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + scr_promise_reject_pending(p); + } else if (s->disposed) { + scr_promise_resolve_dyn(p, scr_dyn_retain(scr_dyn_undefined())); + } else { + ScrAdsRun *run = calloc(1, sizeof *run); + if (!run) scr_trap("scriptc: out of memory\n"); + run->storage = scr_ds_storage_new(); + scr_ds_move(run->storage, s); + run->next = run->storage->len; + run->result = scr_promise_retain(p); + scr_ads_step(run); + } + scr_promise_release(p); + return boxed; +} + +static SCR_TL ScrDyn *scr_ads_method_values[6]; +static const char *const scr_ads_method_names[] = {"use", "adopt", "defer", "move", "disposeAsync", "disposed"}; +static const uint32_t scr_ads_method_lengths[] = {1, 2, 1, 0, 0, 0}; +enum { SCR_ADS_M_USE, SCR_ADS_M_ADOPT, SCR_ADS_M_DEFER, SCR_ADS_M_MOVE, SCR_ADS_M_DISPOSE, SCR_ADS_M_DISPOSED }; + +static ScrDisposableStack *scr_ads_alloc(void); +static ScrDyn *scr_ads_box(ScrDisposableStack *s); + +static ScrDyn *scr_ads_method_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + unsigned index = (unsigned)scr_box_get_f64(clo->caps[0]); + ScrDyn *self = scr_dyn_this_get(); + const ScrDyn *arg0 = argc > 0 ? args[0] : scr_dyn_undefined(); + const ScrDyn *arg1 = argc > 1 ? args[1] : scr_dyn_undefined(); + const ScrDynHandleTag tag = SCR_DYNH_ASYNC_DISPOSABLE_STACK; + ScrDyn *out = NULL; + ScrDisposableStack *s; + switch (index) { + case SCR_ADS_M_USE: + if (!(s = scr_ds_receiver(self, tag, "use", false, true))) break; + if (arg0->kind == SCR_DYN_NULL || arg0->kind == SCR_DYN_UNDEF) { + scr_ds_push(s, NULL, NULL, SCR_DS_ABSENT); + } else { + ScrDyn *method = scr_dyn_using_method(arg0, true); + if (!method) break; + scr_ds_push(s, scr_dyn_retain((ScrDyn *)arg0), method, SCR_DS_USE); + } + out = scr_dyn_retain((ScrDyn *)arg0); + break; + case SCR_ADS_M_ADOPT: + if (!(s = scr_ds_receiver(self, tag, "adopt", false, true)) || !scr_ds_callable(arg1)) break; + scr_ds_push(s, scr_dyn_retain((ScrDyn *)arg0), scr_dyn_retain((ScrDyn *)arg1), SCR_DS_ADOPT); + out = scr_dyn_retain((ScrDyn *)arg0); + break; + case SCR_ADS_M_DEFER: + if (!(s = scr_ds_receiver(self, tag, "defer", false, true)) || !scr_ds_callable(arg0)) break; + scr_ds_push(s, NULL, scr_dyn_retain((ScrDyn *)arg0), SCR_DS_DEFER); + out = scr_dyn_retain(scr_dyn_undefined()); + break; + case SCR_ADS_M_MOVE: { + if (!(s = scr_ds_receiver(self, tag, "move", false, true))) break; + ScrDisposableStack *moved = scr_ads_alloc(); + scr_ds_move(moved, s); + out = scr_ads_box(moved); + scr_ds_release(moved); + break; + } + default: + out = scr_ads_dispose_async(self); + break; + } + scr_dyn_release(self); + return out; +} + +static ScrDyn *scr_ads_method_value(unsigned index) { + if (!scr_ads_method_values[index]) { + ScrClosure *clo = scr_closure_new(NULL, 1); + clo->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(clo->caps[0], (double)index); + scr_ads_method_values[index] = scr_dyn_new_func(clo, scr_ads_method_call, scr_ads_method_lengths[index], + "", scr_ads_method_names[index]); + } + return scr_dyn_retain(scr_ads_method_values[index]); +} + +static ScrDyn *scr_ads_get(void *ptr, const char *key, size_t len) { + ScrDisposableStack *s = ptr; + if (len == 8 && memcmp(key, "disposed", 8) == 0) return scr_dyn_new_bool(s->disposed); + for (unsigned i = 0; i < SCR_ADS_M_DISPOSED; i++) { + if (strlen(scr_ads_method_names[i]) == len && memcmp(key, scr_ads_method_names[i], len) == 0) + return scr_ads_method_value(i); + } + return NULL; +} + +static bool scr_ads_set(void *ptr, const char *key, size_t len, const ScrDyn *value) { + (void)ptr; + (void)key; + (void)len; + (void)value; + return false; +} + +/* [Symbol.asyncDispose] is disposeAsync itself, as in Node. */ +static ScrDyn *scr_ads_symbol_get(void *ptr, const ScrDyn *key) { + (void)ptr; + if (key->kind != SCR_DYN_SYMBOL) return NULL; + ScrStr *name = scr_str_new("asyncDispose", 12); + ScrSym *async_dispose = scr_sym_well_known(name); + scr_str_release(name); + bool match = key->v.symbol.value == async_dispose; + scr_sym_release(async_dispose); + return match ? scr_ads_method_value(SCR_ADS_M_DISPOSE) : NULL; +} + +static ScrDyn *scr_ads_inspect_view(void *ptr) { + (void)ptr; + return scr_dyn_new_obj(); +} + +static void scr_ads_teardown(void) { + for (size_t i = 0; i < sizeof scr_ads_method_values / sizeof scr_ads_method_values[0]; i++) { + scr_dyn_release(scr_ads_method_values[i]); + scr_ads_method_values[i] = NULL; + } +} + +static const ScrDynHandleOps scr_ads_ops = { + .cls = "AsyncDisposableStack", + .retain = scr_ds_retain, + .release = scr_ds_release, + .get = scr_ads_get, + .set = scr_ads_set, + .inspect_view = scr_ads_inspect_view, + .json_empty_object = true, + .inspect_view_empty = true, + .symbol_get = scr_ads_symbol_get, +}; + +static ScrDisposableStack *scr_ads_alloc(void) { + static SCR_TL bool installed; + if (!installed) { + installed = true; + scr_dyn_handle_install(SCR_DYNH_ASYNC_DISPOSABLE_STACK, &scr_ads_ops); + scr_atexit(scr_ads_teardown); + } + return scr_ds_storage_new(); +} + +static ScrDyn *scr_ads_box(ScrDisposableStack *s) { + return scr_dyn_new_handle(s, SCR_DYNH_ASYNC_DISPOSABLE_STACK); +} + +ScrDyn *scr_async_disposable_stack_new(void) { + ScrDisposableStack *s = scr_ads_alloc(); + ScrDyn *out = scr_ads_box(s); + scr_ds_release(s); + return out; +} + +bool scr_async_disposable_stack_is(const ScrDyn *value) { + return value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_ASYNC_DISPOSABLE_STACK; +} diff --git a/packages/runtime/src/scr_disposable.h b/packages/runtime/src/scr_disposable.h new file mode 100644 index 000000000..7c7150abe --- /dev/null +++ b/packages/runtime/src/scr_disposable.h @@ -0,0 +1,51 @@ +#ifndef SCR_DISPOSABLE_H +#define SCR_DISPOSABLE_H + +/* Storage shared by the DisposableStack handle (scr_symbol.c) and the + * AsyncDisposableStack handle (scr_async_disposable.c). A stack is a + * collector-headered object behind a traced dyn handle: its resources hold + * callbacks that can capture the stack itself. Each entry records how + * disposal calls it: `use` calls the value's dispose method with the value + * as `this`, `adopt` calls the callback with the value, `defer` calls the + * callback with no arguments, and an absent entry (an AsyncDisposableStack + * use() of null or undefined) calls nothing. */ + +#include "scr_runtime.h" + +enum { SCR_DS_USE, SCR_DS_ADOPT, SCR_DS_DEFER, SCR_DS_ABSENT }; + +typedef struct { + ScrDyn *value; /* the resource (use, adopt); NULL for defer and absent */ + ScrDyn *method; /* NULL for absent */ + unsigned kind; + /* Which members the last trace visited: the collector frees those + * itself, and gcfree must not read them afterwards. */ + bool value_traced, method_traced; +} ScrDisposableResource; + +typedef struct { + size_t rc; + bool disposed; + ScrDisposableResource *items; + size_t len, cap; +} ScrDisposableStack; + +/* A new, empty stack (+1). */ +ScrDisposableStack *scr_ds_storage_new(void); +void *scr_ds_retain(void *stack); +void scr_ds_release(void *stack); +/* Moves value and method in. */ +void scr_ds_push(ScrDisposableStack *s, ScrDyn *value, ScrDyn *method, unsigned kind); +/* Moves `from`'s entries to `to` and marks `from` disposed. */ +void scr_ds_move(ScrDisposableStack *to, ScrDisposableStack *from); +/* V8's rendering of a value in a TypeError. */ +void scr_ds_put_value(ScrJsonBuf *b, const ScrDyn *value); +/* Whether `value` is callable; false with V8's TypeError pending. */ +bool scr_ds_callable(const ScrDyn *value); +/* The stack behind `self` when it has handle tag `tag`, or NULL with + * Node's TypeError (an incompatible receiver of .prototype.) + * or ReferenceError (a disposed stack, when `live` is required) pending. */ +ScrDisposableStack *scr_ds_receiver(const ScrDyn *self, ScrDynHandleTag tag, const char *name, bool getter, + bool live); + +#endif diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 392670bef..bc001fee1 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -169,6 +169,7 @@ typedef enum { SCR_DYNH_NET_SOCKET_ADDRESS, /* net.SocketAddress (scr_net.c) */ SCR_DYNH_NET_BLOCK_LIST, /* net.BlockList (scr_net.c) */ SCR_DYNH_DISPOSABLE_STACK, /* DisposableStack (scr_symbol.c) */ + SCR_DYNH_ASYNC_DISPOSABLE_STACK, /* AsyncDisposableStack (scr_async_disposable.c) */ SCR_DYNH_COUNT, } ScrDynHandleTag; @@ -633,6 +634,11 @@ bool scr_disposable_stack_is(const ScrDyn *value); bool scr_disposable_stack_use(const ScrDyn *stack, const ScrDyn *value); bool scr_disposable_stack_use_callback(const ScrDyn *stack, const ScrDyn *value, const ScrDyn *dispose); bool scr_disposable_stack_adopt(const ScrDyn *stack, const ScrDyn *value, const ScrDyn *on_dispose); +/* AsyncDisposableStack (scr_async_disposable.c): a new stack (+1) and + * whether a value is one. scr_disposable_stack_use_callback also registers + * on an AsyncDisposableStack, whose disposal awaits the function's result. */ +ScrDyn *scr_async_disposable_stack_new(void); +bool scr_async_disposable_stack_is(const ScrDyn *value); void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored); void scr_dyn_symbol_key_placeholder(ScrDyn *value, ScrDyn *key); bool scr_dyn_has_key_computed(const ScrDyn *value, const ScrDyn *key); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 13553fc7f..325db039c 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -3872,6 +3872,7 @@ ScrDyn *scr_dyn_new_handle(void *h, ScrDynHandleTag tag) { d->v.handle.tag = tag; d->v.handle.traced = tag == SCR_DYNH_WEAK_MAP || tag == SCR_DYNH_WEAK_SET || tag == SCR_DYNH_WORKER || tag == SCR_DYNH_MESSAGE_PORT || tag == SCR_DYNH_DISPOSABLE_STACK || + tag == SCR_DYNH_ASYNC_DISPOSABLE_STACK || ((tag == SCR_DYNH_SET || tag == SCR_DYNH_MAP) && ((ScrMap *)h)->traced != 0); return d; @@ -5406,7 +5407,8 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) { d->v.handle.tag == SCR_DYNH_WEAK_MAP || d->v.handle.tag == SCR_DYNH_WEAK_SET || d->v.handle.tag == SCR_DYNH_SET || d->v.handle.tag == SCR_DYNH_MAP || d->v.handle.tag == SCR_DYNH_REGEXP || d->v.handle.tag == SCR_DYNH_DATE || d->v.handle.tag == SCR_DYNH_BLOB || - d->v.handle.tag == SCR_DYNH_FILE || d->v.handle.tag == SCR_DYNH_DISPOSABLE_STACK) { + d->v.handle.tag == SCR_DYNH_FILE || d->v.handle.tag == SCR_DYNH_DISPOSABLE_STACK || + d->v.handle.tag == SCR_DYNH_ASYNC_DISPOSABLE_STACK) { ScrJsonBuf b; scr_jb_init(&b); scr_jb_puts(&b, "[object "); @@ -5992,7 +5994,7 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) { d->v.handle.tag <= SCR_DYNH_ABORT_CONTROLLER) || d->v.handle.tag == SCR_DYNH_EVENT_TARGET || d->v.handle.tag == SCR_DYNH_CUSTOM_EVENT || d->v.handle.tag == SCR_DYNH_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SHARED_ARRAY_BUFFER || d->v.handle.tag == SCR_DYNH_SET || d->v.handle.tag == SCR_DYNH_MAP || d->v.handle.tag == SCR_DYNH_BLOB || d->v.handle.tag == SCR_DYNH_FILE || - d->v.handle.tag == SCR_DYNH_DISPOSABLE_STACK) { + d->v.handle.tag == SCR_DYNH_DISPOSABLE_STACK || d->v.handle.tag == SCR_DYNH_ASYNC_DISPOSABLE_STACK) { ScrJsonBuf b; scr_jb_init(&b); scr_jb_puts(&b, "[object "); diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c index ebfb56291..aa2ab8565 100644 --- a/packages/runtime/src/scr_symbol.c +++ b/packages/runtime/src/scr_symbol.c @@ -12,6 +12,7 @@ * always after scr_init registered the RC audit, so atexit's LIFO order * runs it before the audit and the audited string counts stay exact. */ #include "scr_runtime.h" +#include "scr_disposable.h" #include #include @@ -459,31 +460,10 @@ ScrDyn *scr_dyn_using_dispose(const ScrDyn *method, const ScrDyn *value) { } /* ── DisposableStack ─────────────────────────────────────────────────── - * The stack is a collector-headered object behind a traced dyn handle: its - * resources hold callbacks that can capture the stack itself. Each entry - * records how disposal calls it: `use` calls the value's dispose method - * with the value as `this`, `adopt` calls the callback with the value, and - * `defer` calls the callback with no arguments. Disposal runs the entries - * in reverse order; each later failure becomes a SuppressedError over the - * error so far, and a single failure is rethrown as it was thrown. */ -enum { SCR_DS_USE, SCR_DS_ADOPT, SCR_DS_DEFER }; - -typedef struct { - ScrDyn *value; /* the resource (use, adopt); NULL for defer */ - ScrDyn *method; - unsigned kind; - /* Which members the last trace visited: the collector frees those - * itself, and gcfree must not read them afterwards. */ - bool value_traced, method_traced; -} ScrDisposableResource; - -typedef struct { - size_t rc; - bool disposed; - ScrDisposableResource *items; - size_t len, cap; -} ScrDisposableStack; - + * Storage and entries are described in scr_disposable.h. Disposal runs + * the entries in reverse order; each later failure becomes a + * SuppressedError over the error so far, and a single failure is rethrown + * as it was thrown. */ static bool scr_ds_traced(const ScrDyn *value) { return value && value->kind > SCR_DYN_STR && value->kind != SCR_DYN_UNDEF; } @@ -521,12 +501,12 @@ static void scr_ds_destroy(void *ptr) { scr_ds_gcfree(ptr); } -static void *scr_ds_retain(void *ptr) { +void *scr_ds_retain(void *ptr) { ((ScrDisposableStack *)ptr)->rc++; return ptr; } -static void scr_ds_release(void *ptr) { +void scr_ds_release(void *ptr) { ScrDisposableStack *s = ptr; if (!s) return; if (--s->rc == 0) { @@ -535,7 +515,7 @@ static void scr_ds_release(void *ptr) { } else scr_cyc_released(s); } -static void scr_ds_push(ScrDisposableStack *s, ScrDyn *value, ScrDyn *method, unsigned kind) { +void scr_ds_push(ScrDisposableStack *s, ScrDyn *value, ScrDyn *method, unsigned kind) { if (s->len == s->cap) { size_t cap = s->cap ? s->cap * 2 : 4; ScrDisposableResource *items = realloc(s->items, cap * sizeof *items); @@ -548,7 +528,7 @@ static void scr_ds_push(ScrDisposableStack *s, ScrDyn *value, ScrDyn *method, un /* V8's rendering of a value in a TypeError: primitives by ToString, * arrays as [object Array], other objects as #. */ -static void scr_ds_put_value(ScrJsonBuf *b, const ScrDyn *value) { +void scr_ds_put_value(ScrJsonBuf *b, const ScrDyn *value) { switch (value->kind) { case SCR_DYN_UNDEF: scr_jb_puts(b, "undefined"); return; case SCR_DYN_NULL: scr_jb_puts(b, "null"); return; @@ -577,22 +557,24 @@ static void scr_ds_put_value(ScrJsonBuf *b, const ScrDyn *value) { } } -static ScrDisposableStack *scr_ds_of(const ScrDyn *value) { - return value && value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_DISPOSABLE_STACK - ? value->v.handle.ptr : NULL; +static ScrDisposableStack *scr_ds_of(const ScrDyn *value, ScrDynHandleTag tag) { + return value && value->kind == SCR_DYN_HANDLE && value->v.handle.tag == tag ? value->v.handle.ptr : NULL; } -bool scr_disposable_stack_is(const ScrDyn *value) { return scr_ds_of(value) != NULL; } +bool scr_disposable_stack_is(const ScrDyn *value) { + return scr_ds_of(value, SCR_DYNH_DISPOSABLE_STACK) != NULL; +} -/* The receiver of DisposableStack.prototype., or NULL with Node's - * TypeError (an incompatible receiver) or ReferenceError (a disposed - * stack, when `live` is required) pending. */ -static ScrDisposableStack *scr_ds_receiver(const ScrDyn *self, const char *name, bool getter, bool live) { - ScrDisposableStack *s = scr_ds_of(self); +ScrDisposableStack *scr_ds_receiver(const ScrDyn *self, ScrDynHandleTag tag, const char *name, bool getter, + bool live) { + ScrDisposableStack *s = scr_ds_of(self, tag); + const char *cls = tag == SCR_DYNH_DISPOSABLE_STACK ? "DisposableStack" : "AsyncDisposableStack"; ScrJsonBuf message; if (!s) { scr_jb_init(&message); - scr_jb_puts(&message, getter ? "Method get DisposableStack.prototype." : "Method DisposableStack.prototype."); + scr_jb_puts(&message, getter ? "Method get " : "Method "); + scr_jb_puts(&message, cls); + scr_jb_puts(&message, ".prototype."); scr_jb_puts(&message, name); scr_jb_puts(&message, " called on incompatible receiver "); scr_ds_put_value(&message, self); @@ -600,8 +582,11 @@ static ScrDisposableStack *scr_ds_receiver(const ScrDyn *self, const char *name, return NULL; } if (live && s->disposed) { + /* V8 names the base class at the end for both stack classes. */ scr_jb_init(&message); - scr_jb_puts(&message, "Cannot call DisposableStack.prototype."); + scr_jb_puts(&message, "Cannot call "); + scr_jb_puts(&message, cls); + scr_jb_puts(&message, ".prototype."); scr_jb_puts(&message, name); scr_jb_puts(&message, " on an already-disposed DisposableStack"); scr_throw_error(SCR_ERR_REFERENCE, scr_jb_finish(&message)); @@ -610,7 +595,7 @@ static ScrDisposableStack *scr_ds_receiver(const ScrDyn *self, const char *name, return s; } -static bool scr_ds_callable(const ScrDyn *value) { +bool scr_ds_callable(const ScrDyn *value) { if (scr_using_typeof_is(value, "function", 8)) return true; ScrJsonBuf message; scr_jb_init(&message); @@ -621,7 +606,7 @@ static bool scr_ds_callable(const ScrDyn *value) { } bool scr_disposable_stack_use(const ScrDyn *stack, const ScrDyn *value) { - ScrDisposableStack *s = scr_ds_receiver(stack, "use", false, true); + ScrDisposableStack *s = scr_ds_receiver(stack, SCR_DYNH_DISPOSABLE_STACK, "use", false, true); if (!s) return false; if (value->kind == SCR_DYN_NULL || value->kind == SCR_DYN_UNDEF) return true; ScrDyn *method = scr_dyn_using_method(value, false); @@ -631,21 +616,23 @@ bool scr_disposable_stack_use(const ScrDyn *stack, const ScrDyn *value) { } bool scr_disposable_stack_use_callback(const ScrDyn *stack, const ScrDyn *value, const ScrDyn *dispose) { - ScrDisposableStack *s = scr_ds_receiver(stack, "use", false, true); + ScrDynHandleTag tag = stack->kind == SCR_DYN_HANDLE && stack->v.handle.tag == SCR_DYNH_ASYNC_DISPOSABLE_STACK + ? SCR_DYNH_ASYNC_DISPOSABLE_STACK : SCR_DYNH_DISPOSABLE_STACK; + ScrDisposableStack *s = scr_ds_receiver(stack, tag, "use", false, true); if (!s) return false; scr_ds_push(s, scr_dyn_retain((ScrDyn *)value), scr_dyn_retain((ScrDyn *)dispose), SCR_DS_ADOPT); return true; } bool scr_disposable_stack_adopt(const ScrDyn *stack, const ScrDyn *value, const ScrDyn *on_dispose) { - ScrDisposableStack *s = scr_ds_receiver(stack, "adopt", false, true); + ScrDisposableStack *s = scr_ds_receiver(stack, SCR_DYNH_DISPOSABLE_STACK, "adopt", false, true); if (!s || !scr_ds_callable(on_dispose)) return false; scr_ds_push(s, scr_dyn_retain((ScrDyn *)value), scr_dyn_retain((ScrDyn *)on_dispose), SCR_DS_ADOPT); return true; } static bool scr_disposable_stack_defer(const ScrDyn *stack, const ScrDyn *on_dispose) { - ScrDisposableStack *s = scr_ds_receiver(stack, "defer", false, true); + ScrDisposableStack *s = scr_ds_receiver(stack, SCR_DYNH_DISPOSABLE_STACK, "defer", false, true); if (!s || !scr_ds_callable(on_dispose)) return false; scr_ds_push(s, NULL, scr_dyn_retain((ScrDyn *)on_dispose), SCR_DS_DEFER); return true; @@ -714,21 +701,16 @@ static ScrDyn *scr_ds_method_call(ScrClosure *clo, ScrDyn *const *args, size_t a break; case SCR_DS_M_MOVE: { /* The new stack takes the entries; this one becomes disposed. */ - ScrDisposableStack *s = scr_ds_receiver(self, "move", false, true); + ScrDisposableStack *s = scr_ds_receiver(self, SCR_DYNH_DISPOSABLE_STACK, "move", false, true); if (!s) break; ScrDisposableStack *moved = scr_ds_alloc(); - moved->items = s->items; - moved->len = s->len; - moved->cap = s->cap; - s->items = NULL; - s->len = s->cap = 0; - s->disposed = true; + scr_ds_move(moved, s); out = scr_ds_box(moved); scr_ds_release(moved); break; } default: { - ScrDisposableStack *s = scr_ds_receiver(self, "dispose", false, false); + ScrDisposableStack *s = scr_ds_receiver(self, SCR_DYNH_DISPOSABLE_STACK, "dispose", false, false); if (s) out = scr_ds_dispose(s); break; } @@ -803,6 +785,23 @@ static const ScrDynHandleOps scr_ds_ops = { .symbol_get = scr_ds_symbol_get, }; +ScrDisposableStack *scr_ds_storage_new(void) { + static ScrCycType cyc_type = {&scr_ds_trace, &scr_ds_gcfree, SCR_CYC_KIND_NONE, 0}; + ScrDisposableStack *s = scr_cyc_alloc(sizeof *s, &cyc_type); + s->rc = 1; + scr_obj_alloc_note(); + return s; +} + +void scr_ds_move(ScrDisposableStack *to, ScrDisposableStack *from) { + to->items = from->items; + to->len = from->len; + to->cap = from->cap; + from->items = NULL; + from->len = from->cap = 0; + from->disposed = true; +} + static ScrDisposableStack *scr_ds_alloc(void) { static SCR_TL bool installed; if (!installed) { @@ -810,11 +809,7 @@ static ScrDisposableStack *scr_ds_alloc(void) { scr_dyn_handle_install(SCR_DYNH_DISPOSABLE_STACK, &scr_ds_ops); scr_atexit(scr_ds_teardown); } - static ScrCycType cyc_type = {&scr_ds_trace, &scr_ds_gcfree, SCR_CYC_KIND_NONE, 0}; - ScrDisposableStack *s = scr_cyc_alloc(sizeof *s, &cyc_type); - s->rc = 1; - scr_obj_alloc_note(); - return s; + return scr_ds_storage_new(); } static ScrDyn *scr_ds_box(ScrDisposableStack *s) { return scr_dyn_new_handle(s, SCR_DYNH_DISPOSABLE_STACK); } diff --git a/tests/corpus/5902-async-disposable-stack.ts b/tests/corpus/5902-async-disposable-stack.ts new file mode 100644 index 000000000..7564d6803 --- /dev/null +++ b/tests/corpus/5902-async-disposable-stack.ts @@ -0,0 +1,156 @@ +// AsyncDisposableStack: disposeAsync interleaved with a microtask ticker (each +// awaited result, a throwing callback, absent resources, failures), class +// resources with [Symbol.asyncDispose] or [Symbol.dispose], await using over +// a stack, move, SuppressedError chains, and stacks whose callbacks capture +// the stack. Node.js prints the same. +const log: string[] = []; +async function ticker(label: string, n: number): Promise { + for (let i = 0; i < n; i++) { + log.push(label + i); + await null; + } +} +async function scenario(name: string, build: (s: AsyncDisposableStack) => void): Promise { + log.length = 0; + const s = new AsyncDisposableStack(); + build(s); + const tick = ticker("t", 14); + const p = s.disposeAsync().then( + () => log.push("resolved"), + (e: unknown) => log.push("rejected " + (e instanceof Error ? e.message : String(e))), + ); + log.push("sync end"); + await p; + await tick; + console.log(name + ": " + log.join(" ")); +} +async function timings(): Promise { + await scenario("empty", () => {}); + await scenario("one sync defer", (s) => s.defer(() => { log.push("d"); })); + await scenario("one async defer", (s) => s.defer(async () => { log.push("d"); })); + await scenario("two defers", (s) => { + s.defer(() => { log.push("d1"); }); + s.defer(() => { log.push("d2"); }); + }); + await scenario("sync use", (s) => { s.use({ [Symbol.dispose]() { log.push("u"); } }); }); + await scenario("async use", (s) => { s.use({ async [Symbol.asyncDispose]() { log.push("u"); } }); }); + await scenario("null use", (s) => { s.use(null); }); + await scenario("null then defer", (s) => { s.defer(() => { log.push("d"); }); s.use(null); }); + await scenario("defer then null", (s) => { s.use(undefined); s.defer(() => { log.push("d"); }); }); + await scenario("adopt", (s) => { s.adopt(3, (v) => { log.push("a" + v); }); }); + await scenario("slow async", (s) => s.defer(async () => { log.push("d start"); await null; await null; log.push("d end"); })); + await scenario("throws", (s) => s.defer(() => { throw new Error("boom"); })); + await scenario("rejects", (s) => s.defer(async () => { throw new Error("boom"); })); + await scenario("two failures", (s) => { + s.defer(() => { throw new Error("first"); }); + s.defer(async () => { throw new Error("second"); }); + }); + await scenario("returns promise", (s) => s.defer(() => Promise.resolve())); + await scenario("already disposed", (s) => { void s.disposeAsync(); }); +} + +const events: string[] = []; +class AsyncResource { + readonly name: string; + constructor(name: string) { + this.name = name; + } + async [Symbol.asyncDispose](): Promise { + events.push("async start " + this.name); + await null; + events.push("async end " + this.name); + } +} +class SyncResource { + readonly name: string; + constructor(name: string) { + this.name = name; + } + [Symbol.dispose](): void { + events.push("sync " + this.name); + } +} +class Both extends SyncResource { + async [Symbol.asyncDispose](): Promise { + events.push("both async " + this.name); + } +} +class FailingAsync { + async [Symbol.asyncDispose](): Promise { + throw new Error("async failure"); + } +} +async function eventTicker(n: number): Promise { + for (let i = 0; i < n; i++) { + events.push("t" + i); + await null; + } +} +async function main(): Promise { + const tick = eventTicker(10); + { + await using stack = new AsyncDisposableStack(); + const a = stack.use(new AsyncResource("a")); + stack.use(new SyncResource("s")); + stack.use(new Both("b")); + stack.adopt(new SyncResource("adopted"), async (r) => { + events.push("adopt " + r.name); + }); + stack.defer(() => { + events.push("defer"); + }); + events.push("got " + a.name + " " + stack.disposed); + } + events.push("after block"); + await tick; + console.log(events.join("\n")); + events.length = 0; + + const outer = new AsyncDisposableStack(); + outer.use(new SyncResource("moved")); + const moved = outer.move(); + console.log(outer.disposed, moved.disposed, moved instanceof AsyncDisposableStack, moved instanceof DisposableStack); + await moved.disposeAsync(); + await moved.disposeAsync(); + console.log(events.join(","), moved.disposed); + + const failing = new AsyncDisposableStack(); + failing.use(new FailingAsync()); + failing.defer(() => { + throw new TypeError("sync failure"); + }); + failing.use(new FailingAsync()); + try { + await failing.disposeAsync(); + } catch (e) { + const chain: string[] = []; + let cur: unknown = e; + while (cur instanceof SuppressedError) { + chain.push((cur.error as Error).message); + cur = cur.suppressed; + } + chain.push((cur as Error).message); + console.log(chain.join(" <- ")); + } + try { + await using s = new AsyncDisposableStack(); + s.defer(async () => { + throw new Error("disposal"); + }); + throw new Error("body"); + } catch (e) { + if (e instanceof SuppressedError) console.log(e.name, (e.error as Error).message, (e.suppressed as Error).message); + } + for (let i = 0; i < 3; i++) { + const cyc = new AsyncDisposableStack(); + cyc.defer(() => console.log("never", cyc.disposed)); + } + const shown = new AsyncDisposableStack(); + console.log(shown, String(shown), JSON.stringify(shown), typeof shown.disposeAsync); + const sync = new DisposableStack(); + sync.defer(() => console.log("sync stack inside async")); + await using wrapper = new AsyncDisposableStack(); + wrapper.use(sync); + console.log("end of main"); +} +void timings().then(() => main()); diff --git a/tests/corpus/5903-async-disposable-stack-js.js b/tests/corpus/5903-async-disposable-stack-js.js new file mode 100644 index 000000000..604231095 --- /dev/null +++ b/tests/corpus/5903-async-disposable-stack-js.js @@ -0,0 +1,48 @@ +// AsyncDisposableStack from JavaScript: untyped resources and callbacks, the +// order of async and sync disposals against a microtask ticker, detached +// methods, and Node's TypeError, ReferenceError and rejection texts. Node.js +// prints the same. +const messages = new AsyncDisposableStack(); +const t = (f) => { try { const r = f(); console.log("ok", r); } catch (e) { console.log(e.name + ": " + e.message); } }; +const { use, defer, move, disposeAsync, adopt } = messages; +t(() => use(null)); +t(() => defer.call({}, () => {})); +t(() => move.call(5)); +disposeAsync.call({}).catch((e) => console.log("rejected", e.name + ": " + e.message)); +t(() => messages.adopt(1, 2)); +t(() => messages.use(5)); +t(() => messages.use({})); +console.log(messages.disposeAsync.name, messages.disposeAsync.length, messages[Symbol.asyncDispose] === messages.disposeAsync, messages.use.length, messages.adopt.length, messages.defer.length, messages.move.length, typeof messages.dispose); +console.log(String(messages), JSON.stringify(messages), messages); +messages.disposeAsync().then(() => { t(() => messages.defer(() => {})); t(() => messages.move()); console.log(messages.disposed); }); +const m = new AsyncDisposableStack(); m.defer(() => console.log("moved ran")); const n = m.move(); console.log(m.disposed, n.disposed, n instanceof AsyncDisposableStack, n instanceof DisposableStack); n.disposeAsync(); +const order = []; +async function orderTicker(n) { for (let i = 0; i < n; i++) { order.push("t" + i); await null; } } +async function main() { + const tick = orderTicker(12); + const s = new AsyncDisposableStack(); + s.defer(() => { order.push("sync defer"); }); + s.defer(async () => { order.push("async defer start"); await null; order.push("async defer end"); }); + s.use({ [Symbol.dispose]() { order.push("sync use"); } }); + s.use({ async [Symbol.asyncDispose]() { order.push("async use"); } }); + s.adopt(1, (v) => { order.push("adopt " + v); }); + s.use(null); + const p = s.disposeAsync(); + order.push("returned " + (typeof p.then)); + await p; + order.push("done " + s.disposed); + await s.disposeAsync(); + order.push("again"); + await tick; + console.log(order.join("\n")); + const e = new AsyncDisposableStack(); + e.defer(async () => { throw new Error("a"); }); + e.defer(() => { throw new Error("b"); }); + try { await e.disposeAsync(); } catch (err) { console.log(err.name, err.error.message, err.suppressed.message); } + try { e.use({}); } catch (err) { console.log(err.name, err.message); } + const f = new AsyncDisposableStack(); void 0; console.log(typeof f.dispose, typeof f[Symbol.asyncDispose], String(f)); + try { new AsyncDisposableStack().use(3); } catch (err) { console.log(err.message); } + try { new AsyncDisposableStack().use({}); } catch (err) { console.log(err.message); } + console.log(new AsyncDisposableStack()); +} +main(); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index d8b28b23b..d202b4edb 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -1849,6 +1849,8 @@ "5859-disposable-stack-js.js": 3100, "5882-string-decoder-runtime-encoding.ts": 2541, "5883-os-set-priority.ts": 2380, + "5902-async-disposable-stack.ts": 3200, + "5903-async-disposable-stack-js.js": 3100, "5904-async-generator-completion.ts": 2950, "5905-array-from-async-generator.ts": 2750, "5906-array-from-async-generator-js.js": 2950, @@ -4496,6 +4498,8 @@ "5859-disposable-stack-js.js": 3600, "5882-string-decoder-runtime-encoding.ts": 3854, "5883-os-set-priority.ts": 3417, + "5902-async-disposable-stack.ts": 3550, + "5903-async-disposable-stack-js.js": 3500, "5904-async-generator-completion.ts": 3800, "5905-array-from-async-generator.ts": 3500, "5906-array-from-async-generator-js.js": 3750,