Merge main

This commit is contained in:
Chris Tate
2026-10-20 02:49:54 -05:00
32 changed files with 224 additions and 47 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 };
@@ -322,7 +322,9 @@ export function funcCoerceAdapter(
),
];
} else if (checkedRet) {
body = [{ kind: "return", value: { kind: "dynCheck", value: call, type: toT.ret, loc }, loc }];
body = [
{ kind: "return", value: { kind: "dynCheck", value: call, type: toT.ret, loc }, loc },
];
} else {
const result = lowerer.coerceToExpected(call, toT.ret);
if (!typeEquals(result.type, toT.ret))
@@ -144,7 +144,6 @@ import {
fencePerfEntryKeyWalk,
isPerfEntryType,
lowerHistogramMethodCall,
lowerMonitorEventLoopDelayCall,
lowerPerfHooksCall,
perfEntryTag,
} from "./builtins/performance.js";
@@ -243,16 +242,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 (
@@ -13835,6 +13835,24 @@ const OBJECT_PROTOTYPE_KEYS: ReadonlySet<string> = new Set([
* idiom over multiple shapes), index-signature keys, class instances,
* and dyn/unknown stay fenced. Keys are literal strings — a computed key
* over a shape would need the runtime key table. */
/** 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",
]);
function lowerInExpression(lowerer: Lowerer, expr: ts.BinaryExpression, loc: SrcLoc): IrExpr {
if (
lowerer.isStdlibGlobal(expr.right, "process") &&
@@ -14018,9 +14036,10 @@ function lowerInExpression(lowerer: Lowerer, expr: ts.BinaryExpression, loc: Src
// predicate's `err instanceof Error && "code" in err` shape): `code`
// answers from the runtime error's code slot (stamped by fs/system/
// event/spawn errors, absent on plain constructions — exactly Node's
// own-property answer), and the always-declared members fold true.
// Everything else fences: the runtime object carries no other dynamic
// properties to ask (`stack` would be a lie — Node has one, we don't).
// own-property answer), and the always-present members (message, name,
// stack, Object.prototype's keys, members the static class declares)
// fold true. Other keys test the declaring subclasses and the error's
// own runtime properties; keys of built-in errors' extra slots fence.
if (recv.type.kind === "object") {
// %DOMException first: `'cause' in e` answers the options form's
// own-property record (the runtime slot), and code/name/message are
@@ -14076,6 +14095,9 @@ function lowerInExpression(lowerer: Lowerer, expr: ts.BinaryExpression, loc: Src
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) ||
@@ -14084,12 +14106,12 @@ function lowerInExpression(lowerer: Lowerer, expr: ts.BinaryExpression, loc: Src
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);
@@ -1350,7 +1350,8 @@ function lowerLabeled(lowerer: Lowerer, stmt: ts.LabeledStatement): IrStmt | IrS
}
export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt[] | null {
if (ownPrototypeResetStatement(lowerer, stmt)) return { kind: "block", body: [], loc: locOf(stmt) };
if (ownPrototypeResetStatement(lowerer, stmt))
return { kind: "block", body: [], loc: locOf(stmt) };
if (ts.isClassDeclaration(stmt)) {
if (!stmt.name) lowerer.unsupported("SC1090", stmt, "anonymous local class declarations");
const info = lowerer.lowerClassExpressionInfo(stmt);
@@ -10924,7 +10925,13 @@ function resetTarget(lowerer: Lowerer, expr: ts.Expression): ts.Expression | nul
function enclosingConstructor(node: ts.Node): ts.ConstructorDeclaration | null {
for (let cur = node.parent; cur !== undefined; cur = cur.parent) {
if (ts.isConstructorDeclaration(cur)) return cur;
if (ts.isFunctionLike(cur) || (ts.isClassDeclaration(cur) || ts.isClassExpression(cur)) || ts.isSourceFile(cur)) return null;
if (
ts.isFunctionLike(cur) ||
ts.isClassDeclaration(cur) ||
ts.isClassExpression(cur) ||
ts.isSourceFile(cur)
)
return null;
}
return null;
}
@@ -10947,7 +10954,8 @@ function ownPrototypeRef(
)
return true;
const cls = ctor.parent;
if (!ts.isIdentifier(base) || !(ts.isClassDeclaration(cls) || ts.isClassExpression(cls))) return false;
if (!ts.isIdentifier(base) || !(ts.isClassDeclaration(cls) || ts.isClassExpression(cls)))
return false;
const symbol = lowerer.resolveValueSymbol(base);
const info = symbol ? lowerer.classBySymbol.get(symbol) : undefined;
if (!info || lowerer.checker.valueDeclarationOf(symbol!) !== cls) return false;
+17 -2
View File
@@ -5146,11 +5146,15 @@ function mapGenericIndexedAccess(type: ts.Type, ctx: TypeMapperCtx): IrType | nu
const obj = type.getObjectType();
if (obj === undefined || !(obj.flags & ts.TypeFlags.TypeParameter)) return null;
const bound = resolveTypeParam(obj);
const idx = type.getIndexType();
if (idx === undefined) return null;
// `T[keyof T]` over a T bound to an unknown-valued record view
// (`Record<string, unknown>` maps to the checked-dynamic value, see
// mapRecordType): every member reads as unknown.
if (bound?.kind === "dyn" && idx.isIndexType() && idx.getTarget() === obj) return DYN;
if (!bound || bound.kind !== "record") return null;
const shape = shapes.get(bound.shapeId);
if (!shape || shape.tuple) return null;
const idx = type.getIndexType();
if (idx === undefined) return null;
let covered: IrType[] | null = null;
if (idx.isStringLiteralType()) {
const f = shape.fields.find((x) => x.name === idx.value);
@@ -5206,6 +5210,17 @@ function mapBoundKeyofNeverConditional(type: ts.Type, ctx: TypeMapperCtx): IrTyp
const target = check.getTarget();
if (!target || !(target.flags & ts.TypeFlags.TypeParameter)) return null;
const bound = resolveTypeParam(target);
// A T bound to the checked-dynamic value (`Record<string, unknown>`, an
// unknown-valued record view) has no static key set to decide the
// branch, but both branches of the lib's `keyof T extends never ?
// unknown : T[keyof T]` read as unknown there: one answer either way.
if (bound?.kind === "dyn") {
const whenEmpty = type.getTrueType();
const otherwise = type.getFalseType();
const a = whenEmpty ? mapType(whenEmpty, ctx) : null;
const b = otherwise ? mapType(otherwise, ctx) : null;
return a && b && typeEquals(a, b) ? a : null;
}
if (bound?.kind !== "record") return null;
const shape = shapes.get(bound.shapeId);
if (!shape || shape.tuple) return null;
@@ -363,6 +363,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",
@@ -364,7 +364,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 },
+2 -2
View File
@@ -506,11 +506,11 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
}
if (fn.startsWith("dgram.") || fn.startsWith("dns.")) features.dgram = true;
if (fn.startsWith("tlsca.")) features.tlsCa = true;
if (fn.startsWith("perf.loop") || fn === "perf.idleTime" || fn.startsWith("perf.eld"))
features.perfLoop = true;
// scr_cipher.c over the mbedTLS archive; DEP0182 reports through the
// process-warning unit, and unknown output encodings render through
// the inspect unit.
if (fn.startsWith("perf.loop") || fn === "perf.idleTime" || fn.startsWith("perf.eld"))
features.perfLoop = true;
if (fn === "crypto.cipherNew") {
features.cipher = true;
features.dynAsync = true;
@@ -53,6 +53,11 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [
["crypto.cipherNew", "crypto.createCipheriv and createDecipheriv"],
["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"],
@@ -157,6 +162,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 };
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6410-wrapper-untyped-results.ts"
],
"diags": []
}
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/6411-prototype-reset-constructors/issues.js",
"<repo>/tests/corpus/6411-prototype-reset-constructors/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6468-generic-unknown-record-entries.ts"
],
"diags": []
}
+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());
@@ -0,0 +1,33 @@
// Inside a generic body, `Object.entries(tags)` over `T extends
// Record<string, unknown>` reads `[string, T[keyof T]]` elements. An
// instance whose T is the unknown-valued record view reads every member
// as unknown; a record instance reads the union of its member types.
function upper(text: string): string {
return text.toUpperCase();
}
function scrub(text: string): string {
return text.replace(/secret/g, "***");
}
function describe<T extends Record<string, unknown>>(tags: T): string[] {
return Object.entries(tags).map(
([key, value]) => `${key}=${typeof value === "string" ? upper(value) : String(value)}`,
);
}
function redact<T extends Record<string, unknown>>(tags: T): T {
return Object.fromEntries(
Object.entries(tags).map(([key, value]) => [key, typeof value === "string" ? scrub(value) : value]),
) as T;
}
function flatten(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : { value };
}
console.log(describe(flatten({ b: 1, a: "x", c: true })));
console.log(describe(flatten(5)));
console.log(describe({ name: "n", size: 3 }));
const cleaned = redact(flatten({ token: "secret", count: 2 }));
console.log(cleaned, Object.keys(cleaned));
@@ -0,0 +1 @@
{"plain":4713}
@@ -0,0 +1 @@
{"plain":3445}
@@ -0,0 +1 @@
{"plain":5309}
@@ -0,0 +1 @@
{"plain":3632}
@@ -0,0 +1 @@
{"plain":4787}
@@ -0,0 +1 @@
{"plain":4127}
@@ -0,0 +1 @@
{"plain":4693}
@@ -0,0 +1 @@
{"plain":3803}
@@ -0,0 +1 @@
{"plain":3820}
@@ -0,0 +1 @@
{"plain":3726}
@@ -0,0 +1 @@
{"plain":3837}
@@ -0,0 +1 @@
{"plain":4711}
+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",