mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Store builtin timer functions as real function values in JavaScript
`var nextTick = typeof setImmediate === "function" ? setImmediate : setTimeout` (bcryptjs) and `cond ? setImmediate : process.nextTick` stored the builtin as an identity token string, so typeof answered "string" and calling it threw "nextTick is not a function". The JavaScript identifier, `globalThis.X` and destructuring spellings of setTimeout, setInterval, setImmediate, clearTimeout, clearInterval, clearImmediate and queueMicrotask, and a stored process.nextTick, now read one interned runtime function per name. Conditional, logical and assigned values carry it like any function: typeof is "function", identity holds across spellings, and calls validate the callback (ERR_INVALID_ARG_TYPE), coerce the delay, pass the extra arguments and return the same numeric handles the direct calls return, which the stored clear functions accept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9fab4348d3
commit
acfdc7fe13
@@ -5,6 +5,8 @@ Some builtins can be stored and passed as functions:
|
||||
- `util.isDeepStrictEqual`, `styleText`, `stripVTControlCharacters`, `toUSVString`, `inherits`, `deprecate`, `callbackify`, `debuglog`, and `debug`.
|
||||
- `fs.existsSync`, `unlinkSync`, `chmodSync`, `chownSync`, `renameSync`, and `closeSync`, and `fs/promises.unlink`, `chmod`, and `rename`.
|
||||
|
||||
In JavaScript, `setTimeout`, `setInterval`, `setImmediate`, `clearTimeout`, `clearInterval`, `clearImmediate`, `queueMicrotask`, and `process.nextTick` are function values wherever they are stored, including conditional and logical expressions and assignments: `typeof` answers `"function"`, identity holds across spellings, and calls schedule as the direct calls do, returning the same numeric timer handles. Calls through these values bypass `node:test` mock timers. Other builtin globals stored in JavaScript remain opaque identity values.
|
||||
|
||||
Other immutable aliases of table-backed Node builtins support direct calls, but cannot be passed elsewhere. `util.promisify` supports compile-time conversions of `child_process.execFile`, `child_process.exec`, `setTimeout` and `setImmediate` (to the `timers/promises` functions, as in Node), `stream.finished` and `stream.pipeline` (to the `stream/promises` functions, as in Node), the `zlib` callback one-shots `deflate`, `inflate`, `deflateRaw`, `inflateRaw`, `gzip`, `gunzip` and `unzip` (with or without options), the `crypto` callback functions `randomBytes`, `pbkdf2`, `scrypt`, `hkdf`, `randomFill`, `randomInt` and `generateKey`, `dns.lookup` (settling with `{ address, family }`, or the address array with `all: true`) and `dns.lookupService` (settling with `{ hostname, service }`), and the `fs` callback functions that have an `fs/promises` counterpart (`access`, `appendFile`, `chmod`, `chown`, `copyFile`, `lchown`, `link`, `lstat`, `lutimes`, `mkdir`, `mkdtemp`, `readFile`, `readdir`, `readlink`, `realpath`, `rename`, `rm`, `rmdir`, `stat`, `statfs`, `symlink`, `truncate`, `unlink`, `utimes`, and `writeFile`); calls through the converted function lower as the `fs/promises` call.
|
||||
|
||||
`util.styleText` supports standard formats and aliases, `#RGB` and `#RRGGBB` hex colors, arrays of formats, nested ANSI resets, and explicit color enabling with `{ validateStream: false }`. Native calls accept standard process streams; custom native streams and mutations to `util.inspect.colors` remain unsupported.
|
||||
|
||||
@@ -900,6 +900,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.nativeUrlIs": "scr_dyn_native_url_is",
|
||||
"dyn.nativeDateIs": "scr_dyn_native_date_is",
|
||||
"dyn.globalConstructor": "scr_dyn_global_constructor",
|
||||
"dyn.builtinFunction": "scr_dyn_builtin_function",
|
||||
"date.nativeNew": "scr_dyn_native_date_new",
|
||||
"date.nativeNewFields": "scr_dyn_native_date_new_fields",
|
||||
"date.checkedValue": "scr_dyn_native_date_value",
|
||||
@@ -1612,4 +1613,5 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"timers.queueMicrotaskDyn",
|
||||
"timers.setImmediateFnValue",
|
||||
"timers.immediatePromise",
|
||||
"dyn.builtinFunction",
|
||||
]);
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
funcOf,
|
||||
} from "../../../ir/ir.js";
|
||||
import { lowerBuiltinLoaderValue } from "../lower-builtin-values.js";
|
||||
import { builtinFunctionValue } from "../lower-exprs.js";
|
||||
import { lowerProcessIpcSend, ipcMessageListener, ipcDisconnectListener } from "./ipc.js";
|
||||
import { lowerDiagnosticsSubscriber, lowerTracingArguments } from "./async-context.js";
|
||||
|
||||
@@ -385,6 +386,11 @@ export function lowerProcessProperty(
|
||||
if (member === "hrtime") {
|
||||
return { kind: "libCall", fn: "process.hrtimeValue", args: [], type: DYN, loc };
|
||||
}
|
||||
// A stored process.nextTick in JavaScript (`cond ? setImmediate :
|
||||
// process.nextTick`): the interned builtin function value.
|
||||
if (member === "nextTick" && isJsSourceFile(expr.getSourceFile())) {
|
||||
return builtinFunctionValue("process.nextTick", loc);
|
||||
}
|
||||
if (member === "getBuiltinModule") {
|
||||
// Keep the native loader's argument validation when the function is
|
||||
// stored. The declaration's string signature must not insert a checked
|
||||
|
||||
@@ -1453,8 +1453,12 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
const canonical = stdlibGlobalNameOf(lowerer, expr) ?? expr.text;
|
||||
// Builtin constructors have real function values in both source
|
||||
// languages (TypeScript reads them as dynamic values).
|
||||
const constructorValue = builtinGlobalValue(canonical, loc);
|
||||
if (constructorValue !== null) return constructorValue;
|
||||
const builtinValue = builtinGlobalValue(
|
||||
canonical,
|
||||
loc,
|
||||
isJsSourceFile(expr.getSourceFile()),
|
||||
);
|
||||
if (builtinValue !== null) return builtinValue;
|
||||
if (isJsSourceFile(expr.getSourceFile())) {
|
||||
return { kind: "strLit", value: `[builtin ${canonical}]`, type: STRING, loc };
|
||||
}
|
||||
@@ -2354,7 +2358,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
loc,
|
||||
};
|
||||
return (
|
||||
builtinGlobalValue(canonical, loc) ?? {
|
||||
builtinGlobalValue(canonical, loc, true) ?? {
|
||||
kind: "strLit",
|
||||
value: `[builtin ${canonical}]`,
|
||||
type: STRING,
|
||||
@@ -16330,9 +16334,36 @@ const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet<string> = new Set([
|
||||
"WeakSet",
|
||||
]);
|
||||
|
||||
/** The value a JavaScript source reads for the stdlib global `canonical`,
|
||||
* or null when it stays an identity token. */
|
||||
export function builtinGlobalValue(canonical: string, loc: SrcLoc): IrExpr | null {
|
||||
/** The builtin global functions JavaScript sources read as interned
|
||||
* function values (the `cond ? setImmediate : process.nextTick` feature
|
||||
* probe of bcryptjs and async libraries): typeof answers "function",
|
||||
* identity holds across spellings, and calls run the timer APIs. */
|
||||
const GLOBAL_FUNCTION_VALUES: ReadonlySet<string> = new Set([
|
||||
"setTimeout",
|
||||
"setInterval",
|
||||
"setImmediate",
|
||||
"clearTimeout",
|
||||
"clearInterval",
|
||||
"clearImmediate",
|
||||
"queueMicrotask",
|
||||
]);
|
||||
|
||||
/** A builtin function value: one interned runtime function per name. */
|
||||
export function builtinFunctionValue(name: string, loc: SrcLoc): IrExpr {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "dyn.builtinFunction",
|
||||
args: [{ kind: "strLit", value: name, type: STRING, loc }],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The value a source reads for the stdlib global `canonical` (builtin
|
||||
* constructors in both languages, builtin functions in JavaScript), or
|
||||
* null when it stays an identity token (JavaScript) or fenced. */
|
||||
export function builtinGlobalValue(canonical: string, loc: SrcLoc, js = false): IrExpr | null {
|
||||
if (js && GLOBAL_FUNCTION_VALUES.has(canonical)) return builtinFunctionValue(canonical, loc);
|
||||
if (GLOBAL_CONSTRUCTOR_VALUES.has(canonical)) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
|
||||
@@ -3337,7 +3337,7 @@ function stdlibGlobalTokenDestructure(
|
||||
alias = canonical;
|
||||
// Builtin constructors read as their interned function values, the
|
||||
// same values the bare and `globalThis.X` spellings read.
|
||||
value = builtinGlobalValue(canonical, locOf(el));
|
||||
value = builtinGlobalValue(canonical, locOf(el), true);
|
||||
} else if (globalName === "console" && !CONSOLE_CALL_MEMBERS.has(propName)) {
|
||||
token = `[builtin console.${propName}]`;
|
||||
} else if (TOKEN_OPAQUE_GLOBALS.has(globalName)) {
|
||||
|
||||
@@ -210,6 +210,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* interned function per name, the same one dynamic values answer for
|
||||
* `.constructor`. The name is a compiler-chosen literal. */
|
||||
"dyn.globalConstructor": { argTypes: [STRING], result: DYN },
|
||||
/** A builtin global function (setTimeout, setInterval, setImmediate,
|
||||
* the clear functions, queueMicrotask, process.nextTick) read as a
|
||||
* JavaScript value: one interned function per name whose calls run the
|
||||
* timer APIs (it pulls in the event loop). The name is a compiler-chosen
|
||||
* literal. */
|
||||
"dyn.builtinFunction": { argTypes: [STRING], result: DYN },
|
||||
"date.nativeNew": { argTypes: [DYN], result: DYN },
|
||||
/** `new Date(year, monthIndex, ...)` in JavaScript: the argument array's
|
||||
* first seven values pass through ToNumber in order (later ones are
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6236-builtin-function-values.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -3717,6 +3717,102 @@ ScrDyn *scr_set_immediate_dyn_value(void) {
|
||||
1, "(cb,...)", "setImmediate");
|
||||
}
|
||||
|
||||
/* ── Builtin global functions as dynamic values ─────────────────────────
|
||||
* A JavaScript source that stores a builtin global function (the
|
||||
* `var nextTick = typeof setImmediate === "function" ? setImmediate :
|
||||
* setTimeout` feature probe) reads one interned function per name, so
|
||||
* typeof answers "function" and `===` is identity. Calls through it run
|
||||
* the timer and tick APIs with Node's argument contract: a function
|
||||
* callback (else ERR_INVALID_ARG_TYPE), the delay through ToNumber, the
|
||||
* remaining arguments passed to the callback. Timer functions answer the
|
||||
* same numeric handles the direct calls answer, which the clear functions
|
||||
* accept. A name without a dynamic call path throws a named fence. */
|
||||
static SCR_TL ScrDyn *scr_builtin_fn_table;
|
||||
|
||||
static void scr_builtin_fn_cleanup(void) {
|
||||
scr_dyn_release(scr_builtin_fn_table);
|
||||
scr_builtin_fn_table = NULL;
|
||||
}
|
||||
|
||||
static bool scr_builtin_fn_callback(ScrDyn *const *args, size_t argc) {
|
||||
if (argc > 0 && args[0]->kind == SCR_DYN_FUNC) return true;
|
||||
static const char msg[] = "The \"callback\" argument must be of type function.";
|
||||
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_ARG_TYPE");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* The closure that calls args[0] with args[first..] when the loop fires it. */
|
||||
static ScrClosure *scr_builtin_fn_deferred(ScrDyn *const *args, size_t argc, size_t first) {
|
||||
ScrDyn *rest = scr_dyn_new_arr();
|
||||
for (size_t i = first; i < argc; i++) scr_dyn_arr_push(rest, scr_dyn_retain(args[i]));
|
||||
ScrClosure *c = scr_closure_new((void *)scr_imm_dyn_fire, 2);
|
||||
c->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL);
|
||||
scr_box_set_ref(c->caps[0], scr_dyn_retain(args[0]));
|
||||
c->caps[1] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL);
|
||||
scr_box_set_ref(c->caps[1], rest); /* moves */
|
||||
return c;
|
||||
}
|
||||
|
||||
static ScrDyn *scr_builtin_fn_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) {
|
||||
ScrStr *name = scr_box_get_ref(clo->caps[0]);
|
||||
ScrDyn *result = NULL;
|
||||
bool timeout = strcmp(name->data, "setTimeout") == 0;
|
||||
if (timeout || strcmp(name->data, "setInterval") == 0) {
|
||||
double ms = 0;
|
||||
if (scr_builtin_fn_callback(args, argc) && (argc < 2 || scr_dyn_number_coerce_js(args[1], &ms))) {
|
||||
ScrClosure *c = scr_builtin_fn_deferred(args, argc, 2);
|
||||
result = scr_dyn_new_num(timeout ? scr_set_timeout_handle(c, ms) : scr_set_interval(c, ms));
|
||||
}
|
||||
} else if (strcmp(name->data, "setImmediate") == 0) {
|
||||
if (scr_builtin_fn_callback(args, argc))
|
||||
result = scr_dyn_new_num(scr_set_immediate(scr_builtin_fn_deferred(args, argc, 1)));
|
||||
} else if (strcmp(name->data, "process.nextTick") == 0) {
|
||||
if (scr_builtin_fn_callback(args, argc)) {
|
||||
scr_next_tick(scr_builtin_fn_deferred(args, argc, 1));
|
||||
result = scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
} else if (strcmp(name->data, "queueMicrotask") == 0) {
|
||||
if (scr_builtin_fn_callback(args, argc)) {
|
||||
scr_queue_microtask_dyn(args[0]);
|
||||
result = scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
} else if (strcmp(name->data, "clearTimeout") == 0 || strcmp(name->data, "clearInterval") == 0 ||
|
||||
strcmp(name->data, "clearImmediate") == 0) {
|
||||
/* Node ignores anything that is not a live handle. */
|
||||
if (argc > 0 && args[0]->kind == SCR_DYN_NUM) {
|
||||
if (name->data[5] == 'I' && name->data[6] == 'm') scr_clear_immediate(args[0]->v.num);
|
||||
else scr_clear_interval(args[0]->v.num);
|
||||
}
|
||||
result = scr_dyn_retain(scr_dyn_undefined());
|
||||
} else {
|
||||
char message[160];
|
||||
int length = snprintf(message, sizeof message, "calling '%s' through a stored value is not supported yet", name->data);
|
||||
scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)length, "SC2020");
|
||||
}
|
||||
scr_str_release(name);
|
||||
return result;
|
||||
}
|
||||
|
||||
ScrDyn *scr_dyn_builtin_function(const ScrStr *name) {
|
||||
if (!scr_builtin_fn_table) {
|
||||
scr_builtin_fn_table = scr_dyn_new_obj_null_proto();
|
||||
scr_atexit(scr_builtin_fn_cleanup);
|
||||
}
|
||||
ScrDyn *value = scr_dyn_obj_get(scr_builtin_fn_table, name->data, name->len);
|
||||
if (!value) {
|
||||
ScrClosure *closure = scr_closure_new(NULL, 1);
|
||||
closure->caps[0] = scr_box_new(SCR_BOX_STR);
|
||||
scr_box_set_ref(closure->caps[0], scr_str_retain((ScrStr *)name));
|
||||
const char *dot = memchr(name->data, '.', name->len);
|
||||
size_t arity = strcmp(name->data, "queueMicrotask") == 0 || strncmp(name->data, "clear", 5) == 0 ? 1 :
|
||||
strcmp(name->data, "setImmediate") == 0 || strcmp(name->data, "process.nextTick") == 0 ? 1 : 2;
|
||||
value = scr_dyn_new_func(closure, scr_builtin_fn_call, arity, "native:builtin",
|
||||
dot ? dot + 1 : name->data);
|
||||
scr_dyn_obj_set(scr_builtin_fn_table, name->data, name->len, value); /* the table owns it */
|
||||
}
|
||||
return scr_dyn_retain(value);
|
||||
}
|
||||
|
||||
/* ── queueMicrotask ──────────────────────────────────────────────────
|
||||
* The READY queue's FIFO is the microtask queue (promise continuations
|
||||
* ride it as parked fibers), so a queueMicrotask callback enters the SAME
|
||||
|
||||
@@ -655,6 +655,12 @@ ScrDyn *scr_dyn_array_constructor(void);
|
||||
/* The interned builtin constructor `name` (Object, Date, RegExp, Map, Set,
|
||||
* Promise, WeakMap, WeakSet) as a dynamic function value: +1. */
|
||||
ScrDyn *scr_dyn_global_constructor(const ScrStr *name);
|
||||
/* A builtin global function (setTimeout, setImmediate, process.nextTick, ...)
|
||||
* as an interned dynamic function value: +1. */
|
||||
ScrDyn *scr_dyn_builtin_function(const ScrStr *name);
|
||||
/* Object.prototype's valueOf/toString/hasOwnProperty/propertyIsEnumerable as
|
||||
* interned function values (+1), or NULL for other names. */
|
||||
ScrDyn *scr_dyn_object_proto_method(const char *name);
|
||||
bool scr_dyn_arr_has_index(const ScrDyn *arr, size_t index);
|
||||
void scr_dyn_arr_push_hole(ScrDyn *arr);
|
||||
ScrDyn *scr_dyn_new_obj(void);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// Builtin timer functions stored as values (bcryptjs's `nextTick` feature
|
||||
// probe, async libraries' scheduler selection): conditional, logical and
|
||||
// assigned values are real functions — typeof answers "function", identity
|
||||
// holds across spellings, and calls schedule with Node's argument contract
|
||||
// and ordering.
|
||||
var nextTick =
|
||||
typeof setImmediate === "function"
|
||||
? setImmediate
|
||||
: typeof queueMicrotask === "function"
|
||||
? queueMicrotask
|
||||
: setTimeout;
|
||||
const pick = process.argv.length > 100;
|
||||
const later = pick ? setImmediate : process.nextTick;
|
||||
const timeout = pick ? setImmediate : setTimeout;
|
||||
const micro = pick || queueMicrotask;
|
||||
let assigned = setTimeout;
|
||||
assigned = pick ? assigned : setImmediate;
|
||||
const { setInterval: every, clearInterval: stop } = globalThis;
|
||||
console.log(typeof nextTick, typeof later, typeof timeout, typeof micro, typeof assigned, typeof every);
|
||||
console.log(nextTick === setImmediate, later === process.nextTick, timeout === setTimeout, micro === queueMicrotask, assigned === globalThis.setImmediate, every === setInterval, stop === clearInterval);
|
||||
console.log(nextTick.name, later.name, timeout.name, setTimeout.length, queueMicrotask.length);
|
||||
|
||||
const order = [];
|
||||
nextTick(function () { order.push("immediate:" + arguments.length); });
|
||||
later((a, b) => order.push("tick:" + a + b), 1, 2);
|
||||
// Microtask and nextTick queues order differently in CommonJS and ESM
|
||||
// entry evaluation, so the microtask reports through its own flag.
|
||||
let microRan = false;
|
||||
micro(() => { microRan = true; });
|
||||
timeout((x) => order.push("timeout:" + x), 60, "x");
|
||||
assigned(() => order.push("assigned"));
|
||||
const cancelled = timeout(() => order.push("cancelled"), 1);
|
||||
(pick ? clearImmediate : clearTimeout)(cancelled);
|
||||
let count = 0;
|
||||
const handle = every(() => {
|
||||
count++;
|
||||
order.push("interval:" + count);
|
||||
if (count === 2) stop(handle);
|
||||
}, 20);
|
||||
timeout(() => console.log(order.join(" "), "micro:" + microRan), 120);
|
||||
|
||||
for (const bad of [undefined, 1, "fn"]) {
|
||||
try {
|
||||
later(bad);
|
||||
} catch (e) {
|
||||
console.log("rejected", typeof bad, e.code, e instanceof TypeError);
|
||||
}
|
||||
}
|
||||
const log = pick ? console.error : console.log;
|
||||
log("log", typeof log);
|
||||
const mx = pick ? Math.min : Math.max;
|
||||
console.log(mx(1, 3, 2), typeof mx);
|
||||
Reference in New Issue
Block a user