From e71a1763bb80efc050dd077f8f5e16658eccdd7b Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Mon, 19 Oct 2026 23:17:06 -0700 Subject: [PATCH] Promisify untyped functions at run time util.promisify(fn) over a function typed any (an npm package method typed by its JavaScript, a bound method of an untyped value) was refused, because the promisified signature is only known at run time. A native wrapper now calls the function with the call's receiver, its arguments and a callback, and answers a promise: a truthy error rejects, otherwise the first value fulfills, the first settlement wins, and a synchronous throw rejects. util.promisify.custom functions are not consulted. The wrapper lives in its own runtime unit (scr_util_promisify.c), linked by the new utilPromisify feature, so other programs are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../src/backend/executable-features.ts | 1 + packages/compiler/src/backend/library-pack.ts | 1 + .../compiler/src/backend/llvm/lib-shared.ts | 2 + .../src/backend/native-link-info-core.ts | 1 + .../compiler/src/backend/native/contracts.ts | 4 + .../compiler/src/backend/native/executable.ts | 2 + .../src/backend/runtime-pack-targets.test.ts | 1 + .../compiler/src/backend/runtime-pack.test.ts | 1 + packages/compiler/src/backend/warm-cache.ts | 1 + .../src/executable/executable-cache.test.ts | 1 + .../src/executable/executable-cache.ts | 2 + .../lowering/builtins/callable-aliases.ts | 8 +- .../lowering/builtins/promisify-callback.ts | 21 ++++- packages/compiler/src/index.ts | 1 + packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 3 + packages/compiler/src/ir/runtime-features.ts | 8 +- packages/compiler/src/native/cli.ts | 1 + packages/compiler/src/native/prepare.ts | 1 + .../compiler/src/native/sanitizer.test.ts | 1 + .../6452-promisify-untyped-functions.ts.json | 6 ++ .../runtime-pack-matrix.mjs | 2 + packages/runtime/src/scr_qs.h | 4 +- packages/runtime/src/scr_util_promisify.c | 77 +++++++++++++++++++ .../6452-promisify-untyped-functions.ts | 47 +++++++++++ .../self-hosting-native-toolchain.test.ts | 1 + .../self-hosting-runtime-features.test.ts | 1 + .../harness/self-hosting-runtime-pack.test.ts | 1 + 28 files changed, 196 insertions(+), 5 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6452-promisify-untyped-functions.ts.json create mode 100644 packages/runtime/src/scr_util_promisify.c create mode 100644 tests/corpus/6452-promisify-untyped-functions.ts diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index eeb6c6900..cba658b00 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -31,6 +31,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { bigint: features.bigint, searchParams: features.searchParams, qs: features.qs, + utilPromisify: features.utilPromisify, parseArgs: features.parseArgs, stream: features.stream, net: features.net, diff --git a/packages/compiler/src/backend/library-pack.ts b/packages/compiler/src/backend/library-pack.ts index 5070a47a8..4037f082a 100644 --- a/packages/compiler/src/backend/library-pack.ts +++ b/packages/compiler/src/backend/library-pack.ts @@ -42,6 +42,7 @@ export async function compilePackedLibrary( bigint: options.bigint ?? false, searchParams: options.searchParams ?? false, qs: false, + utilPromisify: false, parseArgs: false, stream: false, net: false, diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index e48d52160..5ccb29d67 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -82,6 +82,7 @@ export const LIB_FN_SYMS: Record = { "util.inherits": "scr_util_inherits", "util.deprecate": "scr_util_deprecate", "util.callbackify": "scr_util_callbackify", + "util.promisifyDyn": "scr_util_promisify", "util.debuglog": "scr_util_debuglog", "util.typeIs": "scr_dyn_util_type_is", "util.typeValue": "scr_dyn_util_type_value", @@ -1497,6 +1498,7 @@ export const USES_TIMERS_LIB_FNS = new Set([ // The wrappers schedule warnings and callbacks on the tick queue. "util.deprecate", "util.callbackify", + "util.promisifyDyn", "util.debuglog", "worker.new", "worker.parentPort", diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 86146259d..091696986 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -46,6 +46,7 @@ export interface NativeLinkFeatures { bigint: boolean; searchParams: boolean; qs: boolean; + utilPromisify: boolean; parseArgs: boolean; stream: boolean; net: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 33b13960e..11096a08c 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -150,6 +150,10 @@ export interface CcOptions { * line (escape-only programs ride the always-linked component encoder * and never flip this). */ qs?: boolean; + /** The program promisifies an untyped function (util.promisify over + * `any`): compiles scr_util_promisify.c, which also references the + * dyn-async and dyn-invoke units. */ + utilPromisify?: boolean; /** The program uses native util parsing, comparison, or styling. Optional * utility units also require their symbol, inspection, and warning features. */ parseArgs?: boolean; diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 5ebb4796e..18891798d 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -235,6 +235,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ (u) => u.opts.parseArgs && u.opts.inspect && u.opts.dynAsync && u.opts.dynInvoke && u.regex, "scr_util_callable.c", ), + // util.promisify over an untyped function: dyn promises and calls. + unit((u) => u.opts.utilPromisify && u.opts.dynAsync && u.opts.dynInvoke, "scr_util_promisify.c"), unit((u) => u.opts.stream, "scr_stream.c", "scr_fs_stream.c"), // The zlib stream classes reference both the zlib engine and the // stream unit. diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index fd57cb49d..2b083b59e 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -107,6 +107,7 @@ const features = { bigint: false, searchParams: false, qs: false, + utilPromisify: false, parseArgs: false, stream: false, net: false, diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index 61d1dba5e..80317ce2b 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -44,6 +44,7 @@ const BASE: NativeLinkFeatures = { bigint: false, searchParams: false, qs: false, + utilPromisify: false, parseArgs: false, stream: false, net: false, diff --git a/packages/compiler/src/backend/warm-cache.ts b/packages/compiler/src/backend/warm-cache.ts index 2cd111060..cdea8a52c 100644 --- a/packages/compiler/src/backend/warm-cache.ts +++ b/packages/compiler/src/backend/warm-cache.ts @@ -50,6 +50,7 @@ export async function warmNativeCaches( bigint: false, searchParams: false, qs: false, + utilPromisify: false, parseArgs: false, stream: false, net: false, diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index 1b42c3eca..2767bedac 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -45,6 +45,7 @@ const native: EarlyExecutableNativeFeatures = { bigint: false, searchParams: false, qs: false, + utilPromisify: false, parseArgs: false, stream: false, net: false, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 8c67c1006..e842520e6 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -71,6 +71,7 @@ export interface EarlyExecutableNativeFeatures { bigint: boolean; searchParams: boolean; qs: boolean; + utilPromisify: boolean; parseArgs: boolean; stream: boolean; net: boolean; @@ -185,6 +186,7 @@ const BOOLEAN_NATIVE_KEYS = [ "bigint", "searchParams", "qs", + "utilPromisify", "parseArgs", "stream", "net", diff --git a/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts b/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts index afb221d04..8724827e4 100644 --- a/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts +++ b/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts @@ -6,7 +6,7 @@ import { registerHttpClientFnBinding } from "../lower-server.js"; import { isPromisifiedZlibMember } from "./compression.js"; import { isPromisifiedCryptoMember } from "./crypto.js"; import { isPromisifiedDnsMember } from "../lower-dgram.js"; -import { callbackPromisifyShape } from "./promisify-callback.js"; +import { callbackPromisifyShape, untypedPromisifyArgument } from "./promisify-callback.js"; /** Compile-time util.promisify projections. The immutable result binding * owns no runtime slot: calls rewrite to the target's promise lowering, @@ -167,7 +167,11 @@ export function registerPromisifiedBuiltinDecl( : null; // A typed callback-last function value: the call lowers to a native // promisified function, and the binding holds it like any closure. - if (!projection && callbackPromisifyShape(lowerer, e, locOf(e)) !== null) return false; + if ( + !projection && + (callbackPromisifyShape(lowerer, e, locOf(e)) !== null || untypedPromisifyArgument(lowerer, e)) + ) + return false; if (!projection) { lowerer.noLowering( "util.promisify of this target", diff --git a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts index 693079d3f..9e10000ee 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts @@ -3,6 +3,7 @@ import { type Lowerer } from "../lowerer.js"; import { varRef } from "../../../ir/build.js"; import { BOOL, + DYN, type IrExpr, type IrLocal, type IrParam, @@ -90,6 +91,14 @@ function callbackShape( return { fnT, resultT, args, callbackT, callbackParamT: callbackParamT!, inner, error }; } +/** Whether `promisify(fn)`'s one argument is typed `any`: the function is + * only known at run time, so the promisified function is built there. */ +export function untypedPromisifyArgument(lowerer: Lowerer, call: ts.CallExpression): boolean { + if (call.questionDotToken || call.arguments.nodes.length !== 1) return false; + const argNode = call.arguments.nodes[0]!; + return !ts.isSpreadElement(argNode) && (lowerer.typeOf(argNode).flags & ts.TypeFlags.Any) !== 0; +} + /** The callback shape of a `promisify(fn)` call, or null. */ export function callbackPromisifyShape( lowerer: Lowerer, @@ -131,7 +140,17 @@ export function lowerCallbackPromisify( loc: SrcLoc, ): IrExpr | null { const shape = callbackPromisifyShape(lowerer, call, loc); - if (shape === null) return null; + if (shape === null) { + // An untyped (`any`) function: Node's promisify at run time. + if (!untypedPromisifyArgument(lowerer, call)) return null; + return { + kind: "libCall", + fn: "util.promisifyDyn", + args: [lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN)], + type: DYN, + loc, + }; + } const argNode = call.arguments.nodes[0]!; let fn = lowerer.lowerExpr(argNode); if (!typeEquals(fn.type, shape.fnT)) fn = lowerer.coerceToExpected(fn, shape.fnT); diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 7d8d88889..c1ec92b11 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -543,6 +543,7 @@ async function compileExecutableNative( bigint: features.bigint, searchParams: features.searchParams, qs: features.qs, + utilPromisify: features.utilPromisify, parseArgs: features.parseArgs, stream: features.stream, net: features.net, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 7f709f099..1adac1d56 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -503,6 +503,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "util.inherits", "util.deprecate", "util.callbackify", + "util.promisifyDyn", "util.debuglog", // The URI codecs throw the spec's URIError: decoding on bad hex/invalid // UTF-8 octets, encoding (and querystring.escape) on a lone surrogate. diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 7f2748341..5de86f637 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1004,6 +1004,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "util.inherits": { argTypes: [DYN, DYN], result: VOID }, "util.deprecate": { argTypes: [DYN, DYN, DYN, DYN], result: DYN }, "util.callbackify": { argTypes: [DYN], result: DYN }, + /** util.promisify over an untyped function value: a native dyn function + * whose calls answer a dyn promise the appended callback settles. */ + "util.promisifyDyn": { argTypes: [DYN], result: DYN }, "util.debuglog": { argTypes: [DYN, DYN], result: DYN }, "util.typeIs": { argTypes: [DYN, STRING], result: BOOL }, "util.typeValue": { argTypes: [DYN, STRING], result: DYN }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 4d59a6241..a5dcfe9a2 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -61,6 +61,7 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "util.inherits", "util.deprecate", "util.callbackify", + "util.promisifyDyn", "util.debuglog", "als.new", "als.get", @@ -493,7 +494,7 @@ const RUNTIME_FEATURE_RULES = { }), dynInvoke: rule({ initial: (mod) => mod.workers === true, - fns: [...DYN_INVOKE_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS], + fns: [...DYN_INVOKE_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS, "util.promisifyDyn"], expr: (node, _tables) => node.kind === "dynInvoke", }), /** The program has dynamic-tier jsval operations (js.* builtins, the @@ -610,6 +611,10 @@ const RUNTIME_FEATURE_RULES = { }), tls: rule({ prefixes: ["tls.", "https.", "http2."], types: ["secureCtx"] }), tlsCa: rule({ prefixes: ["tlsca."] }), + /** util.promisify over an untyped function (scr_util_promisify.c): its + * promises go through the dyn-async unit and its calls through the + * dyn-invoke unit. */ + utilPromisify: rule({ fns: ["util.promisifyDyn"] }), /** process.emitWarning is assigned (process.setEmitWarning). */ warningReplace: rule({ fns: ["process.setEmitWarning"] }), /** crypto.subtle importKey/sign/verify/digest and CryptoKey: @@ -662,6 +667,7 @@ function noRuntimeFeatures(): RuntimeFeatures { symbol: false, tls: false, tlsCa: false, + utilPromisify: false, warningReplace: false, webcrypto: false, zlib: false, diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index 7d00acbc8..7cb191622 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -89,6 +89,7 @@ async function main(): Promise { bigint: false, searchParams: false, qs: false, + utilPromisify: false, parseArgs: false, stream: false, net: false, diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index 54100a413..0335ace48 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -143,6 +143,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures { "bigint", "searchParams", "qs", + "utilPromisify", "parseArgs", "stream", "net", diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index 6ca84af6f..fd7572db6 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -95,6 +95,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( bigint: false, searchParams: false, qs: false, + utilPromisify: false, parseArgs: false, stream: false, net: false, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6452-promisify-untyped-functions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6452-promisify-untyped-functions.ts.json new file mode 100644 index 000000000..bf040a1ec --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6452-promisify-untyped-functions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6452-promisify-untyped-functions.ts" + ], + "diags": [] +} diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 1e959eccc..be8a5632c 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -88,6 +88,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_util_compare.c", all("parseArgs", "symbol")], ["scr_util_style.c", all("parseArgs", "inspect", "dynAsync")], ["scr_util_callable.c", all("parseArgs", "inspect", "dynAsync", "dynInvoke", "regex")], + // util.promisify over an untyped function creates dyn promises and calls. + ["scr_util_promisify.c", all("utilPromisify", "dynAsync", "dynInvoke")], ["scr_stream.c", "stream"], // The zlib stream classes reference both the zlib engine and the stream unit. ["scr_zlib_stream.c", all("zlib", "stream")], diff --git a/packages/runtime/src/scr_qs.h b/packages/runtime/src/scr_qs.h index 3d00dfae6..fc5ca5da5 100644 --- a/packages/runtime/src/scr_qs.h +++ b/packages/runtime/src/scr_qs.h @@ -58,12 +58,14 @@ ScrStr *scr_util_style_text(const struct ScrDyn *format, const struct ScrDyn *te * once the stream is a terminal (scr_util_style.c). */ bool scr_util_terminal_colors(void); /* util.inherits, util.deprecate, util.callbackify and util.debuglog - * (scr_util_callable.c). Arguments are borrowed; function results are +1 + * (scr_util_callable.c), and util.promisify over an untyped function + * (scr_util_promisify.c). Arguments are borrowed; function results are +1 * native dyn functions, or NULL with Node's TypeError pending. */ void scr_util_inherits(struct ScrDyn *ctor, struct ScrDyn *super_ctor); struct ScrDyn *scr_util_deprecate(struct ScrDyn *fn, struct ScrDyn *msg, struct ScrDyn *code, struct ScrDyn *options); struct ScrDyn *scr_util_callbackify(struct ScrDyn *original); +struct ScrDyn *scr_util_promisify(struct ScrDyn *original); struct ScrDyn *scr_util_debuglog(struct ScrDyn *section, struct ScrDyn *callback); /* Node/libuv system error utilities. Arguments are checked without coercion; diff --git a/packages/runtime/src/scr_util_promisify.c b/packages/runtime/src/scr_util_promisify.c new file mode 100644 index 000000000..7ca250a92 --- /dev/null +++ b/packages/runtime/src/scr_util_promisify.c @@ -0,0 +1,77 @@ +/* util.promisify over an untyped function. The wrapper is a native dyn + * function: each call creates a promise, calls the original with the + * call's receiver and arguments plus an error-first callback, and answers + * the promise. Typed functions keep the compiler's static wrapper; this + * unit links only into programs that promisify an `any` value. All + * arguments are borrowed. */ +#include "scr_runtime.h" + +#include + +static ScrBox *up_box(ScrDyn *value /* moves in */) { + ScrBox *box = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + scr_box_set_ref(box, value); + return box; +} + +static ScrDyn *up_cap(ScrClosure *closure, size_t index) { + return scr_box_get_ref(closure->caps[index]); /* +1 */ +} + +/* ── util.promisify over an untyped function ──────────────────────────── + * The wrapper calls original with the call's receiver and arguments plus a + * callback, and answers a promise the callback settles: a truthy first + * argument rejects with it, otherwise the second argument fulfills. A + * synchronous throw rejects, as Node's executor does. The first settlement + * wins. util.promisify.custom functions are not consulted. */ +static ScrDyn *up_callback(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrDyn *settles = up_cap(closure, 0); + ScrPromise *promise = scr_dyn_promise_of(settles); + if (promise && scr_promise_is_pending(promise)) { + ScrDyn *err = argc ? args[0] : scr_dyn_undefined(); + if (scr_dyn_truthy(err)) { + scr_dyn_throw(scr_dyn_retain(err)); + scr_promise_reject_pending(promise); + } else { + scr_promise_resolve_dyn(promise, scr_dyn_retain(argc > 1 ? args[1] : scr_dyn_undefined())); + } + } + scr_dyn_release(settles); + return scr_dyn_retain(scr_dyn_undefined()); +} + +static ScrDyn *up_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrDyn *original = up_cap(closure, 0); + ScrDyn *receiver = scr_dyn_this_get(); + ScrPromise *promise = scr_promise_new(); + ScrDyn *result = scr_dyn_new_promise(promise); + ScrClosure *settle = scr_closure_new(NULL, 1); + settle->caps[0] = up_box(scr_dyn_retain(result)); + ScrDyn *callback = scr_dyn_new_func(settle, up_callback, 2, "native:util.promisify", ""); + ScrDyn **full = malloc((argc + 1) * sizeof *full); + if (!full) scr_trap("out of memory"); + for (size_t i = 0; i < argc; i++) full[i] = args[i]; + full[argc] = callback; + scr_dyn_this_push_dyn(receiver); + ScrDyn *returned = scr_dyn_call(original, full, argc + 1, "function"); + scr_dyn_this_pop(); + free(full); + if (returned) scr_dyn_release(returned); + else if (scr_promise_is_pending(promise)) scr_promise_reject_pending(promise); + else scr_exc_clear(); + scr_dyn_release(callback); + scr_dyn_release(receiver); + scr_dyn_release(original); + scr_promise_release(promise); + return result; +} + +ScrDyn *scr_util_promisify(ScrDyn *original) { + if (!scr_dyn_is_callable(original)) { + scr_dyn_arg_type_fail("original", "of type function", original); + return NULL; + } + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = up_box(scr_dyn_retain(original)); + return scr_dyn_new_func(closure, up_call, 0, "native:util.promisify", ""); +} diff --git a/tests/corpus/6452-promisify-untyped-functions.ts b/tests/corpus/6452-promisify-untyped-functions.ts new file mode 100644 index 000000000..aaae0ee3b --- /dev/null +++ b/tests/corpus/6452-promisify-untyped-functions.ts @@ -0,0 +1,47 @@ +// util.promisify over untyped (`any`) functions: the callback's error +// rejects, its first value fulfills, a synchronous throw rejects, only the +// first settlement counts, and the call's receiver reaches the function. +import { promisify } from "node:util"; + +const client: any = { + name: "db", + close(callback: any) { + setImmediate(() => callback()); + }, + fetch(key: any, callback: any) { + setImmediate(() => (key ? callback(null, `${this.name}:${key}`, "ignored") : callback(new Error("no key")))); + }, + twice(callback: any) { + callback(null, "first"); + callback(new Error("second")); + }, + boom(_callback: any) { + throw new TypeError("synchronous failure"); + }, +}; + +async function main(): Promise { + await promisify(client.close.bind(client))(); + console.log("closed"); + const fetchAsync = promisify(client.fetch.bind(client)); + console.log(await fetchAsync("a")); + try { + await fetchAsync(""); + } catch (error) { + console.log("rejected:", (error as Error).message); + } + console.log("twice:", await promisify(client.twice)()); + try { + await promisify(client.boom)(); + } catch (error) { + console.log("sync:", (error as Error).name, (error as Error).message); + } + const unbound = promisify(client.fetch); + console.log("receiver:", await unbound.call(client, "b")); + const order: string[] = []; + const pending = promisify(client.close.bind(client))().then(() => order.push("settled")); + order.push("returned"); + await pending; + console.log(order.join(", ")); +} +void main(); diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index fa71b5a08..8b399e288 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -30,6 +30,7 @@ const features: NativeLinkFeatures = { bigint: false, searchParams: false, qs: false, + utilPromisify: false, parseArgs: false, stream: false, net: false, diff --git a/tests/harness/self-hosting-runtime-features.test.ts b/tests/harness/self-hosting-runtime-features.test.ts index d2c2b589b..7e11316e5 100644 --- a/tests/harness/self-hosting-runtime-features.test.ts +++ b/tests/harness/self-hosting-runtime-features.test.ts @@ -50,6 +50,7 @@ const featureCalls: readonly [string, IrLibFn][] = [ ["bigint", "bigint.parse"], ["searchParams", "sp.new"], ["qs", "qs.parse"], + ["utilPromisify", "util.promisifyDyn"], ["parseArgs", "util.parseArgs"], ["parseArgs", "util.isDeepStrictEqual"], ["parseArgs", "util.styleText"], diff --git a/tests/harness/self-hosting-runtime-pack.test.ts b/tests/harness/self-hosting-runtime-pack.test.ts index c95d6d9a0..6cd004295 100644 --- a/tests/harness/self-hosting-runtime-pack.test.ts +++ b/tests/harness/self-hosting-runtime-pack.test.ts @@ -31,6 +31,7 @@ const base: NativeLinkFeatures = { bigint: false, searchParams: false, qs: false, + utilPromisify: false, parseArgs: false, stream: false, net: false,