Files
scriptc/packages/compiler/src/ir/runtime-features.ts
T
2026-10-28 11:33:39 -07:00

1701 lines
63 KiB
TypeScript

import type { IrClassDef, IrExpr, IrModule, IrRecordShape, IrType, IrUnionDef } from "./ir.js";
import type { IrLibFn } from "./builtin-signatures.js";
import {
isDirentShape,
recordHandleCodecClass,
recordTextCodecClass,
RUNTIME_EMITTER_CLASS,
RUNTIME_EVENT_CLASS,
RUNTIME_EVENT_TARGET_CLASS,
RUNTIME_STREAM_CLASSES,
} from "./ir.js";
import { everyModuleNode, everyStmtList } from "./traverse.js";
import { eachNodeOfKinds, type IrNodeIndex } from "./node-index.js";
import { JsIcSites, hasJsIcSite } from "./js-ic.js";
const DYN_BUFFER_NUM_METHODS: ReadonlySet<string> = new Set(
["read", "write"].flatMap((rw) => [
...["UInt", "Uint"].flatMap((u) =>
["8", "16LE", "16BE", "32LE", "32BE"].map((w) => `${rw}${u}${w}`),
),
...[
"Int8",
"Int16LE",
"Int16BE",
"Int32LE",
"Int32BE",
"FloatLE",
"FloatBE",
"DoubleLE",
"DoubleBE",
].map((n) => `${rw}${n}`),
]),
);
/** Buffer.prototype members scr_bytes_dyn.c refuses on Buffer receivers. */
const DYN_BUFFER_REFUSED_METHODS: readonly string[] = [
...["read", "write"].flatMap((rw) => [
...["UIntLE", "UIntBE", "UintLE", "UintBE", "IntLE", "IntBE"].map((n) => `${rw}${n}`),
...["BigUInt64", "BigUint64", "BigInt64"].flatMap((n) => [`${rw}${n}LE`, `${rw}${n}BE`]),
]),
...["ascii", "base64", "base64url", "latin1", "hex", "ucs2", "utf8"].flatMap((enc) => [
`${enc}Slice`,
`${enc}Write`,
]),
"inspect",
];
/** The scr_bytes_dyn.c entry, one per method family, that a checked-dynamic
* call of a Buffer.prototype method name routes to: the fixed-width reads and
* writes (both "UInt" and "Uint" spellings), write, fill, toString, the
* searches, and the other Buffer-only members. Each runs the method on a
* Buffer receiver and gives any other receiver the ordinary dispatch. */
export const DYN_BUFFER_METHODS: ReadonlyMap<string, string> = new Map([
...[...DYN_BUFFER_NUM_METHODS].map((m): [string, string] => [m, "scr_dyn_invoke_buffer_num"]),
["write", "scr_dyn_invoke_buffer_write"],
["fill", "scr_dyn_invoke_buffer_fill"],
["toString", "scr_dyn_invoke_buffer_to_string"],
...["indexOf", "lastIndexOf", "includes"].map((m): [string, string] => [
m,
"scr_dyn_invoke_buffer_search",
]),
...[
"toJSON",
"equals",
"compare",
"copy",
"swap16",
"swap32",
"swap64",
...DYN_BUFFER_REFUSED_METHODS,
].map((m): [string, string] => [m, "scr_dyn_invoke_buffer"]),
]);
const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
// DEP0040 rides the dyn-async unit's warning machinery.
"async.awaitDyn",
// The child property table adopts thenables through the dyn-async unit.
"child.propsInstall",
"child.propsResolve",
"crypto.native",
// The crypto global's object extends crypto.native's.
"webcrypto.object",
// Web streams settle their promises with dyn values.
"webstream.class",
"webstream.instanceOf",
"webstream.install",
"dyn.promiseAll",
"dyn.promiseAllSettled",
"dyn.promiseAny",
"dyn.promiseResolve",
"fs.callbackCall",
"fs.callbackGlob",
"fs.callbackValue",
"process.deprecationFlag",
"process.emitWarning",
"process.emitWarningText",
"process.offRejectionHandled",
"process.offUncaughtException",
"process.offUnhandledRejection",
"process.offWarning",
"process.onRejectionHandled",
"process.onUncaughtException",
"process.onUnhandledRejection",
"process.onWarning",
"process.setDeprecationFlag",
"process.setEmitWarning",
"punycode.warn",
// DEP0005 for the deprecated Buffer constructor.
"buffer.deprecation",
// util.aborted answers a checked-dynamic promise, and so do the
// functions util.debounce and util.throttle return.
"util.aborted",
"util.debounce",
"util.throttle",
"timers.immediatePromise",
// The performance timeline (scr_perf.c, linked with this unit for its
// MaxPerformanceEntryBufferExceededWarning).
"perf.clearMarks",
"perf.clearMeasures",
"perf.entries",
"perf.entryDetail",
"perf.entryDuration",
"perf.entryKeep",
"perf.entryName",
"perf.entryRef",
"perf.entryStart",
"perf.entryType",
"perf.mark",
"perf.measure",
// Blob and File (scr_blob.c, linked with this unit, which boxes their
// reads as dyn promises).
"als.bindCallbackDyn",
"als.disable",
"als.enterWith",
"als.exitRun",
"als.get",
"als.name",
"als.new",
"als.newOptions",
"als.run",
"als.snapshot",
"blob.arrayBuffer",
"blob.bytes",
"blob.fileNew",
"blob.is",
"blob.new",
"blob.slice",
"blob.text",
"dc.chanBindStore",
"dc.chanRunStores",
"dc.chanUnbindStore",
"util.callbackify",
"util.promisifyDyn",
"util.debuglog",
"util.deprecate",
"util.diff",
"util.extend",
"util.inherits",
"util.inheritsPrototype",
"util.isArrayDeprecated",
"util.styleText",
]);
/** The libCalls that enter AsyncLocalStorage stores (RuntimeFeatures
* asyncContext). */
const ASYNC_CONTEXT_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"als.enterWith",
"als.exitRun",
"als.run",
"dc.chanBindStore",
"dc.chanRunStores",
// getTestContext() reads the test runner's entry in the same snapshot.
"test.context",
"test.hasContext",
]);
const LEGACY_URL_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"url.domainToASCII",
"url.domainToUnicode",
"url.legacyFormat",
"url.legacyParse",
"url.legacyResolve",
]);
const UTIL_CALLABLE_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"util.callbackify",
"util.debuglog",
"util.deprecate",
"util.diff",
"util.extend",
"util.inherits",
"util.inheritsPrototype",
"util.isArrayDeprecated",
]);
/** Entry points that link the regex unit: the regex and glob matchers (fs.glob's walk compiles its patterns with the glob matcher), and readline's terminal unit (scr_readline_tty.c), which measures with the regex unit's stripVTControlCharacters. */
const REGEX_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dyn.nativeRegexIs",
"fs.callbackGlob",
"fs.globSync",
"fs.globTypesSync",
"fsp.globError",
"fsp.globStrings",
"fsp.globTypes",
"path.matchesGlob",
"path.win32MatchesGlob",
"regexp.escape",
"rl.cursorPos",
"rl.keypressStdin",
"rl.keypressStream",
"stdin.onKeypress",
"util.stripVTControlCharacters",
]);
/** TextDecoder entry points over the legacy encodings. */
const LEGACY_TEXT_DECODER_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"text.decodeLegacy",
"text.decodeLegacyOptions",
"text.decodeStream",
]);
/** Signal-taking readline, stream and timers/promises entry points, which register abort listeners. */
const ABORT_SIGNAL_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"events.onceSignal",
"http.clientSignal",
"http.requestDyn",
"http.requestDynCb",
"https.requestDyn",
"https.requestDynCb",
"rl.createSignal",
"rl.questionAsyncSignal",
"sp.finishedOptsSignal",
"sp.pipelineOptsSignal",
"stream.addAbortSignal",
"stream.addAbortSignalFn",
"stream.finishedOptsSignal",
"tp.schedulerWaitSignal",
"tp.setImmediateSignal",
"tp.setTimeoutSignal",
]);
/** Entry points that link the stream unit (with STREAM_LIB_PREFIXES). */
const STREAM_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"child.stdioStream",
"stream.chunkDone",
"stream.objectMode",
"stream.decodeStrings",
"stream.optionValues",
"stream.checkEncodingOption",
"stream.duplexFromGen",
"stream.duplexFromDyn",
"stream.duplexFromPromise",
"stream.duplexFromAsyncFn",
"stream.duplexFromGenSignal",
"stream.duplexFromAsyncFnSignal",
"fetch.readableFromWeb",
"stream.duplexifyReadable",
"stream.duplexifyWritable",
"stream.duplexifyPair",
"console.bind",
"crypto.hashStream",
"crypto.hashStreamPiped",
"fileHandle.createReadStream",
"fileHandle.createWriteStream",
"fs.createReadStream",
"fs.createWriteStream",
"rl.inputFromDyn",
"rl.outputFromDyn",
"sp.finished",
"sp.pipeline",
"sp.pipelineStages",
"sp.pipelineStagesOpts",
"stream.addAbortSignal",
"stream.addAbortSignalFn",
"stream.destroy",
"stream.destroyErr",
"stream.dynNative",
"stream.dynNativePipe",
"stream.errored",
"stream.pipeDyn",
"stream.prop",
"stream.stdioWritable",
"stream.unboxDyn",
"stream.dynIterableSource",
"stream.dynSourceValue",
"stream.dynTransformResult",
"stream.unboxDynStage",
"stream.handleStream",
"stream.unpipeDyn",
"stream.workerStdio",
]);
/** libCall name prefixes that link the stream unit. */
const STREAM_LIB_PREFIXES: readonly string[] = [
"duplex.",
"fs.fsStream",
"passthrough.",
"readable.",
"sc.",
"sp.finishedOpts",
"sp.pipelineOpts",
"stream.finishedOpts",
"stream.pipelineStages",
"stream.composeStages",
"stream.set",
"transform.",
"writable.",
"zlib.stream",
];
/** The Readable operators and Readable.from over a compiled generator or a checked-dynamic iterator (scr_stream_ops.c), which await through the dyn-async unit (with READABLE_DYN_ASYNC_PREFIXES). */
const READABLE_DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"readable.fromAsyncGen",
"readable.fromAsyncIter",
"readable.fromDone",
"readable.fromDyn",
"readable.fromIter",
"stream.duplexFromGen",
"stream.duplexFromDyn",
"stream.duplexFromPromise",
"stream.duplexFromAsyncFn",
"stream.duplexFromGenSignal",
"stream.duplexFromAsyncFnSignal",
"stream.unboxDynStage",
]);
/** libCall name prefixes of the Readable entry points that await through the dyn-async unit. */
const READABLE_DYN_ASYNC_PREFIXES: readonly string[] = ["readable.fromGen", "readable.op"];
/** Entry points that link the dyn-invoke unit. */
const DYN_INVOKE_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dyn.arrayProtoCall",
"dyn.arrayPrototype",
"dyn.builtinFunction",
"dyn.builtinMethod",
"dyn.defineProperty",
"child.propsDefine",
"dyn.defineProps",
"dyn.definePrototypeProps",
"dyn.functionApply",
"dyn.globalConstructor",
"dyn.namespaceDefine",
"dyn.namespaceNew",
"dyn.objCreateWithProperties",
"dyn.objectTag",
"stream.dynNative",
"stream.dynNativePipe",
"stream.pipeDyn",
"stream.unpipeDyn",
]);
/** Member names whose constant-key dynamic reads answer the inherited Object.prototype member through the dyn-invoke unit. Mirrors expr-dynamic.ts INHERITED_OBJECT_MEMBERS (the IR layer does not import the backend). */
const INHERITED_MEMBER_NAMES: ReadonlySet<string> = new Set([
"constructor",
"hasOwnProperty",
"propertyIsEnumerable",
"toString",
"valueOf",
]);
/** Entry points that link the child-process unit (with CHILD_PROCESS_LIB_PREFIXES): the fork IPC channel on the process object. */
const CHILD_PROCESS_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"process.connected",
"process.disconnect",
"process.forkTarget",
"process.ipcOpen",
"process.onDisconnect",
"process.onMessage",
"process.send",
"process.sendCb",
]);
/** libCall name prefixes that link the child-process unit. */
const CHILD_PROCESS_LIB_PREFIXES: readonly string[] = ["child.", "cp.", "spawnRes.", "writer."];
/** The entry points scr_dyn_builtin_class.c defines. The unit calls
* listeners through the dyn-invoke unit, renders unhandled 'error'
* payloads through the inspect unit, creates EventEmitter's private
* symbols through the symbol unit and shares defaultMaxListeners with the
* emitter unit, so each of them links all five. */
const BUILTIN_CLASS_LIB_FNS: readonly IrLibFn[] = [
"dyn.builtinClass",
// the stream classes, through scr_dyn_builtin_stream.c
"dyn.builtinInstanceOf",
"dyn.builtinStreamClass",
"dyn.functionInstanceOf",
];
/** Entry points that link the symbol unit, besides the sym. and disposable. families. */
const SYMBOL_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
// Web streams answer async iteration through Symbol.asyncIterator.
"webstream.class",
"webstream.instanceOf",
"webstream.install",
// EventEmitter's kShapeMode and kCapture properties
...BUILTIN_CLASS_LIB_FNS,
"dyn.usingDispose",
"dyn.usingMethod",
"util.isDeepStrictEqual",
// The Buffer function value's Symbol(kIsEncodingSymbol) member
"buffer.constructor",
"buffer.staticValue",
"buffer.prototypeMember",
]);
/** Entry points that link the querystring unit. */
const QS_LIB_FNS: ReadonlySet<IrLibFn> = new Set(["qs.parse", "qs.stringify", "qs.unescape"]);
/** Entry points that link the parseArgs unit. */
const PARSE_ARGS_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"util.isDeepStrictEqual",
"util.parseArgs",
"util.styleText",
]);
/** String methods whose Unicode tables live in the regex unit's libunicode. */
const CASE_MAPPING_STR_METHODS: ReadonlySet<string> = new Set([
"normalize",
"toLowerCase",
"toUpperCase",
]);
/** Array methods that link the copying unit. */
const COPYING_ARR_METHODS: ReadonlySet<string> = new Set([
"toReversed",
"toSpliced",
"with",
"withUndefined",
]);
/** Typed-array methods that link the copying unit. */
const COPYING_BYTES_METHODS: ReadonlySet<string> = new Set([
"join",
"toArray",
"toReversed",
"with",
]);
/** Inspect types and expressions together in one traversal. This is a fresh
* snapshot: callers may mutate an IR module between compilation passes. */
/** The checked-dynamic undefined value (dynUndefinedExpr). */
export function isDynUndefined(e: IrExpr): boolean {
return e.kind === "dynFrom" && e.value.kind === "unitLit" && e.value.unit === "undefined";
}
/** With `nodes` (the module's node index), the scan reads the recorded
* expressions and types instead of walking the module. */
export function moduleRuntimeFeatures(mod: IrModule, nodes?: IrNodeIndex): RuntimeFeatures {
return scanRuntimeFeatures(mod, undefined, nodes);
}
/** The module's record shapes, unions and class layouts by id. */
interface TypeTables {
records: Map<string, IrRecordShape>;
unions: Map<string, IrUnionDef>;
classes: Map<string, IrClassDef>;
/** mentionsDgramSocket's carriers, computed on first use. */
dgramCarriers?: TypeCarriers;
/** mentionsTextCodec's and mentionsTextDecoder's carriers. */
textCodecCarriers?: TypeCarriers;
textDecoderCarriers?: TypeCarriers;
}
/** Whether boxing a value of type `t` as dyn boxes a promise, which goes
* through scr_async_dyn.c: a promise, a function whose result type holds
* one, or a record, array, tuple, union, Map or Set that holds either. */
function boxesPromise(t: IrType, tables: TypeTables, seen: Set<string>): boolean {
switch (t.kind) {
case "promise":
return true;
case "func":
// A boxed function converts its result to dyn, which boxes any promise
// the result type holds.
return boxesPromise(t.ret, tables, seen);
case "array":
case "set":
return boxesPromise(t.elem, tables, seen);
case "map":
return boxesPromise(t.key, tables, seen) || boxesPromise(t.value, tables, seen);
case "record": {
if (seen.has(t.shapeId)) return false;
seen.add(t.shapeId);
const shape = tables.records.get(t.shapeId);
return shape?.fields.some((f) => boxesPromise(f.type, tables, seen)) ?? false;
}
case "union": {
if (seen.has(t.unionId)) return false;
seen.add(t.unionId);
const def = tables.unions.get(t.unionId);
return def?.arms.some((a) => boxesPromise(a, tables, seen)) ?? false;
}
case "object": {
// A boxed class instance exposes its fields as dyn values (Object.keys,
// spread, inspection), boxing a promise any field holds. It
// materializes inherited fields too, and the instance can be any
// subclass.
const key = `class:${t.className}`;
if (seen.has(key)) return false;
seen.add(key);
const related = (def: IrClassDef): boolean => {
for (
let c: IrClassDef | undefined = def;
c;
c = c.base ? tables.classes.get(c.base) : undefined
)
if (c.name === t.className) return true;
return false;
};
for (const def of tables.classes.values()) {
if (!related(def)) continue;
for (
let c: IrClassDef | undefined = def;
c;
c = c.base ? tables.classes.get(c.base) : undefined
)
if (c.fields.some((f) => boxesPromise(f.type, tables, seen))) return true;
}
return false;
}
default:
return false;
}
}
/** The record shapes, unions and classes whose values carry a leaf type
* (directly or through other members), computed once per module and leaf. */
interface TypeCarriers {
records: Set<string>;
unions: Set<string>;
anyClass: boolean;
}
/** The leaf a carrier set tracks: a dgram socket, a codec record. */
type CarrierLeaf = (t: IrType, tables: TypeTables) => boolean;
/** Whether `t` carries a leaf, given the carrier sets so far: structural
* through functions (parameters and result), promises and collections;
* named records, unions and class instances by the sets. */
function carriesLeaf(
t: IrType,
carriers: TypeCarriers,
leaf: CarrierLeaf,
tables: TypeTables,
): boolean {
if (leaf(t, tables)) return true;
switch (t.kind) {
case "func":
return (
t.params.some((p) => carriesLeaf(p, carriers, leaf, tables)) ||
carriesLeaf(t.ret, carriers, leaf, tables)
);
case "promise":
return carriesLeaf(t.inner, carriers, leaf, tables);
case "array":
case "set":
return carriesLeaf(t.elem, carriers, leaf, tables);
case "map":
return (
carriesLeaf(t.key, carriers, leaf, tables) || carriesLeaf(t.value, carriers, leaf, tables)
);
case "record":
return carriers.records.has(t.shapeId);
case "union":
return carriers.unions.has(t.unionId);
case "object":
// Any class may be the instance's runtime class or one of its bases.
return carriers.anyClass;
default:
return false;
}
}
/** The module's carriers of `leaf`: a fixpoint over its records, unions and
* class fields. */
function leafCarriers(tables: TypeTables, leaf: CarrierLeaf): TypeCarriers {
const carriers: TypeCarriers = { records: new Set(), unions: new Set(), anyClass: false };
const carries = (t: IrType): boolean => carriesLeaf(t, carriers, leaf, tables);
for (let changed = true; changed;) {
changed = false;
for (const [id, shape] of tables.records)
if (
!carriers.records.has(id) &&
(leaf({ kind: "record", shapeId: id }, tables) || shape.fields.some((f) => carries(f.type)))
) {
carriers.records.add(id);
changed = true;
}
for (const [id, def] of tables.unions)
if (!carriers.unions.has(id) && def.arms.some(carries)) {
carriers.unions.add(id);
changed = true;
}
if (
!carriers.anyClass &&
[...tables.classes.values()].some((c) => c.fields.some((f) => carries(f.type)))
) {
carriers.anyClass = true;
changed = true;
}
}
return carriers;
}
const dgramLeaf: CarrierLeaf = (t) => t.kind === "dgramSocket";
/** Whether converting a value of type `t` to or from dyn converts a dgram
* socket, which goes through scr_dgram_dyn.c: the socket itself, or a
* function, collection, record, union, promise or class instance that
* carries one (boxed functions convert their parameters too). */
function mentionsDgramSocket(t: IrType, tables: TypeTables): boolean {
tables.dgramCarriers ??= leafCarriers(tables, dgramLeaf);
return carriesLeaf(t, tables.dgramCarriers, dgramLeaf, tables);
}
const textCodecLeaf: CarrierLeaf = (t, tables) => {
if (t.kind !== "record") return false;
const shape = tables.records.get(t.shapeId);
return shape !== undefined && recordHandleCodecClass(shape) !== null;
};
const textDecoderLeaf: CarrierLeaf = (t, tables) => {
if (t.kind !== "record") return false;
const shape = tables.records.get(t.shapeId);
return shape !== undefined && recordTextCodecClass(shape) === "TextDecoder";
};
/** Whether converting a value of type `t` to or from dyn converts a
* TextEncoder, TextDecoder or StringDecoder record, which boxes it as a handle of
* scr_text_codec_dyn.c (the same carrier walk as dgram sockets). */
function mentionsTextCodec(t: IrType, tables: TypeTables): boolean {
tables.textCodecCarriers ??= leafCarriers(tables, textCodecLeaf);
return carriesLeaf(t, tables.textCodecCarriers, textCodecLeaf, tables);
}
/** The TextDecoder half: its boxed decode runs scr_text_decode_stream,
* which reaches the legacy decoders. */
function mentionsTextDecoder(t: IrType, tables: TypeTables): boolean {
tables.textDecoderCarriers ??= leafCarriers(tables, textDecoderLeaf);
return carriesLeaf(t, tables.textDecoderCarriers, textDecoderLeaf, tables);
}
/** The libCall fns served by the OPTIONAL events unit (scr_events.c):
* process signal/exit listeners and the piped-stdin surface. */
const PROCESS_EVENT_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"process.offExit",
"process.offSignal",
"process.onExit",
"process.onSignal",
"process.stdio",
"stdin.nextChunk",
"stdin.onData",
"stdin.onEnd",
"stdin.onError",
// node:readline rides the stdin unit (scr_readline.c links beside
// scr_events.c under the same gate).
"rl.checkCompleter",
"rl.checkOptions",
"rl.close",
"rl.create",
"rl.createSignal",
"rl.cursor",
"rl.cursorPos",
"rl.getPrompt",
"rl.history",
"rl.isPromises",
"rl.isTerminal",
"rl.iterNext",
"rl.iterOpen",
"rl.iterReturn",
"rl.keypressStdin",
"rl.keypressStream",
"rl.line",
"rl.moves",
"rl.numProp",
"rl.on",
"rl.pause",
"rl.prompt",
"rl.promptWith",
"rl.question",
"rl.questionAsync",
"rl.questionAsyncDyn",
"rl.questionAsyncSignal",
"rl.questionAsyncSignalDyn",
"rl.questionDyn",
"rl.resume",
"rl.seq",
"rl.setHistory",
"rl.setOptions",
"rl.setPrompt",
"rl.setPromptDyn",
"rl.terminal",
"rl.uiClearLine",
"rl.uiClearScreenDown",
"rl.uiCommit",
"rl.uiCursorTo",
"rl.uiMoveCursor",
"rl.uiOpen",
"rl.uiOpenStream",
"rl.uiRollback",
"rl.write",
"rl.writeKey",
"stdin.onKeypress",
]);
/** The legacy http2.* libCalls implemented by scr_http.c/scr_tls.c (the
* allowHTTP1 compatibility slice) — they must NOT pull scr_http2.c, so
* divergence-57 binaries keep their exact link line. */
const HTTP2_LEGACY_FNS: ReadonlySet<IrLibFn> = new Set([
"http2.streamNoop",
"http2.streamUndefCall",
]);
/** Names that reach the extended process-stream surface (the
* SCR_STDIO_EXTENDED events unit): stdin.setEncoding and 'resize'. */
const STDIO_EXTENDED_NAMES: ReadonlySet<string> = new Set(["setEncoding", "resize"]);
/** The libCall nodes the rules below test. */
type LibCallNode = IrExpr & { kind: "libCall" };
/** A condition over several nodes of the module (a WebCrypto call and an
* algorithm name): the scan's one walk shows it every expression, and the
* feature is set when `result` answers true after the walk. */
interface RuleScan {
expr(node: IrExpr): void;
result(): boolean;
}
/** What makes a program need one optional runtime unit. The feature is set
* when any condition holds: `initial` from the module's declarations, a
* libCall named in `fns` or starting with one of `prefixes`, a libCall
* `libCall` accepts, another expression node `expr` accepts, a type slot of
* one of the `types` kinds, or a `scan` over the whole module. A libCall or
* type that needs several units appears in each unit's rule. */
interface RuntimeFeatureRule {
initial?: (mod: IrModule) => boolean;
scan?: () => RuleScan;
fns?: readonly IrLibFn[];
prefixes?: readonly string[];
libCall?: (node: LibCallNode) => boolean;
expr?: (node: IrExpr, tables: () => TypeTables) => boolean;
types?: readonly IrType["kind"][];
}
/** Gives every rule in the table one type. */
const rule = (r: RuntimeFeatureRule): RuntimeFeatureRule => r;
/* The node:url and WHATWG URL entry points: every URL value comes from
* one, and parsing one can reach the UTS #46 host processing. */
const URL_PREFIXES: readonly string[] = ["url."];
/* The HTTP, HTTPS, HTTP/2 and TLS surfaces, which link the net unit. */
const NET_PREFIXES: readonly string[] = ["net.", "http.", "tls.", "https.", "http2."];
/** The scr_dh.c entry points (the dh unit). */
export const DH_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"crypto.dhNew",
"crypto.dhGroupNew",
"crypto.checkPrimeSync",
"crypto.checkPrimeCb",
"crypto.generatePrimeSync",
"crypto.generatePrimeCb",
]);
/** The scr_pki.c entry points (the pki unit). */
export const PKI_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"crypto.createPrivateKey",
"crypto.createPublicKey",
"crypto.createSign",
"crypto.createVerify",
"crypto.generateKeyPairCb",
"crypto.generateKeyPairSync",
"crypto.signOneshot",
"crypto.verifyOneshot",
"crypto.x509PublicKey",
"crypto.x509Verify",
"crypto.x509CheckPrivateKey",
"crypto.diffieHellman",
]);
/** The WebCrypto algorithms scr_webcrypto_pki.c implements over the pki
* unit (lowercased). */
const WEBCRYPTO_ASYMMETRIC_NAMES: ReadonlySet<string> = new Set([
"ecdsa",
"ed25519",
"rsa-pss",
"rsassa-pkcs1-v1_5",
]);
/** Whether a string literal in the module names Ed25519, without regard
* to case ("ed25519" for node:crypto, "Ed25519" for WebCrypto): the
* Ed25519 unit links then, alongside the pki unit. */
function ed25519NameScan(): RuleScan {
let named = false;
return {
expr: (node) => {
if (!named && node.kind === "strLit" && node.value.toLowerCase() === "ed25519") named = true;
},
result: () => named,
};
}
/** moduleUsesWebCryptoAsymmetric's condition as a runtime-feature scan. */
function webCryptoAsymmetricScan(): RuleScan {
let webcrypto = false;
let named = false;
return {
expr: (node) => {
if (node.kind === "libCall" && node.fn.startsWith("webcrypto.")) webcrypto = true;
else if (
!named &&
node.kind === "strLit" &&
WEBCRYPTO_ASYMMETRIC_NAMES.has(node.value.toLowerCase())
)
named = true;
},
result: () => webcrypto && named,
};
}
/** Whether the module calls WebCrypto and a string literal in it names one
* of the asymmetric algorithms (matched without regard to case, as
* normalizeAlgorithm matches names): scr_webcrypto_pki.c and the pki unit
* link then. */
export function moduleUsesWebCryptoAsymmetric(mod: IrModule): boolean {
let webcrypto = false;
let named = false;
everyModuleNode(mod, {
expr: (node) => {
if (node.kind === "libCall" && node.fn.startsWith("webcrypto.")) webcrypto = true;
if (node.kind === "strLit" && WEBCRYPTO_ASYMMETRIC_NAMES.has(node.value.toLowerCase()))
named = true;
return !(webcrypto && named);
},
stmt: () => !(webcrypto && named),
type: () => true,
});
return webcrypto && named;
}
/** One rule per runtime feature, sorted by name. A new feature is an
* entry here and its line in noRuntimeFeatures; RuntimeFeatures, the scan
* and its early exit derive from this table. The lists above keep one
* entry per line, sorted within each commented group, so changes that add
* different entries touch different lines. */
const RUNTIME_FEATURE_RULES = {
assert: rule({ prefixes: ["assert."] }),
/** The program enters AsyncLocalStorage stores (store.run, enterWith,
* a diagnostics_channel store binding) or reads the test runner's
* context (getTestContext): its timer, immediate, tick and
* queueMicrotask scheduling goes through the runtime's *_als entry
* points, which carry the active stores into each callback. */
asyncContext: rule({ fns: [...ASYNC_CONTEXT_LIB_FNS] }),
/** scr_wasm.c converts i64 values through BigInt. */
bigint: rule({
fns: ["process.hrtimeValue"],
prefixes: ["bigint.", "wasm."],
types: ["bigint"],
}),
childProcess: rule({
fns: [...CHILD_PROCESS_LIB_FNS],
prefixes: CHILD_PROCESS_LIB_PREFIXES,
types: ["child", "childStream", "childWriter", "spawnRes"],
}),
/** ChildProcess listener removal (off/removeListener) and the recorded
* terminal signal (signalCode): scr_child_lifecycle.c, a unit of its
* own so a program that never calls them keeps scr_child.c's layout. */
childLifecycle: rule({
fns: [
"child.offSpawn",
"child.offExit",
"child.offClose",
"child.offError",
"child.signalCode",
],
}),
/** scr_cipher.c over the mbedTLS archive; DEP0182 reports through the
* process-warning unit, and unknown output encodings render through the
* inspect unit. WebCrypto's encrypt, decrypt, wrapKey and unwrapKey
* (scr_webcrypto_aes.c) link the same archive. */
cipher: rule({
fns: [
"crypto.cipherNew",
"webcrypto.encrypt",
"webcrypto.decrypt",
"webcrypto.wrapKey",
"webcrypto.unwrapKey",
],
}),
copying: rule({
expr: (node, _tables) =>
(node.kind === "arrIntrinsic" && COPYING_ARR_METHODS.has(node.method)) ||
(node.kind === "bytesIntrinsic" && COPYING_BYTES_METHODS.has(node.method)),
}),
dc: rule({ prefixes: ["dc."] }),
/** dns.lookup lives in the dgram unit. */
dgram: rule({ prefixes: ["dgram.", "dns."], types: ["dgramSocket"] }),
/** A dgram socket converts to or from an untyped value (its handle box,
* a boxed function or record that carries one): scr_dgram_dyn.c over
* the dgram unit. */
dgramDyn: rule({
expr: (node, tables) =>
(node.kind === "dynFrom" && mentionsDgramSocket(node.value.type, tables())) ||
(node.kind === "dynCheck" && mentionsDgramSocket(node.type, tables())),
}),
/** A TextEncoder, TextDecoder or StringDecoder record converts to or from an untyped
* value (its handle box, or a boxed function, record or collection that
* carries one): scr_text_codec_dyn.c. */
textCodecDyn: rule({
fns: ["text.codecIs"],
expr: (node, tables) =>
(node.kind === "dynFrom" && mentionsTextCodec(node.value.type, tables())) ||
(node.kind === "dynCheck" && mentionsTextCodec(node.type, tables())),
}),
/** node:dns/promises: scr_dns_promises.c over the dgram unit. */
dnsPromises: rule({ fns: ["dns.promisesLookup", "dns.promisesLookupService"] }),
dynAsync: rule({
initial: (mod) =>
mod.workers === true ||
mod.functions.some(
(fn) => fn.async === true && fn.generator === undefined && fn.returnType.kind === "dyn",
),
// The legacy url functions, crypto.createCipheriv,
// ecdh.setPublicKey and generateKeyPair's rsa-pss options emit their
// deprecation warnings through the dyn-async unit (WebCrypto's
// encrypt and decrypt link scr_cipher.c too); scr_wasm.c boxes its
// handles and promises through it.
fns: [
...DYN_ASYNC_LIB_FNS,
...READABLE_DYN_ASYNC_LIB_FNS,
...LEGACY_URL_LIB_FNS,
"crypto.cipherNew",
"webcrypto.encrypt",
"webcrypto.decrypt",
"webcrypto.wrapKey",
"webcrypto.unwrapKey",
"crypto.ecdhNew",
"crypto.generateKeyPairCb",
"crypto.generateKeyPairSync",
],
prefixes: [...READABLE_DYN_ASYNC_PREFIXES, "wasm."],
expr: (node, tables) =>
(node.kind === "awaitExpr" && node.type.kind === "dyn") ||
// Rejection reasons cross as dyn values (allSettled's records, any's
// AggregateError list).
(node.kind === "intrinsic" &&
(node.name === "promise.allSettled" || node.name === "promise.any")) ||
(node.kind === "dynFrom" && boxesPromise(node.value.type, tables(), new Set())),
}),
/** EventEmitter and the stream classes as untyped constructor values
* (scr_dyn_builtin_class.c), which call listeners through the dyn-invoke
* unit, render unhandled 'error' payloads through the inspect unit and
* create EventEmitter's private symbols through the symbol unit. */
dynBuiltinClass: rule({
fns: [...BUILTIN_CLASS_LIB_FNS],
}),
/** The stream classes as untyped constructor values with their stream
* half (scr_dyn_builtin_stream.c): native stream state for constructor
* functions, over the stream unit. */
dynBuiltinStream: rule({ fns: ["dyn.builtinStreamClass"] }),
dynInvoke: rule({
initial: (mod) => mod.workers === true,
fns: [
...DYN_INVOKE_LIB_FNS,
...UTIL_CALLABLE_LIB_FNS,
...BUILTIN_CLASS_LIB_FNS,
"util.promisifyDyn",
"buffer.prototypeMember",
// scr_webstreams.c calls the iterators ReadableStream.from reads.
"webstream.class",
"webstream.instanceOf",
"webstream.install",
],
// Constant-key reads of Object.prototype member names answer the
// inherited member through scr_dyn_invoke.c.
expr: (node, _tables) =>
node.kind === "dynInvoke" ||
(node.kind === "dynKeyGet" &&
node.key.kind === "strLit" &&
INHERITED_MEMBER_NAMES.has(node.key.value)),
}),
/** The program has dynamic-tier jsval operations (js.* builtins, the
* jsval pass): links the scr_js_* units (two-tier design I3). */
dynTier: rule({ prefixes: ["js."] }),
/** Checked-dynamic stream slots test values and pipe into JavaScript
* stream objects (scr_stream_dyn.c). */
streamDyn: rule({
fns: ["stream.dynNative", "stream.dynNativePipe", "stream.pipeDyn", "stream.unpipeDyn"],
}),
/** Own properties JavaScript adds to children (scr_dyn_handle_props.c). */
handleProps: rule({ fns: ["child.propsInstall"] }),
/** The declaration-typed boundary's adoption (scr_decl_adopt.c): its
* casts, and closures boxed in a boundary program, which adopt their
* arguments. */
declAdopt: rule({
fns: ["dyn.declAdopt", "dyn.adoptShared"],
initial: (mod) => mod.declBoundary === true,
}),
// Checked conversions into package-declared types name the declaration
// in their TypeError (scr_type_origin.c).
typeOrigins: rule({ initial: (mod) => (mod.declOrigins?.length ?? 0) > 0 }),
/** for await over checked-dynamic values (scr_dyn_for_await.c). */
dynForAwait: rule({ fns: ["dyn.asyncIteratorOpen", "dyn.asyncIteratorWasSync"] }),
/** Buffer.prototype methods called on checked-dynamic values
* (scr_bytes_dyn.c). */
dynBuffer: rule({
// Buffer.prototype members read as values dispatch to the same entries.
fns: ["buffer.prototypeMember"],
expr: (node, _tables) => node.kind === "dynInvoke" && DYN_BUFFER_METHODS.has(node.method),
}),
/** createRequire's require and require.resolve with specifiers computed
* at run time (scr_require.c). */
computedRequire: rule({ fns: ["module.requireComputed", "module.resolveComputed"] }),
/** Builtin functions read as values (scr_builtin_values.c). */
builtinValues: rule({
fns: [
"buffer.byteLenValue",
"buffer.allocValue",
"fs.realpathValue",
"buffer.constructor",
"buffer.installValue",
"buffer.staticValue",
"buffer.prototypeMember",
],
}),
/** The stream unit emits through the emitter unit, also when the
* program constructs no stream (a pipeline of functions). */
/** scr_ecdh.c over the mbedTLS archive; DEP0031 reports through the
* process-warning unit, and unknown encodings and formats render through
* the inspect unit. */
ecdh: rule({ fns: ["crypto.ecdhNew", "crypto.ecdhConvertKey"] }),
/** scr_dh.c over the mbedTLS archive, also for checkPrime and
* generatePrime; unknown encodings and argument values render through
* the inspect unit. */
dh: rule({ fns: [...DH_LIB_FNS] }),
emitter: rule({
initial: (mod) =>
mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS),
fns: [...STREAM_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS],
prefixes: ["emitter.", ...STREAM_LIB_PREFIXES],
}),
/** Subclasses of the runtime EventTarget/Event bases tear down and box
* through the EventTarget unit even when no event.* libCall survives. */
eventTarget: rule({
initial: (mod) =>
(mod.classes ?? []).some(
(c) => c.name === RUNTIME_EVENT_TARGET_CLASS || c.name === RUNTIME_EVENT_CLASS,
),
// util.debounce and util.throttle register options.signal's listener.
fns: [
"events.addAbortListener",
"util.aborted",
"util.debounce",
"util.throttle",
...ABORT_SIGNAL_LIB_FNS,
],
prefixes: ["event."],
}),
/** util.MIMEType and util.MIMEParams (scr_mime.c). */
mime: rule({ prefixes: ["mime."] }),
/** Web streams in checked-dynamic code (scr_webstreams.c). */
webStreams: rule({ prefixes: ["webstream."] }),
/** AbortController and AbortSignal (scr_abort.c, or the fetch unit).
* A WritableStream controller's signal is one (scr_webstreams.c). */
abortSignal: rule({ prefixes: ["abort.", "webstream."] }),
/** util.debounce and util.throttle (scr_util_timing.c). */
utilTiming: rule({ fns: ["util.debounce", "util.throttle"] }),
/** events.once(emitter, name, { signal }) (scr_events_once_signal.c). */
eventsOnceSignal: rule({ fns: ["events.onceSignal"] }),
fetch: rule({ prefixes: ["fetch."] }),
fileHandle: rule({ types: ["fileHandle"] }),
fsWatch: rule({ prefixes: ["fs.watch", "watcher."], types: ["fsWatcher"] }),
/** A socket 'lookup' listener: the C main installs the net unit's
* lookup records (scr_net_lookup_install). */
netLookup: rule({ fns: ["net.sockOnLookup"] }),
http: rule({
prefixes: ["http.", "tls.", "https.", "http2."],
types: ["httpReq", "httpRes", "httpClientReq", "secureCtx"],
}),
/** The real h2 surface (scr_http2.c); the legacy http2.* libCalls of the
* allowHTTP1 slice are served by scr_http.c and scr_tls.c. */
http2: rule({
libCall: (node) => node.fn.startsWith("http2.") && !HTTP2_LEGACY_FNS.has(node.fn),
types: ["http2Session", "http2Stream"],
}),
/** scr_test.c renders thrown strings with the inspect unit's string
* formatting; scr_util_callable.c wraps the inspect unit; readline's
* terminal unit prints completion errors through it when there is a
* completer. */
inspect: rule({
fns: [
"global.native",
// ERR_ARG_NOT_ITERABLE renders the value as util.format's %s does.
"webstream.class",
"webstream.install",
"util.styleText",
"crypto.cipherNew",
"webcrypto.encrypt",
"webcrypto.decrypt",
"webcrypto.wrapKey",
"webcrypto.unwrapKey",
"crypto.ecdhNew",
"crypto.ecdhConvertKey",
...DH_LIB_FNS,
...BUILTIN_CLASS_LIB_FNS,
...UTIL_CALLABLE_LIB_FNS,
],
prefixes: ["insp.", "console.", "test."],
libCall: (node) => node.fn === "rl.terminal" && !isDynUndefined(node.args[3]!),
}),
legacyTextDecoder: rule({
fns: [...LEGACY_TEXT_DECODER_LIB_FNS],
expr: (node, tables) =>
(node.kind === "dynFrom" && mentionsTextDecoder(node.value.type, tables())) ||
(node.kind === "dynCheck" && mentionsTextDecoder(node.type, tables())),
}),
/** The program calls mock.timers members: its timer calls go through
* scr_test.c's mock-aware entry points. */
mockTimers: rule({ prefixes: ["test.mockTimers", "test.ctxMockTimers"] }),
net: rule({
prefixes: NET_PREFIXES,
types: [
"netServer",
"netSocket",
"http2Session",
"http2Stream",
"httpReq",
"httpRes",
"httpClientReq",
"secureCtx",
],
}),
/** Calls into Node.js (@scriptc/node): scr_node_island.c and the
* SCR_NODE_ISLANDS build of the event loop unit. */
nodeIsland: rule({ prefixes: ["nodeIsland."] }),
nodeTest: rule({ prefixes: ["test."], types: ["testCtx"] }),
/** scr_util_callable.c reads options through the parseArgs unit's
* descriptors. */
parseArgs: rule({ fns: [...PARSE_ARGS_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS] }),
/** perf_hooks event-loop metrics (eventLoopUtilization,
* monitorEventLoopDelay): scr_perf_loop.c. */
perfLoop: rule({ fns: ["perf.idleTime"], prefixes: ["perf.loop", "perf.eld"] }),
/** scr_pki.c over the mbedTLS archive: asymmetric KeyObjects and
* signatures. */
pki: rule({ scan: webCryptoAsymmetricScan, fns: [...PKI_LIB_FNS] }),
/** scr_ed25519.c (the vendored libsodium Ed25519), linked with pki: a
* string literal in the module names Ed25519. */
ed25519: rule({ scan: ed25519NameScan }),
processEvents: rule({ fns: [...PROCESS_EVENT_LIB_FNS, "util.styleText"], types: ["procStream"] }),
/** scr_url_legacy.c parses and stringifies queries. */
qs: rule({ fns: [...QS_LIB_FNS, ...LEGACY_URL_LIB_FNS] }),
/** scr_url_legacy.c lowercases and normalizes hostnames with the regex
* unit's libunicode, the WHATWG URL host parser runs UTS #46 through
* scr_idna.c (which links with the regex unit), NODE_DEBUG matching in
* scr_util_callable.c and readline's terminal unit use the regex unit,
* and locale case mapping runs its default conversion. */
regex: rule({
fns: [
...REGEX_LIB_FNS,
...LEGACY_URL_LIB_FNS,
...UTIL_CALLABLE_LIB_FNS,
"rl.terminal",
"str.localeCase",
],
prefixes: URL_PREFIXES,
expr: (node, _tables) =>
node.kind === "regexLit" ||
node.kind === "regexIntrinsic" ||
(node.kind === "strIntrinsic" && CASE_MAPPING_STR_METHODS.has(node.method)),
types: ["regex", "url"],
}),
searchParams: rule({ fns: ["url.searchParams"], prefixes: ["sp."], types: ["searchParams"] }),
/** URL programs install the IDNA unit's ToASCII for the WHATWG host
* parser (scr_url_idna_install). */
urlIdna: rule({ prefixes: URL_PREFIXES, types: ["url"] }),
/** stdin setEncoding / string data listeners / stdio 'resize' events. A
* typed string 'data' listener takes setEncoding-decoded chunks; the
* process stream object's setEncoding and 'resize' members are reached
* by name (dynamic member calls and event-name strings). */
stdioExtended: rule({
libCall: (node) =>
node.fn === "stdin.onData" &&
node.args[0]?.type.kind === "func" &&
node.args[0].type.params[0]?.kind === "string",
expr: (node, _tables) =>
(node.kind === "dynInvoke" && STDIO_EXTENDED_NAMES.has(node.method)) ||
(node.kind === "strLit" && STDIO_EXTENDED_NAMES.has(node.value)),
}),
stream: rule({
initial: (mod) => (mod.classes ?? []).some((c) => RUNTIME_STREAM_CLASSES.has(c.name)),
fns: [...STREAM_LIB_FNS, "dyn.builtinStreamClass"],
prefixes: STREAM_LIB_PREFIXES,
}),
/** A Dirent row converted to dyn carries Node's Symbol(type) key
* (scr_dyn_dirent_finish, defined in scr_symbol.c). */
symbol: rule({
initial: (mod) => (mod.records ?? []).some(isDirentShape),
fns: [...SYMBOL_LIB_FNS],
prefixes: ["sym.", "disposable."],
types: ["symbol"],
}),
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"] }),
/** Module namespace objects as checked-dynamic values
* (scr_module_namespace.c). */
moduleNamespace: rule({ fns: ["module.namespaceValue"] }),
/** A module namespace boxed for typed code (scr_module_namespace_dyn.c). */
moduleNamespaceDyn: rule({ fns: ["module.namespaceDyn"] }),
/** The WebAssembly API (scr_wasm.c). */
wasm: rule({ prefixes: ["wasm."] }),
/** crypto.subtle importKey/sign/verify/digest and CryptoKey:
* scr_webcrypto.c. */
webcrypto: rule({ prefixes: ["webcrypto."] }),
/** scr_x509.c: the X509Certificate fields beyond the validity window and
* fingerprint, which stay in scr_lib.c. */
x509: rule({
fns: [
"crypto.x509Ca",
"crypto.x509Fingerprint256",
"crypto.x509Fingerprint512",
"crypto.x509InfoAccess",
"crypto.x509Issuer",
"crypto.x509Raw",
"crypto.x509RawDer",
"crypto.x509RawStr",
"crypto.x509SerialNumber",
"crypto.x509SignatureAlgorithm",
"crypto.x509SignatureAlgorithmOid",
"crypto.x509Subject",
"crypto.x509SubjectAltName",
"crypto.x509CheckHost",
"crypto.x509CheckEmail",
"crypto.x509CheckIP",
"crypto.x509CheckIssued",
"crypto.x509LegacyObject",
"crypto.x509Pem",
// getPeerCertificate and getCertificate build with scr_x509.c too.
"tls.sockGetPeerCertificate",
"tls.sockGetCertificate",
],
}),
zlib: rule({ prefixes: ["zlib."] }),
/** node:zlib's Brotli forms: zlib.brotliInstall, which %main calls when
* the program uses Brotli (scr_brotli.c). */
brotli: rule({ fns: ["zlib.brotliInstall"] }),
};
type RuntimeFeatureName = keyof typeof RUNTIME_FEATURE_RULES;
export type RuntimeFeatures = Record<RuntimeFeatureName, boolean>;
const RUNTIME_FEATURE_NAMES: RuntimeFeatureName[] = [];
for (const name in RUNTIME_FEATURE_RULES) RUNTIME_FEATURE_NAMES.push(name as RuntimeFeatureName);
/** Every feature off, in the table's order. TypeScript rejects this
* literal when it misses a feature or names one without a rule. */
function noRuntimeFeatures(): RuntimeFeatures {
return {
assert: false,
asyncContext: false,
bigint: false,
childProcess: false,
cipher: false,
copying: false,
dc: false,
dgram: false,
dgramDyn: false,
textCodecDyn: false,
dnsPromises: false,
dynAsync: false,
dynBuiltinClass: false,
dynBuiltinStream: false,
dynInvoke: false,
dynTier: false,
ecdh: false,
x509: false,
dh: false,
emitter: false,
eventTarget: false,
eventsOnceSignal: false,
fetch: false,
fileHandle: false,
fsWatch: false,
http2: false,
http: false,
inspect: false,
legacyTextDecoder: false,
mime: false,
webStreams: false,
abortSignal: false,
utilTiming: false,
mockTimers: false,
net: false,
netLookup: false,
nodeIsland: false,
nodeTest: false,
parseArgs: false,
perfLoop: false,
pki: false,
ed25519: false,
processEvents: false,
qs: false,
regex: false,
searchParams: false,
urlIdna: false,
stdioExtended: false,
moduleNamespace: false,
moduleNamespaceDyn: false,
stream: false,
streamDyn: false,
handleProps: false,
childLifecycle: false,
declAdopt: false,
typeOrigins: false,
dynForAwait: false,
dynBuffer: false,
builtinValues: false,
computedRequire: false,
symbol: false,
tls: false,
tlsCa: false,
utilPromisify: false,
warningReplace: false,
wasm: false,
webcrypto: false,
zlib: false,
brotli: false,
};
}
const ruleOf = (name: RuntimeFeatureName): RuntimeFeatureRule => RUNTIME_FEATURE_RULES[name];
/** The rules indexed for the scan: features by libCall name and by type
* kind, and the prefix and predicate conditions in rule order. */
const FEATURES_BY_FN = new Map<string, RuntimeFeatureName[]>();
const FEATURES_BY_TYPE = new Map<string, RuntimeFeatureName[]>();
const FEATURE_PREFIXES: { prefix: string; name: RuntimeFeatureName }[] = [];
const FEATURE_LIBCALL_TESTS: RuntimeFeatureName[] = [];
const FEATURE_EXPR_TESTS: RuntimeFeatureName[] = [];
for (const name of RUNTIME_FEATURE_NAMES) {
const rule = ruleOf(name);
for (const fn of rule.fns ?? []) {
const names = FEATURES_BY_FN.get(fn);
if (names) names.push(name);
else FEATURES_BY_FN.set(fn, [name]);
}
for (const kind of rule.types ?? []) {
const names = FEATURES_BY_TYPE.get(kind);
if (names) names.push(name);
else FEATURES_BY_TYPE.set(kind, [name]);
}
for (const prefix of rule.prefixes ?? []) FEATURE_PREFIXES.push({ prefix, name });
if (rule.libCall) FEATURE_LIBCALL_TESTS.push(name);
if (rule.expr) FEATURE_EXPR_TESTS.push(name);
}
function scanRuntimeFeatures(
mod: IrModule,
stopAt?: RuntimeFeatureName,
nodes?: IrNodeIndex,
): RuntimeFeatures {
const features = noRuntimeFeatures();
const scans: { name: RuntimeFeatureName; scan: RuleScan }[] = [];
for (const name of RUNTIME_FEATURE_NAMES) {
const { initial, scan } = ruleOf(name);
if (initial && initial(mod)) features[name] = true;
if (scan) scans.push({ name, scan: scan() });
}
const keepGoing = (): boolean => stopAt === undefined || !features[stopAt];
let tables: TypeTables | undefined;
const typeTables = (): TypeTables =>
(tables ??= {
records: new Map((mod.records ?? []).map((r) => [r.id, r])),
unions: new Map((mod.unions ?? []).map((u) => [u.id, u])),
classes: new Map((mod.classes ?? []).map((c) => [c.name, c])),
});
const expr = (node: IrExpr): boolean => {
for (let i = 0; i < scans.length; i++) scans[i]!.scan.expr(node);
if (node.kind === "libCall") {
const fn = node.fn;
for (const name of FEATURES_BY_FN.get(fn) ?? []) features[name] = true;
for (const { prefix, name } of FEATURE_PREFIXES)
if (!features[name] && fn.startsWith(prefix)) features[name] = true;
for (const name of FEATURE_LIBCALL_TESTS)
if (!features[name] && ruleOf(name).libCall!(node)) features[name] = true;
} else {
for (const name of FEATURE_EXPR_TESTS)
if (!features[name] && ruleOf(name).expr!(node, typeTables)) features[name] = true;
}
return keepGoing();
};
const type = (node: IrType): boolean => {
for (const name of FEATURES_BY_TYPE.get(node.kind) ?? []) features[name] = true;
return keepGoing();
};
// Each condition is a fact about one expression or one type (the rule
// scans combine such facts in no particular order), so the index's
// expressions and distinct types answer as the walk does. An early stop
// (stopAt) keeps the walk.
if (nodes !== undefined && stopAt === undefined) {
const expressions = nodes.expressions();
for (const fn of mod.functions)
eachNodeOfKinds(fn, expressions, (node) => {
expr(node as IrExpr);
});
for (const t of nodes.types) type(t);
} else if (keepGoing()) everyModuleNode(mod, { expr, stmt: keepGoing, type });
for (const { name, scan } of scans) if (scan.result()) features[name] = true;
// Inline-cache sites of dynamic-tier functions use the tier's caches
// (scr_js_object.c, with the SCR_JS_TIER build of scr_json.c).
if (!features.dynTier && mod.functions.some((fn) => fn.tier === "dynamic")) {
const sites = new JsIcSites(mod);
features.dynTier = mod.functions.some((fn) => fn.tier === "dynamic" && hasJsIcSite(fn, sites));
}
return features;
}
/** True when the module contains any regex construct — a regexLit /
* regexIntrinsic node or a regex-typed slot anywhere. This is the link
* switch that pulls scr_regex.c + the vendored libregexp into the binary
* (native-toolchain.ts); regex-free programs keep the historical command line. The
* typed traversal visits type slots as well as executable nodes. */
/** True when the module enters AsyncLocalStorage stores (RuntimeFeatures
* asyncContext): a walk for those libCalls alone, without the other
* features' rules. */
export function moduleUsesAsyncContext(mod: IrModule): boolean {
const visitor = {
expr: (e: IrExpr): boolean => e.kind !== "libCall" || !ASYNC_CONTEXT_LIB_FNS.has(e.fn),
stmt: (): boolean => true,
};
return !mod.functions.every((fn) => everyStmtList(fn.body, visitor));
}
export function moduleUsesRegex(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "regex").regex;
}
/** True when the module contains an intrinsic whose implementation lives
* in scr_copying.c. This is the link switch that keeps the optional
* Array-copying and typed-array bridge TU out of unrelated binaries. */
export function moduleUsesCopying(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "copying").copying;
}
/** True when a non-UTF-8 TextDecoder call survives lowering. This gates the
* generated legacy mapping tables inside scr_bytes.c; the default UTF-8
* decoder and unrelated Buffer users keep their prior runtime object. */
export function moduleUsesLegacyTextDecoder(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "legacyTextDecoder").legacyTextDecoder;
}
/** True when a FileHandle type survives in the module. This is the link
* switch for scr_file_handle.c; fs/promises.open carries the type inside its
* promise result even when no handle method is called. */
export function moduleUsesFileHandle(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "fileHandle").fileHandle;
}
/** True when native fetch operations survive lowering. This links
* scr_fetch.c and its socket/tls/zlib dependencies and makes the emitted
* main call scr_fetch_install. Fetch-free graphs keep their link lines. */
export function moduleUsesFetch(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "fetch").fetch;
}
/** True when the module uses the process-events surface — the link switch
* that pulls scr_events.c into the binary and has the emitted main call
* scr_events_install (native-toolchain.ts + emitter; the scr_regex/scr_fetch/scr_zlib
* gating precedent). Event-free programs pay zero bytes and keep their
* exact link line. Same generic-walk shape as moduleUsesRegex. */
export function moduleUsesProcessEvents(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "processEvents").processEvents;
}
/** True when the module uses the node:events EventEmitter surface — the
* link switch that pulls scr_events_emitter.c into the binary (native-toolchain.ts; the
* scr_events.c gating precedent, but pure data structure: no install, no
* loop hooks). Two signals: the `%EventEmitter` class def rides the
* module (any emitter-typed value or `extends EventEmitter` subclass
* references it, and the emitted RC/trace helpers call scr_emitter_*),
* or an emitter.* libCall survived (the defaultMaxListeners statics carry
* no emitter-typed value). Emitter-free programs pay zero bytes and keep
* their exact link line. */
export function moduleUsesEmitter(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "emitter").emitter;
}
/** True when the program touches the node:stream surface (scr_stream.c —
* the moduleUsesEmitter story: the class defs ride the module whenever a
* stream-typed value exists, and every stream libCall names its unit).
* Stream programs always use the emitter unit too — the stream class
* defs pull `%EventEmitter` through their base chain, so
* moduleUsesEmitter answers true whenever this does. Stream-free
* programs pay zero bytes and keep their exact link line. */
export function moduleUsesStream(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "stream").stream;
}
/** True when the program links scr_dyn_builtin_stream.c (stream classes
* as untyped constructor values): its runtime streams box through that
* unit's view and checked casts to the stream classes accept constructor-
* function streams. Programs without it keep their exact code. */
export function moduleUsesDynBuiltinStream(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "dynBuiltinStream").dynBuiltinStream;
}
export const NPM_COMPRESS_MIN = 1024;
export function moduleUsesZlib(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "zlib").zlib;
}
/** True when the module contains any dc.* libCall — the link switch that
* pulls scr_dc.c (the diagnostics_channel registry and pub/sub) into the
* binary (native-toolchain.ts). Channel-free binaries keep their exact size class.
* Same walk shape as moduleUsesZlib. */
export function moduleUsesDc(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "dc").dc;
}
/** True when the module contains any assert libCall — the link switch
* that pulls scr_assert.c into the binary (native-toolchain.ts). scr_regex.c calls the
* assert throw/inspect helpers (assert.match lives there), so the regex
* switch also pulls scr_assert.c; assert-free, regex-free binaries keep
* the historical command line and size. Same walk shape as
* moduleUsesZlib. */
export function moduleUsesAssert(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "assert").assert;
}
/** True when the module contains any dynInvoke node or dyn.defineProps
* libCall — the link switch that pulls scr_dyn_invoke.c (the prototype-
* method dispatch on dyn receivers, plus scr_dyn_display and
* scr_dyn_define_props) into the binary (native-toolchain.ts; the assert gating
* precedent — dispatch-free binaries keep their exact size class). Same
* walk shape as moduleUsesZlib. */
export function moduleUsesDynInvoke(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "dynInvoke").dynInvoke;
}
/** True when the module contains any insp libCall — the link switch that
* pulls scr_inspect.c into the binary (native-toolchain.ts; the assert gating
* precedent — inspect-free binaries keep the historical command line and
* size class). Same walk shape as moduleUsesZlib. */
/** True when the module needs scr_async_dyn.c — the checked-dynamic
* async surfaces (dyn-promise then/catch/finally reactions, await of a
* dyn value, `new Promise(setImmediate)`, AsyncLocalStorage, the
* unhandledRejection/warning process events). Also pulled by the
* dynInvoke and dc gates (their TUs call into this one) — native-toolchain.ts. Same
* walk shape as moduleUsesZlib. */
export function moduleUsesDynAsync(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "dynAsync").dynAsync;
}
export function moduleUsesInspect(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "inspect").inspect;
}
/** True when the module reaches child_process or carries one of its runtime
* handle/result types. Besides the existing link consequence, this gates the
* checked-dynamic ChildProcess handle table installed from scr_child.c. */
export function moduleUsesChildProcess(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "childProcess").childProcess;
}
/** True when the module contains any net libCall — the link switch that
* pulls scr_net.c into the binary and has the emitted main call
* scr_net_install (native-toolchain.ts + emitter; the scr_events gating precedent).
* Net-free programs pay zero bytes and keep their exact link line. Same
* generic-walk shape as moduleUsesZlib. */
export function moduleUsesNet(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "net").net;
}
/** True when the module contains any sym.* libCall or a symbol-kind type
* anywhere in the IR — the link switch that pulls scr_symbol.c into the
* binary (native-toolchain.ts; the scr_net gating precedent — no install call, the
* Symbol.for registry initializes lazily). The TYPE check matters like
* net's: a symbol-typed local whose initializer compiled to a runtime
* fence still emits release calls that need the unit linked. Symbol-free
* programs pay zero bytes and keep their exact link line. */
export function moduleUsesSymbol(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "symbol").symbol;
}
/** True when bigint runtime operations or bigint-typed storage appears in
* the IR. The type check keeps retain/release references link-safe even
* when a producing statement was replaced by a runtime fence. */
export function moduleUsesBigInt(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "bigint").bigint;
}
/** True when the module uses the URLSearchParams surface — sp.* libCalls,
* the url.searchParams getter, or a searchParams-kind type anywhere on
* the IR (a fenced statement can leave a typed local whose release call
* still needs the unit linked) — the link switch that pulls
* scr_url_params.c into the binary (the moduleUsesSymbol precedent: pure
* data structure, no loop hooks, cross-compiles everywhere). sp-free
* programs keep their exact link line; scr_url.c itself stays
* always-linked and never references the unit. */
export function moduleUsesSearchParams(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "searchParams").searchParams;
}
/** True when the module uses the node:querystring surface — the qs.*
* libCalls that live in scr_qs.c (parse/stringify/unescape; qs.escape
* emits the always-linked component encoder and deliberately does NOT
* flip this switch) — the link switch that pulls scr_qs.c into the
* binary (the moduleUsesSearchParams precedent: pure data transforms, no
* loop hooks, cross-compiles everywhere). qs-free programs keep their
* exact link line. Same generic-walk shape as moduleUsesZlib. */
export function moduleUsesQs(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "qs").qs;
}
export function moduleUsesParseArgs(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "parseArgs").parseArgs;
}
/** True when the module contains any fs.watch/watcher.* libCall — the
* link switch that pulls scr_watch.c into the binary and has the emitted
* main call scr_watch_install (native-toolchain.ts + emitter; the scr_net gating
* precedent). Watch-free programs pay zero bytes and keep their exact
* link line. Same generic-walk shape as moduleUsesZlib. */
export function moduleUsesFsWatch(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "fsWatch").fsWatch;
}
/** True when the module contains any test.* libCall or a testCtx handle
* type — the link switch that pulls scr_test.c into the binary and has
* the emitted main return scr_test_exit_code() after the loop drains
* (native-toolchain.ts + emitter; the moduleUsesDgram shape). Test-free programs pay
* zero bytes and keep their exact link line. */
export function moduleUsesNodeTest(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "nodeTest").nodeTest;
}
/** True when the module calls a mock.timers member (a test.mockTimers* or
* test.ctxMockTimers* libCall). The emitter then routes the timer calls
* through scr_test.c's mock-aware entry points; other programs keep the
* loop's timer functions. */
export function moduleUsesMockTimers(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "mockTimers").mockTimers;
}
/** True when the module contains any dgram.* or dns.* libCall — the
* link switch that pulls scr_dgram.c into the binary and has the emitted
* main call scr_dgram_install (native-toolchain.ts + emitter; the scr_net gating
* precedent — dns.lookup lives in the same unit, so either prefix
* answers). Dgram-free programs pay zero bytes and keep their exact link
* line. Same generic-walk shape as moduleUsesZlib. */
export function moduleUsesDgram(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "dgram").dgram;
}
/** True when the module contains any http.* libCall — the link switch
* that pulls scr_http.c into the binary (native-toolchain.ts; moduleUsesNet already
* answers true for these, so scr_net.c comes along). */
export function moduleUsesHttpServer(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "http").http;
}
/** True when the module uses the REAL h2 surface (scr_http2.c): any core
* http2.* libCall, or an h2 handle type left behind by a fenced statement
* (its emitted release call needs the unit — the moduleUsesNet story). */
export function moduleUsesHttp2(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "http2").http2;
}
/** True when the module contains any tls.* or https.* libCall — the link
* switch that pulls scr_tls.c and the vendored mbedTLS archive into the
* binary (native-toolchain.ts; moduleUsesNet and moduleUsesHttpServer already answer
* true for these, so scr_net.c and scr_http.c come along). TLS-free
* programs keep their exact link line and never build mbedTLS. */
export function moduleUsesTls(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "tls").tls;
}
/** True when the module contains any tlsca.* libCall — the link switch
* for scr_tls_ca.c, the CA-store introspection unit (getCACertificates /
* rootCertificates / setDefaultCACertificates). Deliberately SEPARATE
* from moduleUsesTls: the unit is plain PEM-block bookkeeping, so a
* program that only inspects the CA store never pulls mbedTLS. native-toolchain.ts
* also compiles the unit whenever TLS itself links — scr_tls.c consults
* the unit's default-set override for its trust anchors. */
export function moduleUsesTlsCa(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "tlsCa").tlsCa;
}
/** True when the module has dynamic-tier jsval operations or inline-cache
* sites: the link switch for the scr_js_* runtime units (two-tier design
* I3). Programs without them never link a scr_js_ symbol. */
export function moduleUsesDynTier(mod: IrModule): boolean {
return scanRuntimeFeatures(mod, "dynTier").dynTier;
}