diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 5de00992..b309826c 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -1,51 +1,44 @@ /** Runtime requirements derived from the lowered program, shared by compiler hosts. */ -import { - moduleEmbedsBuiltin, moduleEmbedsCompressedNpm, moduleUsesAssert, moduleUsesBigInt, - moduleUsesCopying, moduleUsesDc, moduleUsesDgram, moduleUsesDynAsync, moduleUsesDynInvoke, - moduleUsesEmitter, moduleUsesFetch, moduleUsesFileHandle, moduleUsesFsWatch, moduleUsesHttp2, - moduleUsesHttpServer, moduleUsesInspect, moduleUsesLegacyTextDecoder, moduleUsesNet, - moduleUsesNodeTest, moduleUsesParseArgs, moduleUsesProcessEvents, moduleUsesQs, moduleUsesRegex, - moduleUsesSearchParams, moduleUsesStream, moduleUsesSymbol, moduleUsesTls, moduleUsesTlsCa, - moduleUsesZlib, type IrModule, -} from "../ir/ir.js"; +import { moduleEmbedsBuiltin, moduleEmbedsCompressedNpm, moduleRuntimeFeatures, type IrModule } from "../ir/ir.js"; import { hasForeignFfiCallback } from "./ffi-callbacks.js"; import type { NativeLinkFeatures } from "./native-link-info.js"; export function executableLinkFeatures(mod: IrModule, dynamic: boolean): NativeLinkFeatures { + const features = moduleRuntimeFeatures(mod); return { dynamic, - regex: moduleUsesRegex(mod), - copying: moduleUsesCopying(mod), - textDecoderLegacy: moduleUsesLegacyTextDecoder(mod), - fileHandle: moduleUsesFileHandle(mod), - fetch: moduleUsesFetch(mod), + regex: features.regex, + copying: features.copying, + textDecoderLegacy: features.legacyTextDecoder, + fileHandle: features.fileHandle, + fetch: features.fetch, netIsland: moduleEmbedsBuiltin(mod, "node:http") || moduleEmbedsBuiltin(mod, "node:https") || moduleEmbedsBuiltin(mod, "node:net") || moduleEmbedsBuiltin(mod, "node:tls"), - zlib: moduleUsesZlib(mod) || moduleEmbedsCompressedNpm(mod), - assert: moduleUsesAssert(mod), - inspect: moduleUsesInspect(mod), - dynInvoke: moduleUsesDynInvoke(mod), - dc: moduleUsesDc(mod), - dynAsync: moduleUsesDynAsync(mod), - events: moduleUsesProcessEvents(mod), - emitter: moduleUsesEmitter(mod), - symbol: moduleUsesSymbol(mod), - bigint: moduleUsesBigInt(mod), - searchParams: moduleUsesSearchParams(mod), - qs: moduleUsesQs(mod), - parseArgs: moduleUsesParseArgs(mod), - stream: moduleUsesStream(mod), - net: moduleUsesNet(mod), - http: moduleUsesHttpServer(mod), - http2: moduleUsesHttp2(mod), - dgram: moduleUsesDgram(mod), - watch: moduleUsesFsWatch(mod), + zlib: features.zlib || moduleEmbedsCompressedNpm(mod), + assert: features.assert, + inspect: features.inspect, + dynInvoke: features.dynInvoke, + dc: features.dc, + dynAsync: features.dynAsync, + events: features.processEvents, + emitter: features.emitter, + symbol: features.symbol, + bigint: features.bigint, + searchParams: features.searchParams, + qs: features.qs, + parseArgs: features.parseArgs, + stream: features.stream, + net: features.net, + http: features.http, + http2: features.http2, + dgram: features.dgram, + watch: features.fsWatch, foreignFfi: hasForeignFfiCallback(mod.ffiImports ?? []), - nodeTest: moduleUsesNodeTest(mod), - tls: moduleUsesTls(mod), - tlsCa: moduleUsesTlsCa(mod), + nodeTest: features.nodeTest, + tls: features.tls, + tlsCa: features.tlsCa, }; } diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 891a0105..d180f26c 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1,4 +1,3 @@ -import { moduleUsesDgram, moduleUsesHttp2 } from "../../ir/ir.js"; import { emitLlvmLayouts } from "./layouts.js"; import { llvmBytes as llBytes } from "../literals.js"; import { InternalCompilerError } from "../../errors.js"; @@ -37,7 +36,7 @@ import type { IrUnionDef, SrcLoc, } from "../../ir/ir.js"; -import { CAUGHT, ffiCallbackType, isDynTypedRefType, isFfiContextParam, isRefCounted, isUnitType, moduleEmbedsBuiltin, moduleEmbedsCompressedNpm, moduleUsesChildProcess, moduleUsesDynInvoke, moduleUsesFetch, moduleUsesFsWatch, moduleUsesHttpServer, moduleUsesNet, moduleUsesNodeTest, moduleUsesProcessEvents, moduleUsesStream, moduleUsesTls, moduleUsesTlsCa, NPM_COMPRESS_MIN, POINTER_KINDS, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, typeKey, VOID } from "../../ir/ir.js"; +import { CAUGHT, ffiCallbackType, isDynTypedRefType, isFfiContextParam, isRefCounted, isUnitType, moduleRuntimeFeatures, moduleEmbedsBuiltin, moduleEmbedsCompressedNpm, NPM_COMPRESS_MIN, POINTER_KINDS, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, typeKey, VOID } from "../../ir/ir.js"; import { matchIntegerBytesForLoop } from "../../ir/integer-loops.js"; import { scalarizeNumericRecords } from "../../ir/scalar-records.js"; import { analyzeIntegerRanges, type IntegerRanges } from "../../ir/integer-ranges.js"; @@ -847,7 +846,8 @@ export class LlEmitter { // outlive the RC audit — release it with the globals (the runtime ABI’s // sc_release_globals tail). Only when the dispatch unit is even // linked (defineProps is the only writer). - const fnValueProps = moduleUsesDynInvoke(this.mod) ? [...this.fnValues] : []; + const runtimeFeatures = moduleRuntimeFeatures(this.mod); + const fnValueProps = runtimeFeatures.dynInvoke ? [...this.fnValues] : []; if (fnValueProps.length > 0) this.declare(`declare void @scr_box_release(ptr)`); const globalReleaseLines = (prefix: string): string[] => { const lines: string[] = []; @@ -872,26 +872,26 @@ export class LlEmitter { // Exit listeners can read MODULE GLOBALS directly, so they must run // BEFORE the global releases (the runtime ABI’s runExitListeners // ordering — the atexit half becomes an idempotent no-op). - const usesEvents = moduleUsesProcessEvents(this.mod); - const usesChildProcess = moduleUsesChildProcess(this.mod); - const usesFsWatch = moduleUsesFsWatch(this.mod); + const usesEvents = runtimeFeatures.processEvents; + const usesChildProcess = runtimeFeatures.childProcess; + const usesFsWatch = runtimeFeatures.fsWatch; // Stream-surface programs fill the loop's stream hook (the deferred // next-tick emissions) and the emitter's post-registration flow kick // before %main — scr_stream.c links only when the line is emitted // (native-toolchain.ts gates on the same predicate). - const usesStream = moduleUsesStream(this.mod); + const usesStream = runtimeFeatures.stream; // Net-surface programs fill the loop's net hooks (and the netSocket // handle-dispatch ops for the checked-dynamic boundary); http-surface // programs additionally stamp the httpReq/httpRes ops — the C main's // install lines, gated on the same predicates native-toolchain.ts links by. - const usesNet = moduleUsesNet(this.mod); - const usesDgram = moduleUsesDgram(this.mod); - const usesHttp2 = moduleUsesHttp2(this.mod); - const usesHttp = moduleUsesHttpServer(this.mod); + const usesNet = runtimeFeatures.net; + const usesDgram = runtimeFeatures.dgram; + const usesHttp2 = runtimeFeatures.http2; + const usesHttp = runtimeFeatures.http; // Fetch-referencing programs register the native fetch bridge before // any island entry (the engine's lazy boot consults it) — native-toolchain.ts // compiles scr_fetch.c on the same predicate. - const usesFetch = moduleUsesFetch(this.mod); + const usesFetch = runtimeFeatures.fetch; const embedsZlib = moduleEmbedsBuiltin(this.mod, "node:zlib"); const embedsNet = moduleEmbedsBuiltin(this.mod, "node:http") || @@ -899,13 +899,13 @@ export class LlEmitter { moduleEmbedsBuiltin(this.mod, "node:net") || moduleEmbedsBuiltin(this.mod, "node:tls"); const snapshotsTlsCa = - moduleUsesTls(this.mod) || moduleUsesTlsCa(this.mod) || + runtimeFeatures.tls || runtimeFeatures.tlsCa || moduleEmbedsBuiltin(this.mod, "node:https") || moduleEmbedsBuiltin(this.mod, "node:tls"); // The process verdict has the same precedence as the C reference // emitter: node:test owns the final status when present; otherwise an // embedded process.exitCode owns it; ordinary programs return zero. - const usesNodeTest = moduleUsesNodeTest(this.mod); + const usesNodeTest = runtimeFeatures.nodeTest; const programExitUsesIsland = !usesNodeTest && usesIsland; // Declared NOW — the extern block flushes before main assembles. if (usesEvents) this.declare(`declare void @scr_events_install()`); diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index e9cc32c6..7aa77dbe 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -49,8 +49,7 @@ import { import { validateSidecar } from "./library/sidecar-validate.js"; import type { EntryExportInfo } from "./frontend/lib-exports.js"; import type { ContractFacts } from "./frontend/lib-contract.js"; -import { moduleLibAsyncSurface, moduleLibNondeterministicSurface, moduleEmbedsBuiltin, moduleUsesAssert, moduleUsesCopying, moduleUsesEmitter, moduleUsesFetch, moduleUsesInspect, moduleUsesLegacyTextDecoder, moduleUsesRegex, moduleUsesSearchParams, moduleUsesSymbol, moduleUsesZlib, type IrFfiImport, type IrLibSection, type IrModule, type IrRecordShape, type IrType, type SrcLoc } from "./ir/ir.js"; -import { moduleUsesBigInt } from "./ir/ir.js"; +import { moduleRuntimeFeatures, moduleLibAsyncSurface, moduleLibNondeterministicSurface, moduleEmbedsBuiltin, moduleUsesFetch, type IrFfiImport, type IrLibSection, type IrModule, type IrRecordShape, type IrType, type SrcLoc } from "./ir/ir.js"; import { serializeModule } from "./ir/serialize.js"; import { validateModule } from "./ir/validate.js"; import { loadProgram } from "./frontend/program-node.js"; @@ -1728,18 +1727,19 @@ function libraryNativeFeatures( mod: IrModule, backend: "llvm", ): EarlyLibraryNativeFeatures { + const features = moduleRuntimeFeatures(mod); return { backend, - regex: moduleUsesRegex(mod), - assert: moduleUsesAssert(mod), - inspect: moduleUsesInspect(mod), - symbol: moduleUsesSymbol(mod), - bigint: moduleUsesBigInt(mod), - searchParams: moduleUsesSearchParams(mod), - emitter: moduleUsesEmitter(mod), - zlib: moduleUsesZlib(mod), - copying: moduleUsesCopying(mod), - textDecoderLegacy: moduleUsesLegacyTextDecoder(mod), + regex: features.regex, + assert: features.assert, + inspect: features.inspect, + symbol: features.symbol, + bigint: features.bigint, + searchParams: features.searchParams, + emitter: features.emitter, + zlib: features.zlib, + copying: features.copying, + textDecoderLegacy: features.legacyTextDecoder, ...(mod.lib?.identity !== undefined ? { buildId: mod.lib.identity.buildId } : {}), }; } diff --git a/packages/compiler/src/ir/ir.test.ts b/packages/compiler/src/ir/ir.test.ts index 73d80b91..7164bc0d 100644 --- a/packages/compiler/src/ir/ir.test.ts +++ b/packages/compiler/src/ir/ir.test.ts @@ -1,4 +1,6 @@ import { describe, expect, test } from "vitest"; +import { IR_VERSION } from "./serialize.js"; +import { moduleRuntimeFeatures, moduleUsesHttp2, moduleUsesTlsCa, VOID, type IrModule, type IrLibFn } from "./ir.js"; import { DYN, F64, HANDLE_KINDS, BYTES_ELEMENT_NAME, bytesOf, canConvertToDyn, canDynCheckTo, type IrBytesElem, isIslandCallbackParamType, isJsonSafeType, isJsonStringifySafeType, isJsonStringifyDynamicType, type IrRecordShape, type IrType, type IrUnionDef, POINTER_KINDS, STRING, arrayOf, typeEquals, typeKey } from "./ir.js"; describe("IR kind sets", () => { @@ -120,3 +122,58 @@ describe("native bigint checked storage", () => { } }); }); + + +describe("runtime feature snapshots", () => { + const loc = { file: "features.ts", start: 0, end: 0 }; + const module = (): IrModule => ({ + irVersion: IR_VERSION, sourceFile: loc.file, entry: "main", + functions: [{ name: "main", params: [], locals: [], returnType: VOID, body: [], loc }], + }); + const call = (fn: IrLibFn) => ({ + kind: "exprStmt" as const, loc, + expr: { kind: "libCall" as const, fn, args: [], type: VOID, loc }, + }); + + test("updates link requirements after an IR edit without changing earlier snapshots", () => { + const mod = module(); + const first = moduleRuntimeFeatures(mod); + mod.functions[0]!.body.push(call("http2.connect"), call("tlsca.get")); + expect(moduleRuntimeFeatures(mod)).toMatchObject({ net: true, http: true, http2: true, tls: true, tlsCa: true }); + expect(moduleUsesHttp2(mod)).toBe(true); + expect(moduleUsesTlsCa(mod)).toBe(true); + expect(first).toMatchObject({ net: false, http: false, http2: false, tls: false, tlsCa: false }); + mod.functions[0]!.body = []; + expect(moduleRuntimeFeatures(mod)).toEqual(first); + expect(moduleUsesHttp2(mod)).toBe(false); + }); + + test("keeps legacy HTTP/2 calls and isolated CA inspection out of the HTTP/2 unit", () => { + const mod = module(); + mod.functions[0]!.body.push(call("http2.streamNoop")); + expect(moduleRuntimeFeatures(mod)).toMatchObject({ net: true, http: true, http2: false, tls: true }); + mod.functions[0]!.body = [call("tlsca.get")]; + expect(moduleRuntimeFeatures(mod)).toMatchObject({ net: false, http: false, http2: false, tls: false, tlsCa: true }); + }); + + test("retains runtime units needed only by nested handle storage", () => { + const mod = module(); + mod.functions[0]!.locals.push({ + id: "handles", name: "handles", mutable: false, + type: arrayOf({ kind: "promise", inner: { kind: "secureCtx" } }), + }); + expect(moduleRuntimeFeatures(mod)).toMatchObject({ net: true, http: true, tls: true, http2: false }); + }); + + test("detects promise boxing independently of static promise storage", () => { + const mod = module(); + const promise = { kind: "promise" as const, inner: STRING }; + mod.functions[0]!.locals.push({ id: "promise", name: "promise", mutable: false, type: promise }); + expect(moduleRuntimeFeatures(mod).dynAsync).toBe(false); + mod.functions[0]!.body.push({ kind: "exprStmt", loc, expr: { + kind: "dynFrom", type: DYN, loc, + value: { kind: "varRef", localId: "promise", type: promise, loc }, + } }); + expect(moduleRuntimeFeatures(mod).dynAsync).toBe(true); + }); +}); diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index de6ccf07..46df8808 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -7,8 +7,8 @@ import { InternalCompilerError } from "../errors.js"; * - Every node carries `loc` (source span) and every expression carries its * computed `type` — backends never re-derive types. * - Structured control flow (statement tree), not basic blocks: the only - * current backend is C and there are no optimization passes yet. When a mid-end - * arrives, a structured→CFG lowering pass slots in after validation. + * LLVM backend lowers these into control flow. A future IR optimization + * pass can run after validation. * - The type union is deliberately written for extension (`dyn` shipped * with JSON, `promise` with async). All switches over IrType/IrExpr/ * IrStmt must have exhaustive `never` default arms so adding a member @@ -6502,6 +6502,156 @@ export function canExitIslandToType( return false; } +export interface RuntimeFeatures { + regex: boolean; + copying: boolean; + legacyTextDecoder: boolean; + fileHandle: boolean; + fetch: boolean; + processEvents: boolean; + emitter: boolean; + stream: boolean; + zlib: boolean; + dc: boolean; + assert: boolean; + dynInvoke: boolean; + dynAsync: boolean; + inspect: boolean; + childProcess: boolean; + net: boolean; + symbol: boolean; + bigint: boolean; + searchParams: boolean; + qs: boolean; + parseArgs: boolean; + fsWatch: boolean; + nodeTest: boolean; + dgram: boolean; + http: boolean; + http2: boolean; + tls: boolean; + tlsCa: boolean; +} + +const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ + "async.awaitDyn", "timers.immediatePromise", + "process.onUncaughtException", "process.offUncaughtException", + "process.onUnhandledRejection", "process.offUnhandledRejection", + "process.onRejectionHandled", "process.offRejectionHandled", + "process.onWarning", "process.offWarning", "process.emitWarning", + "als.new", "als.get", "als.run", "als.exitRun", "als.enterWith", "als.disable", + "dc.chanBindStore", "dc.chanUnbindStore", "dc.chanRunStores", +]); + +/** Inspect types and expressions together in one traversal. This is a fresh + * snapshot: callers may mutate an IR module between compilation passes. */ +export function moduleRuntimeFeatures(mod: IrModule): RuntimeFeatures { + return scanRuntimeFeatures(mod); +} + +function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): RuntimeFeatures { + const features: RuntimeFeatures = { + regex: false, copying: false, legacyTextDecoder: false, fileHandle: false, + fetch: mod.embedded?.modules.some((m) => m.usesFetch === true) ?? false, + processEvents: false, emitter: (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS), + stream: (mod.classes ?? []).some((c) => RUNTIME_STREAM_CLASSES.has(c.name)), + zlib: mod.embedded?.edges.some((e) => e.to === "node:zlib") ?? false, + dc: false, assert: false, dynInvoke: false, dynAsync: false, inspect: false, + childProcess: false, net: false, symbol: false, bigint: false, searchParams: false, + qs: false, parseArgs: false, fsWatch: false, nodeTest: false, dgram: false, + http: false, http2: false, tls: false, tlsCa: false, + }; + const visit = (value: unknown): void => { + if (value === null || typeof value !== "object" || (stopAt !== undefined && features[stopAt])) return; + if (Array.isArray(value)) { + for (const item of value) visit(item); + return; + } + const node = value as { + kind?: unknown; fn?: unknown; method?: unknown; op?: unknown; name?: unknown; + type?: { kind?: unknown }; + value?: { type?: { kind?: unknown; ret?: { kind?: unknown } } }; + }; + const kind = node.kind; + if (kind === "libCall" && typeof node.fn === "string") { + const fn = node.fn; + if (fn === "regexp.escape" || fn === "dyn.nativeRegexIs") features.regex = true; + if (fn === "text.decodeLegacy") features.legacyTextDecoder = true; + if (fn.startsWith("fetch.")) features.fetch = true; + if (PROCESS_EVENT_LIB_FNS.has(fn)) features.processEvents = true; + if (fn.startsWith("emitter.")) features.emitter = true; + if (fn.startsWith("readable.") || fn.startsWith("writable.") || fn.startsWith("duplex.") || + fn.startsWith("transform.") || fn.startsWith("passthrough.") || fn.startsWith("sc.") || + fn.startsWith("stream.set") || fn === "stream.destroy" || fn === "stream.destroyErr" || + fn === "stream.prop" || fn === "stream.errored" || fn === "sp.finished" || fn === "sp.pipeline") features.stream = true; + if (fn.startsWith("zlib.")) features.zlib = true; + if (fn.startsWith("dc.")) features.dc = true; + if (fn.startsWith("assert.")) features.assert = true; + if (fn === "dyn.defineProps" || fn === "dyn.defineProperty" || fn === "dyn.objCreateWithProperties" || fn === "dyn.arrayProtoCall") features.dynInvoke = true; + if (DYN_ASYNC_LIB_FNS.has(fn)) features.dynAsync = true; + if (fn.startsWith("insp.")) features.inspect = true; + if (fn.startsWith("cp.") || fn.startsWith("child.") || fn.startsWith("writer.") || fn.startsWith("spawnRes.") || + fn === "process.forkTarget" || fn === "process.connected" || fn === "process.send" || fn === "process.sendCb" || + fn === "process.disconnect" || fn === "process.onMessage" || fn === "process.onDisconnect") features.childProcess = true; + if (fn.startsWith("net.")) features.net = true; + if (fn.startsWith("http.")) { features.net = true; features.http = true; } + if (fn.startsWith("tls.") || fn.startsWith("https.") || fn.startsWith("http2.")) { + features.net = true; features.http = true; features.tls = true; + } + if (fn.startsWith("http2.") && !HTTP2_LEGACY_FNS.has(fn)) features.http2 = true; + if (fn.startsWith("sym.")) features.symbol = true; + if (fn.startsWith("bigint.")) features.bigint = true; + if (fn.startsWith("sp.") || fn === "url.searchParams") features.searchParams = true; + if (fn === "qs.parse" || fn === "qs.stringify" || fn === "qs.unescape") features.qs = true; + if (fn === "util.parseArgs") features.parseArgs = true; + if (fn.startsWith("fs.watch") || fn.startsWith("watcher.")) features.fsWatch = true; + if (fn.startsWith("test.")) features.nodeTest = true; + if (fn.startsWith("dgram.") || fn.startsWith("dns.")) features.dgram = true; + if (fn.startsWith("tlsca.")) features.tlsCa = true; + } else { + switch (kind) { + case "regex": case "regexLit": case "regexIntrinsic": features.regex = true; break; + case "strIntrinsic": + if (node.method === "toLowerCase" || node.method === "toUpperCase") features.regex = true; + break; + case "arrIntrinsic": + if (node.method === "toReversed" || node.method === "toSpliced" || node.method === "with" || node.method === "withUndefined") features.copying = true; + break; + case "bytesIntrinsic": + if (node.method === "toReversed" || node.method === "with" || node.method === "join" || node.method === "toArray") features.copying = true; + break; + case "fileHandle": features.fileHandle = true; break; + case "jsOp": + if (node.op === "globalGet" && node.name === "fetch") features.fetch = true; + break; + case "dynInvoke": features.dynInvoke = true; break; + case "awaitExpr": + if (node.type?.kind === "dyn") features.dynAsync = true; + break; + case "dynFrom": { + const boxed = node.value?.type; + if (boxed?.kind === "promise" || (boxed?.kind === "func" && boxed.ret?.kind === "promise")) features.dynAsync = true; + break; + } + case "child": case "childStream": case "childWriter": case "spawnRes": features.childProcess = true; break; + case "netServer": case "netSocket": features.net = true; break; + case "http2Session": case "http2Stream": features.net = true; features.http2 = true; break; + case "httpReq": case "httpRes": case "httpClientReq": features.net = true; features.http = true; break; + case "secureCtx": features.net = true; features.http = true; features.tls = true; break; + case "symbol": features.symbol = true; break; + case "bigint": features.bigint = true; break; + case "searchParams": features.searchParams = true; break; + case "fsWatcher": features.fsWatch = true; break; + case "testCtx": features.nodeTest = true; break; + case "dgramSocket": features.dgram = true; break; + } + } + for (const key of Object.keys(value)) visit((value as Record)[key]); + }; + visit(mod); + 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 @@ -6509,118 +6659,28 @@ export function canExitIslandToType( * JSON walk: `kind` discriminants live only on IR objects, so user string * VALUES can never false-positive. */ export function moduleUsesRegex(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const kind = (v as { kind?: unknown }).kind; - if (kind === "regex" || kind === "regexLit" || kind === "regexIntrinsic") { - found = true; - return; - } - // The lre-backed case conversions live in scr_regex.c (libunicode's - // case tables): they ride the same link switch as regex nodes. - if (kind === "strIntrinsic") { - const method = (v as { method?: unknown }).method; - if (method === "toLowerCase" || method === "toUpperCase") { - found = true; - return; - } - } - // RegExp.escape lives in scr_regex.c too (needing no engine — it - // keeps the always-linked string TU out of hello-world's size class). - if (kind === "libCall" && ["regexp.escape", "dyn.nativeRegexIs"].includes(String((v as { fn?: unknown }).fn))) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + 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 { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; method?: unknown }; - if ( - node.kind === "arrIntrinsic" && - (node.method === "toReversed" || - node.method === "toSpliced" || - node.method === "with" || - node.method === "withUndefined") - ) { - found = true; - return; - } - if ( - node.kind === "bytesIntrinsic" && - (node.method === "toReversed" || - node.method === "with" || - node.method === "join" || - node.method === "toArray") - ) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + 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 { - let found = false; - const visit = (value: unknown): void => { - if (found || value === null || typeof value !== "object") return; - if (Array.isArray(value)) { - for (const item of value) visit(item); - return; - } - const node = value as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && node.fn === "text.decodeLegacy") { - found = true; - return; - } - for (const key of Object.keys(value)) visit((value as Record)[key]); - }; - visit(mod); - return found; + 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 { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - if ((v as { kind?: unknown }).kind === "fileHandle") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "fileHandle").fileHandle; } /** True when user code lowers static fetch or the embedded npm graph @@ -6631,29 +6691,7 @@ export function moduleUsesFileHandle(mod: IrModule): boolean { * and local `fetch` bindings do not change the link or target capability. * Fetch-free graphs keep their exact historical link lines. */ export function moduleUsesFetch(mod: IrModule): boolean { - const embedded = mod.embedded; - if (embedded && embedded.modules.some((m) => m.usesFetch === true)) return true; - // User-code fetch is either the engine global or the static libCall pair. - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; op?: unknown; name?: unknown; fn?: unknown }; - if ( - (node.kind === "jsOp" && node.op === "globalGet" && node.name === "fetch") || - (node.kind === "libCall" && - typeof node.fn === "string" && node.fn.startsWith("fetch.")) - ) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "fetch").fetch; } /** The libCall fns served by the OPTIONAL events unit (scr_events.c): @@ -6682,22 +6720,7 @@ const PROCESS_EVENT_LIB_FNS: ReadonlySet = new Set([ * 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 { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && PROCESS_EVENT_LIB_FNS.has(node.fn)) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "processEvents").processEvents; } /** True when the module uses the node:events EventEmitter surface — the @@ -6710,23 +6733,7 @@ export function moduleUsesProcessEvents(mod: IrModule): boolean { * no emitter-typed value). Emitter-free programs pay zero bytes and keep * their exact link line. */ export function moduleUsesEmitter(mod: IrModule): boolean { - if ((mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS)) return true; - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && node.fn.startsWith("emitter.")) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "emitter").emitter; } /** True when the program touches the node:stream surface (scr_stream.c — @@ -6737,34 +6744,7 @@ export function moduleUsesEmitter(mod: IrModule): boolean { * moduleUsesEmitter answers true whenever this does. Stream-free * programs pay zero bytes and keep their exact link line. */ export function moduleUsesStream(mod: IrModule): boolean { - if ((mod.classes ?? []).some((c) => RUNTIME_STREAM_CLASSES.has(c.name))) return true; - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if ( - node.kind === "libCall" && - typeof node.fn === "string" && - (node.fn.startsWith("readable.") || node.fn.startsWith("writable.") || - node.fn.startsWith("duplex.") || node.fn.startsWith("transform.") || - node.fn.startsWith("passthrough.") || - node.fn === "stream.destroy" || node.fn === "stream.destroyErr" || - node.fn === "stream.prop" || node.fn === "stream.errored" || - node.fn === "sp.finished" || node.fn === "sp.pipeline" || - node.fn.startsWith("sc.") || - node.fn.startsWith("stream.set")) - ) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "stream").stream; } /** True when the embedded npm graph has an edge into `builtin` — the @@ -6801,23 +6781,7 @@ export function moduleEmbedsCompressedNpm(mod: IrModule): boolean { * generic-walk shape as moduleUsesRegex: `kind`/`fn` discriminants live * only on IR objects. */ export function moduleUsesZlib(mod: IrModule): boolean { - if (moduleEmbedsBuiltin(mod, "node:zlib")) return true; - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && node.fn.startsWith("zlib.")) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "zlib").zlib; } /** True when the module contains any dc.* libCall — the link switch that @@ -6825,22 +6789,7 @@ export function moduleUsesZlib(mod: IrModule): boolean { * binary (native-toolchain.ts). Channel-free binaries keep their exact size class. * Same walk shape as moduleUsesZlib. */ export function moduleUsesDc(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && node.fn.startsWith("dc.")) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "dc").dc; } /** True when the module contains any assert libCall — the link switch @@ -6850,22 +6799,7 @@ export function moduleUsesDc(mod: IrModule): boolean { * the historical command line and size. Same walk shape as * moduleUsesZlib. */ export function moduleUsesAssert(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && node.fn.startsWith("assert.")) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "assert").assert; } /** True when the module contains any dynInvoke node or dyn.defineProps @@ -6875,25 +6809,7 @@ export function moduleUsesAssert(mod: IrModule): boolean { * precedent — dispatch-free binaries keep their exact size class). Same * walk shape as moduleUsesZlib. */ export function moduleUsesDynInvoke(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "dynInvoke" || (node.kind === "libCall" && - (node.fn === "dyn.defineProps" || node.fn === "dyn.defineProperty" || - node.fn === "dyn.objCreateWithProperties" || - node.fn === "dyn.arrayProtoCall"))) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "dynInvoke").dynInvoke; } /** True when the module contains any insp libCall — the link switch that @@ -6907,92 +6823,18 @@ export function moduleUsesDynInvoke(mod: IrModule): boolean { * dynInvoke and dc gates (their TUs call into this one) — native-toolchain.ts. Same * walk shape as moduleUsesZlib. */ export function moduleUsesDynAsync(mod: IrModule): boolean { - const fns = new Set([ - "async.awaitDyn", "timers.immediatePromise", - "process.onUncaughtException", "process.offUncaughtException", - "process.onUnhandledRejection", "process.offUnhandledRejection", - "process.onRejectionHandled", "process.offRejectionHandled", - "process.onWarning", "process.offWarning", "process.emitWarning", - "als.new", "als.get", "als.run", "als.exitRun", "als.enterWith", "als.disable", - "dc.chanBindStore", "dc.chanUnbindStore", "dc.chanRunStores", - ]); - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown; type?: { kind?: unknown } }; - if (node.kind === "libCall" && typeof node.fn === "string" && fns.has(node.fn)) { - found = true; - return; - } - // A DYN-typed await reads through scr_await_dyn (the checked-dynamic tree-crossing - // await lives in the gated TU) — promise receivers' awaits and - // the lifted then/catch helpers alike. - const boxed = (node as { value?: { type?: { kind?: unknown; ret?: { kind?: unknown } } } }).value?.type; - if ((node.kind === "awaitExpr" && node.type?.kind === "dyn") || - (node.kind === "dynFrom" && (boxed?.kind === "promise" || (boxed?.kind === "func" && boxed.ret?.kind === "promise")))) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "dynAsync").dynAsync; } export function moduleUsesInspect(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && node.fn.startsWith("insp.")) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + 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 { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if ( - node.kind === "libCall" && - typeof node.fn === "string" && - (node.fn.startsWith("cp.") || node.fn.startsWith("child.") || node.fn.startsWith("writer.") || node.fn.startsWith("spawnRes.") || - node.fn === "process.forkTarget" || node.fn === "process.connected" || node.fn === "process.send" || - node.fn === "process.sendCb" || node.fn === "process.disconnect" || node.fn === "process.onMessage" || - node.fn === "process.onDisconnect") - ) { - found = true; - return; - } - if (node.kind === "child" || node.kind === "childStream" || node.kind === "childWriter" || node.kind === "spawnRes") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "childProcess").childProcess; } /** True when the module contains any net libCall — the link switch that @@ -7001,39 +6843,7 @@ export function moduleUsesChildProcess(mod: IrModule): boolean { * Net-free programs pay zero bytes and keep their exact link line. Same * generic-walk shape as moduleUsesZlib. */ export function moduleUsesNet(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && - (node.fn.startsWith("net.") || node.fn.startsWith("http.") || - node.fn.startsWith("tls.") || node.fn.startsWith("https.") || - node.fn.startsWith("http2."))) { - // http/tls/https/http2 all ride ON net: their libCalls need scr_net.c - // linked and installed too, so the net switch answers for every - // server-family prefix. - found = true; - return; - } - // A net-family HANDLE TYPE anywhere in the IR (a global or local whose - // initializing statement compiled to a runtime fence still carries the - // type): its emitted release call needs the unit linked even when no - // libCall survived the fencing. - if (node.kind === "netServer" || node.kind === "netSocket" || - node.kind === "http2Session" || node.kind === "http2Stream" || - node.kind === "httpReq" || node.kind === "httpRes" || - node.kind === "httpClientReq" || node.kind === "secureCtx") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "net").net; } /** True when the module contains any sym.* libCall or a symbol-kind type @@ -7044,51 +6854,14 @@ export function moduleUsesNet(mod: IrModule): boolean { * 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 { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && node.fn.startsWith("sym.")) { - found = true; - return; - } - if (node.kind === "symbol") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + 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 { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if ( - node.kind === "bigint" || - (node.kind === "libCall" && typeof node.fn === "string" && node.fn.startsWith("bigint.")) - ) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "bigint").bigint; } /** True when the module uses the URLSearchParams surface — sp.* libCalls, @@ -7100,27 +6873,7 @@ export function moduleUsesBigInt(mod: IrModule): boolean { * programs keep their exact link line; scr_url.c itself stays * always-linked and never references the unit. */ export function moduleUsesSearchParams(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && - (node.fn.startsWith("sp.") || node.fn === "url.searchParams")) { - found = true; - return; - } - if (node.kind === "searchParams") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "searchParams").searchParams; } /** True when the module uses the node:querystring surface — the qs.* @@ -7131,45 +6884,14 @@ export function moduleUsesSearchParams(mod: IrModule): boolean { * 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 { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && - (node.fn === "qs.parse" || node.fn === "qs.stringify" || node.fn === "qs.unescape")) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "qs").qs; } /** True when the module uses native util.parseArgs — the link switch for * scr_util.c. The implementation is a pure checked-dynamic data transform, * cross-platform and independent of the island's node:util shim. */ export function moduleUsesParseArgs(mod: IrModule): boolean { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && node.fn === "util.parseArgs") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "parseArgs").parseArgs; } /** True when the module contains any fs.watch/watcher.* libCall — the @@ -7178,29 +6900,7 @@ export function moduleUsesParseArgs(mod: IrModule): boolean { * 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 { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && - (node.fn.startsWith("fs.watch") || node.fn.startsWith("watcher."))) { - found = true; - return; - } - // A watcher HANDLE TYPE left behind by a fenced statement still emits - // scr_watcher_release — the unit must link. - if (node.kind === "fsWatcher") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "fsWatch").fsWatch; } /** True when the module contains any test.* libCall or a testCtx handle @@ -7209,28 +6909,7 @@ export function moduleUsesFsWatch(mod: IrModule): boolean { * (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 { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && node.fn.startsWith("test.")) { - found = true; - return; - } - // A TestContext HANDLE TYPE left behind by a fenced statement still - // emits a release call — the unit must link (the dgram type story). - if (node.kind === "testCtx") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "nodeTest").nodeTest; } /** True when the module contains any dgram.* or dns.* libCall — the @@ -7240,64 +6919,14 @@ export function moduleUsesNodeTest(mod: IrModule): boolean { * 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 { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && - (node.fn.startsWith("dgram.") || node.fn.startsWith("dns."))) { - found = true; - return; - } - // A dgram HANDLE TYPE left behind by a fenced statement still emits a - // release call — the unit must link (the moduleUsesNet type story). - if (node.kind === "dgramSocket") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + 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 { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && - (node.fn.startsWith("http.") || node.fn.startsWith("tls.") || node.fn.startsWith("https.") || - node.fn.startsWith("http2."))) { - // scr_tls.c calls into scr_http.c unconditionally (the https server - // and client are the http ones over a transport), so any tls/https - // use pulls the http unit too. - found = true; - return; - } - // An http-family HANDLE TYPE left behind by a fenced statement still - // emits its release call — the unit must link (the moduleUsesNet type - // story; secureCtx rides here because scr_tls.c calls into scr_http.c). - if (node.kind === "httpReq" || node.kind === "httpRes" || - node.kind === "httpClientReq" || node.kind === "secureCtx") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "http").http; } /** The legacy http2.* libCalls implemented by scr_http.c/scr_tls.c (the @@ -7311,27 +6940,7 @@ const HTTP2_LEGACY_FNS = new Set([ * 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 { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && - node.fn.startsWith("http2.") && !HTTP2_LEGACY_FNS.has(node.fn)) { - found = true; - return; - } - if (node.kind === "http2Session" || node.kind === "http2Stream") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "http2").http2; } /** True when the module contains any tls.* or https.* libCall — the link @@ -7340,30 +6949,7 @@ export function moduleUsesHttp2(mod: IrModule): boolean { * 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 { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && - (node.fn.startsWith("tls.") || node.fn.startsWith("https.") || - node.fn.startsWith("http2."))) { - found = true; - return; - } - // A secureCtx HANDLE TYPE left behind by a fenced statement still - // emits scr_secure_ctx_release — the unit must link. - if (node.kind === "secureCtx") { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "tls").tls; } /** True when the module contains any tlsca.* libCall — the link switch @@ -7374,22 +6960,7 @@ export function moduleUsesTls(mod: IrModule): boolean { * 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 { - let found = false; - const visit = (v: unknown): void => { - if (found || v === null || typeof v !== "object") return; - if (Array.isArray(v)) { - for (const item of v) visit(item); - return; - } - const node = v as { kind?: unknown; fn?: unknown }; - if (node.kind === "libCall" && typeof node.fn === "string" && node.fn.startsWith("tlsca.")) { - found = true; - return; - } - for (const key of Object.keys(v)) visit((v as Record)[key]); - }; - visit(mod); - return found; + return scanRuntimeFeatures(mod, "tlsCa").tlsCa; } /* ── library mode's async_free gate ────────────────────────────────────── diff --git a/tests/fixtures/self-hosting/runtime-features.ts b/tests/fixtures/self-hosting/runtime-features.ts index 5ec71466..fb97dd70 100644 --- a/tests/fixtures/self-hosting/runtime-features.ts +++ b/tests/fixtures/self-hosting/runtime-features.ts @@ -1,27 +1,6 @@ import { readFileSync } from "node:fs"; import { deserializeModule } from "../../../packages/compiler/src/ir/serialize.js"; -import { - moduleUsesRegex, moduleUsesCopying, moduleUsesLegacyTextDecoder, - moduleUsesFileHandle, moduleUsesFetch, moduleUsesProcessEvents, - moduleUsesEmitter, moduleUsesStream, moduleUsesZlib, moduleUsesDc, - moduleUsesAssert, moduleUsesDynInvoke, moduleUsesDynAsync, - moduleUsesInspect, moduleUsesChildProcess, moduleUsesNet, - moduleUsesSymbol, moduleUsesBigInt, moduleUsesSearchParams, moduleUsesQs, - moduleUsesParseArgs, moduleUsesFsWatch, moduleUsesNodeTest, - moduleUsesDgram, moduleUsesHttpServer, moduleUsesHttp2, moduleUsesTls, moduleUsesTlsCa, -} from "../../../packages/compiler/src/ir/ir.js"; +import { moduleRuntimeFeatures } from "../../../packages/compiler/src/ir/ir.js"; const mod = deserializeModule(readFileSync(process.argv[2]!, "utf8")); -console.log(JSON.stringify({ - regex: moduleUsesRegex(mod), copying: moduleUsesCopying(mod), - legacyTextDecoder: moduleUsesLegacyTextDecoder(mod), fileHandle: moduleUsesFileHandle(mod), - fetch: moduleUsesFetch(mod), processEvents: moduleUsesProcessEvents(mod), - emitter: moduleUsesEmitter(mod), stream: moduleUsesStream(mod), - zlib: moduleUsesZlib(mod), dc: moduleUsesDc(mod), assert: moduleUsesAssert(mod), - dynInvoke: moduleUsesDynInvoke(mod), dynAsync: moduleUsesDynAsync(mod), - inspect: moduleUsesInspect(mod), childProcess: moduleUsesChildProcess(mod), net: moduleUsesNet(mod), - symbol: moduleUsesSymbol(mod), bigint: moduleUsesBigInt(mod), searchParams: moduleUsesSearchParams(mod), - qs: moduleUsesQs(mod), parseArgs: moduleUsesParseArgs(mod), fsWatch: moduleUsesFsWatch(mod), - nodeTest: moduleUsesNodeTest(mod), dgram: moduleUsesDgram(mod), http: moduleUsesHttpServer(mod), - http2: moduleUsesHttp2(mod), tls: moduleUsesTls(mod), tlsCa: moduleUsesTlsCa(mod), -})); +console.log(JSON.stringify(moduleRuntimeFeatures(mod))); diff --git a/tests/harness/self-hosting-frontend-program.test.ts b/tests/harness/self-hosting-frontend-program.test.ts index 3534240c..7c496cfe 100644 --- a/tests/harness/self-hosting-frontend-program.test.ts +++ b/tests/harness/self-hosting-frontend-program.test.ts @@ -1,8 +1,9 @@ -import { execFileSync, spawnSync } from "node:child_process"; +import { execFile, execFileSync } from "node:child_process"; +import { promisify } from "node:util"; import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; -import { analyze, compile } from "@scriptc/compiler"; +import { analyzeInChild, compileInChild } from "./self-hosting-compiler-process.js"; import { expect, test } from "vitest"; import { ts7Executable } from "../../packages/compiler/src/frontend/ts7/rpc-api.js"; import type { FrontendProgramRequest } from "../fixtures/self-hosting/frontend-program-cases.js"; @@ -13,6 +14,7 @@ const oracle = join(root, "tests/fixtures/self-hosting/frontend-program-node.ts" const nativeSources = join(root, "packages/compiler/native"); const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp"; const sanitize = process.env["SCRIPTC_SAN"] === "1"; +const execFileAsync = promisify(execFile); function inputs(directory: string): FrontendProgramRequest { const cases: FrontendProgramRequest["cases"] = []; @@ -236,30 +238,25 @@ for (const backend of ["llvm"] as const) { const request = join(directory, "request.json"); const expected = join(directory, "node.json"); writeFileSync(request, JSON.stringify(inputs(directory))); - const node = spawnSync(process.execPath, ["--import", "tsx", oracle, ts7Executable(), request, expected], { encoding: "utf8", timeout: 90_000 }); - expect(node.error, node.stderr).toBeUndefined(); - expect(node.status, node.stderr).toBe(0); + const node = await execFileAsync(process.execPath, ["--import", "tsx", oracle, ts7Executable(), request, expected], { encoding: "utf8", timeout: 90_000 }); expect(node.stdout).toBe(""); expect(node.stderr).toBe(""); const nodeReports = JSON.parse(readFileSync(expected, "utf8")) as Report[]; verify(nodeReports); // Restore edits made by the Node run before starting the native client. writeFileSync(request, JSON.stringify(inputs(directory))); - const { coverage } = analyze(entry, { dynamic: false, ffiProfilePath: profile }); + const coverage = await analyzeInChild(entry, { dynamic: false, ffiProfilePath: profile }); expect(coverage.preflightFailed, JSON.stringify(coverage.diagnostics)).toBe(false); expect(coverage.stats.statementsFailed, JSON.stringify(coverage.diagnostics)).toBe(0); expect(coverage.stats.statementsIsland).toBe(0); expect(coverage.stats.functionsSkipped).toBe(0); - const built = await compile(entry, { + const built = await compileInChild(entry, { backend, dynamic: false, optimization: "dev", sanitize, ffiProfilePath: profile, outDir: directory, outPath: join(directory, process.platform === "win32" ? "frontend.exe" : "frontend"), }); if (!built.ok) throw new Error(built.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); const report = join(directory, "native.json"); - const run = spawnSync(built.binaryPath, [ts7Executable(), request, report], { encoding: "utf8", timeout: 90_000 }); - expect(run.error, run.stderr).toBeUndefined(); - expect(run.signal, run.stderr).toBeNull(); - expect(run.status, run.stderr).toBe(0); + const run = await execFileAsync(built.binaryPath, [ts7Executable(), request, report], { encoding: "utf8", timeout: 90_000 }); expect(run.stdout).toBe(node.stdout); expect(run.stderr).toBe(node.stderr); const nativeReports = JSON.parse(readFileSync(report, "utf8")) as Report[]; diff --git a/tests/harness/self-hosting-native-features.ts b/tests/harness/self-hosting-native-features.ts index 8315605e..a72a5f2f 100644 --- a/tests/harness/self-hosting-native-features.ts +++ b/tests/harness/self-hosting-native-features.ts @@ -1,17 +1,6 @@ -import * as ir from "../../packages/compiler/src/ir/ir.js"; +import { executableLinkFeatures } from "../../packages/compiler/src/backend/executable-features.js"; +import type { IrModule } from "../../packages/compiler/src/ir/ir.js"; -export function nativeFeatures(module: ir.IrModule) { - return { - regex: ir.moduleUsesRegex(module), copying: ir.moduleUsesCopying(module), - inspect: ir.moduleUsesInspect(module), dynInvoke: ir.moduleUsesDynInvoke(module), - symbol: ir.moduleUsesSymbol(module), bigint: ir.moduleUsesBigInt(module), - zlib: ir.moduleUsesZlib(module), assert: ir.moduleUsesAssert(module), - textDecoderLegacy: ir.moduleUsesLegacyTextDecoder(module), fileHandle: ir.moduleUsesFileHandle(module), - fetch: ir.moduleUsesFetch(module), dc: ir.moduleUsesDc(module), dynAsync: ir.moduleUsesDynAsync(module), - events: ir.moduleUsesProcessEvents(module), emitter: ir.moduleUsesEmitter(module), - searchParams: ir.moduleUsesSearchParams(module), qs: ir.moduleUsesQs(module), parseArgs: ir.moduleUsesParseArgs(module), - stream: ir.moduleUsesStream(module), net: ir.moduleUsesNet(module), http: ir.moduleUsesHttpServer(module), - http2: ir.moduleUsesHttp2(module), dgram: ir.moduleUsesDgram(module), watch: ir.moduleUsesFsWatch(module), - nodeTest: ir.moduleUsesNodeTest(module), tls: ir.moduleUsesTls(module), tlsCa: ir.moduleUsesTlsCa(module), - }; +export function nativeFeatures(module: IrModule) { + return executableLinkFeatures(module, false); } diff --git a/tests/harness/self-hosting-runtime-features.test.ts b/tests/harness/self-hosting-runtime-features.test.ts index d9c55fb7..ce26b8e2 100644 --- a/tests/harness/self-hosting-runtime-features.test.ts +++ b/tests/harness/self-hosting-runtime-features.test.ts @@ -52,7 +52,7 @@ const featureCalls: readonly [string, IrLibFn][] = [ test("the production runtime dependency scanners lower entirely statically", () => { const { coverage } = analyze(entry, { dynamic: false }); expect(coverage.preflightFailed).toBe(false); - expect(coverage.stats.statementsTotal).toBeGreaterThan(500); + expect(coverage.stats.statementsTotal).toBeGreaterThan(150); expect(coverage.stats.statementsFailed).toBe(0); expect(coverage.stats.statementsIsland).toBe(0); expect(coverage.stats.functionsSkipped).toBe(0); diff --git a/tests/harness/self-hosting-source-parser.test.ts b/tests/harness/self-hosting-source-parser.test.ts index 11febdde..33a521b2 100644 --- a/tests/harness/self-hosting-source-parser.test.ts +++ b/tests/harness/self-hosting-source-parser.test.ts @@ -1,8 +1,9 @@ -import { execFileSync, spawnSync } from "node:child_process"; +import { execFile, execFileSync } from "node:child_process"; +import { promisify } from "node:util"; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { analyze, compile } from "@scriptc/compiler"; +import { analyzeInChild, compileInChild } from "./self-hosting-compiler-process.js"; import { expect, test } from "vitest"; import { ts7Executable } from "../../packages/compiler/src/frontend/ts7/rpc-api.js"; @@ -12,6 +13,7 @@ const oracle = join(root, "tests/fixtures/self-hosting/ts7-source-parser-node.ts const nativeSources = join(root, "packages/compiler/native"); const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp"; const sanitize = process.env["SCRIPTC_SAN"] === "1"; +const execFileAsync = promisify(execFile); function input() { const fixtures = join(root, "tests/fixtures/npm-static/node_modules"); @@ -91,13 +93,13 @@ for (const backend of ["llvm"] as const) { writeFileSync(profile, JSON.stringify({ ...JSON.parse(readFileSync(join(nativeSources, "ts7-process.ffi.json"), "utf8")), libraries: [object], })); - const { coverage } = analyze(entry, { dynamic: false, ffiProfilePath: profile }); + const coverage = await analyzeInChild(entry, { dynamic: false, ffiProfilePath: profile }); expect(coverage.preflightFailed, JSON.stringify(coverage.diagnostics)).toBe(false); expect(coverage.diagnostics).toEqual([]); expect(coverage.stats.statementsFailed).toBe(0); expect(coverage.stats.statementsIsland).toBe(0); expect(coverage.stats.functionsSkipped).toBe(0); - const built = await compile(entry, { + const built = await compileInChild(entry, { backend, dynamic: false, optimization: "dev", sanitize, ffiProfilePath: profile, outDir: directory, outPath: join(directory, process.platform === "win32" ? "parser.exe" : "parser"), }); @@ -105,16 +107,11 @@ for (const backend of ["llvm"] as const) { const request = join(directory, "request.json"); writeFileSync(request, JSON.stringify(input())); const expected = join(directory, "node.json"); - const node = spawnSync(process.execPath, ["--import", "tsx", oracle, ts7Executable(), request, expected], { encoding: "utf8", timeout: 45_000 }); - expect(node.error, node.stderr).toBeUndefined(); - expect(node.status, node.stderr).toBe(0); + const node = await execFileAsync(process.execPath, ["--import", "tsx", oracle, ts7Executable(), request, expected], { encoding: "utf8", timeout: 45_000 }); expect(node.stdout).toBe(""); expect(node.stderr).toBe(""); const report = join(directory, "native.json"); - const run = spawnSync(built.binaryPath, [ts7Executable(), request, report], { encoding: "utf8", timeout: 45_000 }); - expect(run.error, run.stderr).toBeUndefined(); - expect(run.signal, run.stderr).toBeNull(); - expect(run.status, run.stderr).toBe(0); + const run = await execFileAsync(built.binaryPath, [ts7Executable(), request, report], { encoding: "utf8", timeout: 45_000 }); expect(run.stdout).toBe(node.stdout); expect(run.stderr).toBe(node.stderr); const result = JSON.parse(readFileSync(report, "utf8"));