mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
25e40afa83
commit
e71a1763bb
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -82,6 +82,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"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<string>([
|
||||
// The wrappers schedule warnings and callbacks on the tick queue.
|
||||
"util.deprecate",
|
||||
"util.callbackify",
|
||||
"util.promisifyDyn",
|
||||
"util.debuglog",
|
||||
"worker.new",
|
||||
"worker.parentPort",
|
||||
|
||||
@@ -46,6 +46,7 @@ export interface NativeLinkFeatures {
|
||||
bigint: boolean;
|
||||
searchParams: boolean;
|
||||
qs: boolean;
|
||||
utilPromisify: boolean;
|
||||
parseArgs: boolean;
|
||||
stream: boolean;
|
||||
net: boolean;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -107,6 +107,7 @@ const features = {
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
utilPromisify: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
|
||||
@@ -44,6 +44,7 @@ const BASE: NativeLinkFeatures = {
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
utilPromisify: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
|
||||
@@ -50,6 +50,7 @@ export async function warmNativeCaches(
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
utilPromisify: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
|
||||
@@ -45,6 +45,7 @@ const native: EarlyExecutableNativeFeatures = {
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
utilPromisify: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -503,6 +503,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = 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.
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -61,6 +61,7 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = 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,
|
||||
|
||||
@@ -89,6 +89,7 @@ async function main(): Promise<number> {
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
utilPromisify: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
|
||||
@@ -143,6 +143,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures {
|
||||
"bigint",
|
||||
"searchParams",
|
||||
"qs",
|
||||
"utilPromisify",
|
||||
"parseArgs",
|
||||
"stream",
|
||||
"net",
|
||||
|
||||
@@ -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,
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6452-promisify-untyped-functions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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")],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 <stdlib.h>
|
||||
|
||||
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", "");
|
||||
}
|
||||
@@ -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<void> {
|
||||
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();
|
||||
@@ -30,6 +30,7 @@ const features: NativeLinkFeatures = {
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
utilPromisify: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -31,6 +31,7 @@ const base: NativeLinkFeatures = {
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
utilPromisify: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
|
||||
Reference in New Issue
Block a user