Merge opt/variadic-generic-wrappers into opt/override-variance-error-expando

Brings in #1433's review follow-up (now merged into #1413). In `key in
err`, own `stack` and Object.prototype keys answer true, keys built-in
errors keep outside their class fields stay fenced, and every other key
keeps this branch's answer: a declaring subclass or a runtime-set own
property.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-20 02:14:27 -07:00
co-authored by Claude Opus 5.5
12 changed files with 123 additions and 36 deletions
@@ -1,4 +1,5 @@
import { lowerReadlineCursorCall } from "./builtins/readline.js";
import { lowerMonitorEventLoopDelayCall } from "./builtins/performance.js";
import {
lowerEnvironmentData,
lowerReceiveMessageOnPort,
@@ -69,6 +70,9 @@ export function lowerBuiltinCall(
return lowerAddAbortListener(lowerer, expr, loc);
}
if (bi.module === "events" && bi.member === "once") return lowerEventsOnce(lowerer, expr, loc);
if (bi.module === "perf_hooks" && bi.member === "monitorEventLoopDelay") {
return lowerMonitorEventLoopDelayCall(lowerer, expr);
}
if (bi.module === "events") {
const helper = lowerEventsEmitterHelper(lowerer, expr, bi.member, loc);
if (helper) return helper;
@@ -18,6 +18,7 @@ import {
} from "../../../ir/ir.js";
import { PERF_ENTRY_FIELDS, PERF_ENTRY_MARKER, perfEntryJsonShape } from "../../type-mapper.js";
import { builtinImportOf } from "./module-bindings.js";
import { lowerOptionalArgument } from "../optional-arguments.js";
import { checkedIterableSpread } from "../checked-iterable-spread.js";
/** True for any spelling of node:perf_hooks' `performance` object — the
@@ -663,21 +664,11 @@ export function lowerHistogramMethodCall(
);
}
/** `monitorEventLoopDelay(options?)` from node:perf_hooks (a named import
* or a namespace member): a new sampling histogram handle. The resolution
* must be a literal-free number expression or an object literal option. */
export function lowerMonitorEventLoopDelayCall(
lowerer: Lowerer,
expr: ts.CallExpression,
): IrExpr | null {
const callee = expr.expression;
const bi = ts.isIdentifier(callee)
? builtinImportOf(lowerer, callee)
: ts.isPropertyAccessExpression(callee) && !callee.questionDotToken
? lowerer.builtinMemberOf(callee)
: null;
if (bi === null || bi.module !== "perf_hooks" || bi.member !== "monitorEventLoopDelay")
return null;
/** `monitorEventLoopDelay(options?)` from node:perf_hooks, in any import
* spelling (`lowerBuiltinCall`): a new sampling histogram handle. The
* options are an inline object literal; the runtime validates the
* resolution as Node does (an integer of at least 1). */
export function lowerMonitorEventLoopDelayCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
const loc = locOf(expr);
const args = expr.arguments.nodes;
let resolution: IrExpr = numLit(10, loc);
@@ -698,8 +689,9 @@ export function lowerMonitorEventLoopDelayCall(
);
}
for (const property of options.properties.nodes) {
const shorthand = ts.isShorthandPropertyAssignment(property);
if (
!ts.isPropertyAssignment(property) ||
(!shorthand && !ts.isPropertyAssignment(property)) ||
!ts.isIdentifier(property.name) ||
property.name.text !== "resolution"
) {
@@ -709,7 +701,13 @@ export function lowerMonitorEventLoopDelayCall(
"resolution is the one option",
);
}
resolution = lowerer.lowerExprExpecting(property.initializer, F64);
// An undefined resolution takes the default, as Node's destructuring.
resolution = lowerOptionalArgument(
lowerer,
shorthand ? property.name : property.initializer,
F64,
numLit(10, loc),
);
}
}
return { kind: "libCall", fn: "perf.eldNew", args: [resolution], type: F64, loc };
@@ -1144,6 +1144,24 @@ function lowerPrivateIn(
}
/** The string keys every record inherits from Object.prototype. */
/** Keys built-in errors carry outside their class fields: Node's system
* error properties, and AggregateError's, SuppressedError's and
* AssertionError's own members. */
const BUILTIN_ERROR_SLOT_KEYS: ReadonlySet<string> = new Set([
"errno",
"syscall",
"path",
"dest",
"info",
"errors",
"error",
"suppressed",
"actual",
"expected",
"operator",
"generatedMessage",
]);
const OBJECT_PROTOTYPE_KEYS: ReadonlySet<string> = new Set([
"constructor",
"__defineGetter__",
@@ -1418,6 +1436,9 @@ export function lowerInExpression(
loc,
};
if (key === "message" || key === "name") return always();
// Every Error has its own `stack`, and Error.prototype's `toString`
// and `constructor` are among Object.prototype's keys.
if (key === "stack" || OBJECT_PROTOTYPE_KEYS.has(key)) return always();
// A member the static class declares is always present.
const declares = (c: ClassInfo): boolean =>
c.fields.has(key) ||
@@ -1426,12 +1447,12 @@ export function lowerInExpression(
lowerer.findMethodOn(c, `set:${key}`) !== null;
const staticInfo = lowerer.classes.get(recv.type.className);
if (staticInfo && declares(staticInfo)) return always();
// Any other key: a descendant class that declares it (instanceof),
// Any other key: a descendant class that declares it (instanceof), or
// an own property set at runtime (a throw site's extra fields,
// Object.assign, writes through a dynamic view), or an
// Object.prototype member. `stack` stays fenced: Node has one, a
// compiled error has none.
if (key !== "stack") {
// Object.assign, writes through a dynamic view). Keys built-in errors
// keep outside their class fields depend on the runtime error and
// stay fenced.
if (!BUILTIN_ERROR_SLOT_KEYS.has(key)) {
const stable = recv.kind === "varRef" || recv.kind === "caughtNarrow";
const slot = stable ? null : lowerer.declareHiddenLocal("%inRecv", recv.type);
const ref = (): IrExpr => (slot ? varRef(slot.id, recv.type, loc) : recv);
@@ -151,7 +151,6 @@ import {
fencePerfEntryKeyWalk,
isPerfEntryType,
lowerHistogramMethodCall,
lowerMonitorEventLoopDelayCall,
lowerPerfHooksCall,
perfEntryTag,
} from "./builtins/performance.js";
@@ -254,16 +253,6 @@ const ZLIB_STREAM_METHODS: ReadonlySet<string> = new Set(["flush", "params", "cl
export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
const loc = locOf(expr);
// node:perf_hooks monitorEventLoopDelay (named import or namespace member).
if (
(ts.isIdentifier(expr.expression) && expr.expression.text === "monitorEventLoopDelay") ||
(ts.isPropertyAccessExpression(expr.expression) &&
expr.expression.name.text === "monitorEventLoopDelay")
) {
const histogram = lowerMonitorEventLoopDelayCall(lowerer, expr);
if (histogram) return histogram;
}
// Object's checker return type is `any`. Claim known object inputs
// before generic checked-dynamic dispatch tries to load the constructor.
if (
@@ -366,6 +366,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"perf.entries",
"perf.clearMarks",
"perf.clearMeasures",
// monitorEventLoopDelay's resolution: Node's ERR_OUT_OF_RANGE.
"perf.eldNew",
// Non-TTY stdin: Node's stdin has no setRawMode member, so the call is
// the exact catchable TypeError.
"process.stdinSetRawMode",
@@ -365,7 +365,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
/** perf_hooks event-loop metrics (scr_perf_loop.c): loopStart is the
* loop's start in performance.now() ms (0 before it), idleTime the idle
* ms since. eldNew(resolution ms) creates a monitorEventLoopDelay
* histogram handle; enable/disable answer whether the state changed;
* histogram handle (MAY THROW Node's resolution RangeError); enable/disable answer whether the state changed;
* eldPercentile expects Node's validated range; eldStat(which) reads
* 0 min, 1 max, 2 mean, 3 stddev, 4 count, 5 exceeds. */
"perf.loopStart": { argTypes: [], result: F64 },
@@ -54,6 +54,11 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [
["webcrypto.", "WebCrypto (crypto.subtle)"],
["fs.watch", "fs.watch"],
["watcher.", "fs.watch"],
// The event-loop metrics sample the loop and its timers (scr_perf_loop.c),
// which library links exclude.
["perf.loopStart", "performance.eventLoopUtilization"],
["perf.idleTime", "performance.eventLoopUtilization"],
["perf.eld", "perf_hooks.monitorEventLoopDelay"],
["test.", "the node:test surface"],
["readable.", "the node:stream surface"],
["writable.", "the node:stream surface"],
@@ -158,6 +163,7 @@ export function moduleLibAsyncSurface(mod: IrModule): { surface: string; loc: Sr
[features.nodeTest, "the node:test surface"],
[features.dynAsync, "the checked-dynamic async surface"],
[features.dc, "the diagnostics_channel surface"],
[features.perfLoop, "the perf_hooks event-loop metrics"],
];
for (const [on, surface] of coarse) {
if (on) return { surface, loc: entryLoc };
+23 -1
View File
@@ -21,7 +21,9 @@
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef _WIN32
#include <windows.h>
@@ -212,7 +214,27 @@ static void scr_eld_cleanup(void) {
scr_elds_len = 0;
}
/* Node's validateInteger(resolution, 'options.resolution', 1): a catchable
* RangeError (ERR_OUT_OF_RANGE) for a non-integer or a value out of range,
* with the value as util.inspect renders it ("-0" included). */
static bool scr_eld_resolution_ok(double resolution) {
bool integer = isfinite(resolution) && trunc(resolution) == resolution;
if (integer && resolution >= 1 && resolution <= 9007199254740991.0) return true;
char received[48];
if (resolution == 0 && signbit(resolution))
memcpy(received, "-0", 3);
else
scr_num_received(resolution, received);
char msg[192];
int len = snprintf(msg, sizeof msg,
"The value of \"options.resolution\" is out of range. It must be %s. Received %s",
integer ? ">= 1 && <= 9007199254740991" : "an integer", received);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
return false;
}
double scr_eld_new(double resolution) {
if (!scr_eld_resolution_ok(resolution)) return 0;
if (scr_elds_len == 0 && scr_elds == NULL) scr_atexit(scr_eld_cleanup);
ScrEld *h = calloc(1, sizeof(ScrEld));
ScrEld **grown = realloc(scr_elds, (scr_elds_len + 1) * sizeof(ScrEld *));
@@ -223,7 +245,7 @@ double scr_eld_new(double resolution) {
}
scr_elds = grown;
h->min_value = INT64_MAX;
h->resolution = resolution >= 1 ? resolution : 10;
h->resolution = resolution;
h->tick = scr_closure_new((void *)scr_eld_tick, 0);
scr_elds[scr_elds_len++] = h;
return (double)scr_elds_len;
@@ -23,3 +23,9 @@ function retryable(err: Error): boolean {
}
console.log(retryable(new Error("x")), retryable(new HttpError("a", 500)), retryable(new Timeout("t", 504)));
console.log(describe(new Timeout("t", 504)));
// Keys every Error has: its own stack, and Error.prototype's and
// Object.prototype's members.
function inherent(err: Error): boolean[] {
return ["stack" in err, "toString" in err, "constructor" in err, "hasOwnProperty" in err];
}
console.log(inherent(new Error("s")), inherent(new HttpError("h", 400)), inherent(new TypeError("t")));
+21
View File
@@ -1,6 +1,8 @@
// Event-loop monitoring under @types/node: monitorEventLoopDelay's histogram,
// eventLoopUtilization deltas, and an unref'd interval typed NodeJS.Timeout.
import { monitorEventLoopDelay, performance } from "node:perf_hooks";
import * as perfHooks from "node:perf_hooks";
import { monitorEventLoopDelay as eld } from "node:perf_hooks";
const histogram = monitorEventLoopDelay({ resolution: 10 });
console.log("enable", histogram.enable(), histogram.enable());
@@ -47,3 +49,22 @@ const interval = setInterval(() => {
}, 50);
interval.unref();
console.log("hasRef", interval.hasRef());
// Other import spellings, and Node's resolution validation.
const aliased = eld();
const namespaced = perfHooks.monitorEventLoopDelay({ resolution: 20 });
console.log("spellings", aliased.enable(), namespaced.enable(), aliased.disable(), namespaced.disable());
for (const resolution of [0, -0, -1, 1.5, NaN, Infinity, 1e16, 1]) {
try {
eld({ resolution });
console.log("created", resolution);
} catch (e) {
console.log((e as Error).name, (e as { code?: string }).code, (e as Error).message);
}
}
// An undefined resolution takes Node's default.
function withResolution(resolution?: number): boolean {
const h = monitorEventLoopDelay({ resolution });
return h.enable() && h.disable();
}
console.log("defaults", withResolution(), withResolution(20), eld({ resolution: undefined }).enable());
+15
View File
@@ -73,6 +73,21 @@ describe("async_free detection over the IR", () => {
);
});
test("the event-loop metrics refuse", async () => {
expect(
await surfaceOf(
"eld",
`import { monitorEventLoopDelay } from "node:perf_hooks";\nconst h = monitorEventLoopDelay();\nconsole.log(h.enable());\n`,
),
).toContain("monitorEventLoopDelay");
expect(
await surfaceOf(
"elu",
`import { performance } from "node:perf_hooks";\nconsole.log(performance.eventLoopUtilization().idle >= 0);\n`,
),
).toContain("eventLoopUtilization");
});
test("child_process refuses even in its synchronous spelling", async () => {
expect(
await surfaceOf(
+4 -1
View File
@@ -569,10 +569,13 @@ describe("library profile fences", () => {
expect(processIds).toContain("node-builtin.process.isTTY");
expect(r.profile.fences[1]!.surfaces.every((s) => s.detector !== undefined)).toBe(true);
// The user-timing timeline reads the same clock (mark() and measure()
// default to performance.now()), so it joins the perf_hooks family.
// default to performance.now()), and so do the event-loop metrics, so
// they join the perf_hooks family.
expect(r.profile.fences[2]!.surfaces.map((s) => s.id)).toEqual([
"node-builtin.perf_hooks.monitorEventLoopDelay",
"node-builtin.perf_hooks.performance.clearMarks",
"node-builtin.perf_hooks.performance.clearMeasures",
"node-builtin.perf_hooks.performance.eventLoopUtilization",
"node-builtin.perf_hooks.performance.getEntries",
"node-builtin.perf_hooks.performance.mark",
"node-builtin.perf_hooks.performance.measure",