Centralize builtin contracts and IR analysis ownership

- Derive builtin names from shared signatures and preserve their semantic documentation.
- Separate runtime selection, exception metadata, and library policy from core IR.
- Enforce shared-signature imports and preserve native and public contracts.
This commit is contained in:
Chris Tate
2026-10-05 18:10:38 -05:00
parent c08ab955ae
commit 77b6830da1
30 changed files with 5200 additions and 6414 deletions
+8
View File
@@ -80,6 +80,10 @@
"group": ["**/backend/*", "**/backend/**"],
"message": "frontend must not import backend; use the IR interface.",
},
{
"group": ["**/ir/validate", "**/ir/validate.*"],
"message": "frontend must not import IR validation; use the shared builtin and intrinsic signatures.",
},
{
"group": ["typescript5", "typescript5/**"],
"message": "typescript5 is restricted to the explicit parser islands; TypeScript AST worlds must not mix.",
@@ -130,6 +134,10 @@
"group": ["**/backend/*", "**/backend/**"],
"message": "frontend must not import backend; use the IR interface.",
},
{
"group": ["**/ir/validate", "**/ir/validate.*"],
"message": "frontend must not import IR validation; use the shared builtin and intrinsic signatures.",
},
],
},
],
@@ -1,10 +1,10 @@
/** Runtime requirements derived from the lowered program, shared by compiler hosts. */
import { type IrModule } from "../ir/ir.js";
import {
moduleEmbedsBuiltin,
moduleEmbedsCompressedNpm,
moduleRuntimeFeatures,
type IrModule,
} from "../ir/ir.js";
} from "../ir/runtime-features.js";
import { hasForeignFfiCallback } from "./ffi-callbacks.js";
import type { NativeLinkFeatures } from "./native-link-info.js";
@@ -43,10 +43,6 @@ import {
isFfiContextParam,
isRefCounted,
isUnitType,
moduleRuntimeFeatures,
moduleEmbedsBuiltin,
moduleEmbedsCompressedNpm,
NPM_COMPRESS_MIN,
POINTER_KINDS,
RUNTIME_EMITTER_CLASS,
RUNTIME_ERROR_CLASSES,
@@ -54,6 +50,12 @@ import {
typeKey,
VOID,
} from "../../ir/ir.js";
import {
moduleRuntimeFeatures,
moduleEmbedsBuiltin,
moduleEmbedsCompressedNpm,
NPM_COMPRESS_MIN,
} from "../../ir/runtime-features.js";
import {
matchIntegerArrayForLoop,
matchIntegerBytesForLoop,
@@ -1,6 +1,7 @@
/** Argument marshalling at the runtime C ABI boundary. */
import { InternalCompilerError } from "../../errors.js";
import { MAY_THROW_LIB_FNS, type IrType } from "../../ir/ir.js";
import { type IrType } from "../../ir/ir.js";
import { MAY_THROW_LIB_FNS } from "../../ir/builtin-effects.js";
import type { LibCallExpr, LlValue, LlvmEmitterContext } from "./expr-context.js";
import { vAdapters } from "./shapes.js";
@@ -6,7 +6,8 @@ import { emitDatagramLibCall } from "./lib-datagram.js";
import { emitTestLibCall } from "./lib-test.js";
/* Focused LLVM library-call emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { MAY_THROW_LIB_FNS } from "../../ir/ir.js";
import { MAY_THROW_LIB_FNS } from "../../ir/builtin-effects.js";
import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlvmEmitterContext, LibCallExpr, LibCallPrefix, LlValue } from "./expr-context.js";
import { LIB_FN_SYMS, USES_TIMERS_LIB_FNS } from "./lib-shared.js";
+2 -6
View File
@@ -1,12 +1,8 @@
/* Cheap whole-module may-throw analysis (see computeMayThrow). Pure function
* of the IR module; the emitter consults the result to place unwind checks. */
import type { IrExpr, IrStmt, IrModule } from "../ir/ir.js";
import {
isFfiCallbackParam,
MAY_THROW_ARR_METHODS,
MAY_THROW_BYTES_METHODS,
MAY_THROW_LIB_FNS,
} from "../ir/ir.js";
import { isFfiCallbackParam, MAY_THROW_ARR_METHODS, MAY_THROW_BYTES_METHODS } from "../ir/ir.js";
import { MAY_THROW_LIB_FNS } from "../ir/builtin-effects.js";
import { everyStmtList } from "../ir/traverse.js";
import { hasRetainedFfiCallback } from "./ffi-callbacks.js";
@@ -1,7 +1,8 @@
import { everyModuleNode, everyStmtList, transformStmtList } from "../ir/traverse.js";
import type { LlvmUnsupportedError } from "./llvm/emitter.js";
import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
import { moduleUsesFetch, moduleEmbedsBuiltin, type IrModule, type SrcLoc } from "../ir/ir.js";
import { type IrModule, type SrcLoc } from "../ir/ir.js";
import { moduleUsesFetch, moduleEmbedsBuiltin } from "../ir/runtime-features.js";
/** Reflection tables expose methods that the program never calls. On WASI,
* keep their host-only implementations as explicit runtime refusals. Typed
@@ -12,7 +12,7 @@
* This pass runs once per lowered statement (lowerStmts calls it inside the
* per-statement poison window) and re-checks every libCall / strIntrinsic /
* regexIntrinsic / arrIntrinsic / callValue argument against the SAME
* signature tables the validator enforces (exported from ir/validate.ts —
* signature tables the validator enforces (owned by the IR contract —
* one table, two consumers, no drift):
*
* - a dyn argument whose slot dynCheck can validate (JSON-safe types,
@@ -59,7 +59,8 @@ import {
isUnitType,
typeEquals,
} from "../../ir/ir.js";
import { LIB_FN_SIGS, REGEX_INTRINSIC_SIGS, STR_INTRINSIC_SIGS } from "../../ir/validate.js";
import { LIB_FN_SIGS } from "../../ir/builtin-signatures.js";
import { REGEX_INTRINSIC_SIGS, STR_INTRINSIC_SIGS } from "../../ir/intrinsic-signatures.js";
import { unionMismatchDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js";
/** What a dynCheck can validate — coerceToExpected's dyn→typed domain,
@@ -1735,7 +1735,7 @@ export const BUILTIN_MODULE_FN_ALIASES: Record<
/** Ambient surfaces lowered through DEDICATED code paths (no lowering-table
* row): the Date compositions, perf_hooks' performance.now, and the process
* global's ambient reads and authority calls. These are the determinism
* attestation's ground (ir/ir.ts's LIB_NONDETERMINISTIC_PREFIXES), so
* attestation's ground (library/ir-policy.ts's LIB_NONDETERMINISTIC_PREFIXES), so
* each row projects one surface-manifest entry — a permanent, fenceable id
* — and carries the libCall spellings that witness the surface's reach in
* a compiled graph (the fence detector and the attestation must agree; the
+5 -3
View File
@@ -92,7 +92,8 @@ import {
libraryIdentityHashes,
updateSidecarIdentity,
} from "./library/sidecar.js";
import { moduleRuntimeFeatures, type IrModule, type SrcLoc } from "./ir/ir.js";
import { type IrModule, type SrcLoc } from "./ir/ir.js";
import { moduleRuntimeFeatures } from "./ir/runtime-features.js";
import { serializeModule } from "./ir/serialize.js";
import { validateModule } from "./ir/validate.js";
import { loadProgram } from "./frontend/program-node.js";
@@ -179,7 +180,8 @@ export {
type FetchCompatOption,
type FetchCompatProfile,
} from "./compat/fetch-profile.js";
export { LIB_FN_SIGS, validateModule } from "./ir/validate.js";
export { LIB_FN_SIGS } from "./ir/builtin-signatures.js";
export { validateModule } from "./ir/validate.js";
export { deserializeModule, IR_VERSION, serializeModule } from "./ir/serialize.js";
export {
resolveLibraryFences,
@@ -256,7 +258,7 @@ export {
type ProvenancePackageSource,
type ProvenanceSources,
} from "./frontend/provenance-registry.js";
export * as ir from "./ir/ir.js";
export * as ir from "./ir/api.js";
/** Clang may print every warning from the generated/runtime translation
* units before the actionable linker failure. Keep the source diagnostic
+43
View File
@@ -0,0 +1,43 @@
/* Public IR facade. Compiler passes import their implementation owner directly. */
export * from "./ir.js";
export { MAY_THROW_LIB_FNS } from "./builtin-effects.js";
export {
type RuntimeFeatures,
moduleRuntimeFeatures,
moduleUsesRegex,
moduleUsesCopying,
moduleUsesLegacyTextDecoder,
moduleUsesFileHandle,
moduleUsesFetch,
moduleUsesProcessEvents,
moduleUsesEmitter,
moduleUsesStream,
moduleEmbedsBuiltin,
NPM_COMPRESS_MIN,
moduleEmbedsCompressedNpm,
moduleUsesZlib,
moduleUsesDc,
moduleUsesAssert,
moduleUsesDynInvoke,
moduleUsesDynAsync,
moduleUsesInspect,
moduleUsesChildProcess,
moduleUsesNet,
moduleUsesSymbol,
moduleUsesBigInt,
moduleUsesSearchParams,
moduleUsesQs,
moduleUsesParseArgs,
moduleUsesFsWatch,
moduleUsesNodeTest,
moduleUsesDgram,
moduleUsesHttpServer,
moduleUsesHttp2,
moduleUsesTls,
moduleUsesTlsCa,
} from "./runtime-features.js";
export {
LIB_NONDETERMINISTIC_PREFIXES,
moduleLibAsyncSurface,
moduleLibNondeterministicSurface,
} from "../library/ir-policy.js";
+632
View File
@@ -0,0 +1,632 @@
import type { IrLibFn } from "./builtin-signatures.js";
/** The may-throw seed: libCall members that can raise. Every fs.* member
* EXCEPT existsSync (which, like Node's, swallows errors and returns false)
* throws a catchable error on failure; json.parse throws a catchable
* SyntaxError-shaped string on malformed input. Backends' may-throw analyses must treat a function containing one
* of these as throwing, exactly like a `throw` statement (and must ALSO
* seed on `dynCheck` and `awaitExpr` nodes, which throw on validation
* failure / promise rejection). */
export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"text.encodeInto",
"buffer.isAscii",
"buffer.isUtf8",
"buffer.transcode",
"cp.spawnDynamic",
"child.onDyn",
"text.decoderEncoding",
"dyn.construct",
"dyn.reflectApply",
"dyn.reflectGet",
"dyn.reflectSet",
"dyn.reflectDefine",
"dyn.abstractEq",
"text.decodeOptions",
"text.decodeStream",
"text.decodeLegacyOptions",
"dyn.nativeSetNew",
"ffi.argument",
"ffi.memoryModule",
"process.stdoutWrite",
"process.stderrWrite",
"process.stdoutWriteBytes",
"process.stderrWriteBytes",
"procStream.write",
"weakMap.new",
"weakSet.new",
"dyn.fromEntries",
"bytes.constructor",
"bytes.instanceOf",
"buffer.byteLenDyn",
"bytes.construct",
"arrayBuffer.new",
"arrayBuffer.byteLengthGetter",
"arrayBuffer.viewU8C",
"arrayBuffer.viewI8",
"arrayBuffer.viewU16",
"arrayBuffer.viewI16",
"arrayBuffer.viewU8",
"arrayBuffer.viewU32",
"arrayBuffer.viewI32",
"arrayBuffer.viewF32",
"arrayBuffer.viewF64",
"arrayBuffer.viewDV",
"bytes.bufferSource",
"text.decodeBufferSource",
"bigint.parse",
"bigint.fromF64",
"bigint.div",
"bigint.mod",
"bigint.pow",
"bigint.shl",
"bigint.shr",
"bigint.toString",
"bigint.asUintN",
"bigint.asIntN",
"bigint.bufferRead",
"bigint.bufferWrite",
"bigint.dataViewGet",
"bigint.dataViewSet",
"fetch.requestNew",
"fetch.headersNew",
"fetch.responseNew",
"fetch.abortTimeout",
"fetch.abortAny",
"abort.timeout",
"abort.any",
"fetch.streamNew",
"fetch.streamFrom",
"num.toFixed",
"num.toStringRadix",
"insp.jsonDyn",
// diagnostics_channel: publish runs subscribers synchronously (a throw
// propagates — the documented divergence from triggerUncaughtException);
// the subscribe/unsubscribe forms throw Node's ERR_INVALID_ARG_TYPE for
// non-function subscribers.
"dc.publish",
"dc.subscribe",
"dc.unsubscribe",
"dc.chanSubscribe",
"dc.chanUnsubscribe",
"dc.tcSubscribe",
"dc.tcUnsubscribe",
"dc.tcTraceSync",
"dc.tcTraceCallback",
"dc.tcTracePromise",
"process.onUncaughtException",
"process.offUncaughtException",
"process.onSignal",
"process.offSignal",
"process.onUnhandledRejection",
"process.onRejectionHandled",
"async.awaitDyn",
"als.run",
"als.exitRun",
"dc.chanBindStore",
"dc.chanRunStores",
"http.resWriteHeadDyn",
"http.resWriteContinue",
"http.resWriteProcessing",
"http.resWriteEarlyHints",
"http.validateHeaderName",
"http.validateHeaderValue",
"http.serverTimeoutGet",
"http.serverTimeoutOptionSet",
"net.sockSetEncoding",
"http.reqSetEncoding",
"fs.readFileSyncBuf",
"fs.readFileSyncDyn",
// tls/https runtime options records: the walks throw the catchable
// runtime fence for out-of-bounds members and non-PEM values (the
// divergence-66 stance).
"tls.pemDyn",
"tls.createServerDyn",
"tls.createSecureContextDyn",
"tls.caCertsChk",
"tls.createServerDynCb",
"https.createServerDyn",
"https.createServerDynCb",
"http2.createSecureServerDyn",
"http2.createSecureServerDynCb",
"tls.connect",
"tls.connectCb",
// The CA-store surface: an unknown getCACertificates type throws
// Node's ERR_INVALID_ARG_VALUE TypeError; setDefaultCACertificates
// throws Node's ERR_CRYPTO_OPERATION_FAILED on a certificate-free
// array.
"tlsca.get",
"tlsca.set",
// EventEmitter dispatch runs user listeners synchronously (emit), and
// the mutation forms fire the newListener/removeListener meta events —
// any of them can throw. emitError additionally THROWS its payload when
// 'error' has no listener, Node's unhandled-'error' contract.
"emitter.emit",
"emitter.emitFlex",
"emitter.emitError",
"emitter.on",
"emitter.off",
"emitter.checkListener",
"emitter.onDyn",
"emitter.onFlex",
"emitter.offDyn",
"emitter.onData",
"emitter.onDataDyn",
"emitter.emitData",
"readable.nextChunk",
"readable.nextChunkDyn",
// The checked-dynamic chunk forms throw Node's ERR_INVALID_ARG_TYPE
// chunk TypeError on a non-string/Buffer dyn kind.
"net.sockWriteDyn",
"net.sockEndDyn",
"http.resWriteDyn",
"http.resEndDyn",
"http.resAddTrailers",
"http.resFlushHeaders",
"http.resUncork",
"http.resWriteHead",
"http.resWriteHeadN",
"http.resWriteHeadPairs",
"http.resWrite",
"http.resWriteBytes",
"http.resEnd",
"http.resEndStr",
"http.resEndBytes",
"http.clientWriteDyn",
"http.clientEndDyn",
"http.clientAddTrailers",
"http.clientFlushHeaders",
"http.clientUncork",
"http.clientWrite",
"http.clientWriteBytes",
"http.clientEnd",
"http.clientEndStr",
"http.clientEndBytes",
// The URL-string client form throws catchably on an unparsable input
// ("Invalid URL") or a non-http scheme (ERR_INVALID_PROTOCOL).
"http.requestUrl",
"http.requestUrlCb",
// The h2 client's throwing entries: connect on a bad/non-http
// authority (ERR_INVALID_URL / ERR_INVALID_PROTOCOL), request on a
// closed/destroyed session (ERR_HTTP2_INVALID_SESSION), setEncoding on
// an unknown name (ERR_UNKNOWN_ENCODING). Event fires run user code
// synchronously (the stream-operations stance below), so the data/
// write paths that can fire inline join too.
"http2.connect",
"http2.connectCb",
"http2.sessionRequest",
"http2.streamSetEncoding",
"http2.streamRespond",
"http2.streamWrite",
"http2.streamWriteBytes",
"http2.streamEnd",
"http2.streamEndStr",
"http2.streamEndBytes",
"emitter.removeAll",
// Stream operations run user code synchronously (option callbacks and
// 'data'/'drain'/... listeners on the caller's stack, like emit) — any
// of them may leave a listener's throw pending. destroy with no 'error'
// listener throws the payload itself (the emitter.emitError contract).
// The dyn-options constructors walk the record at runtime: a consumed-
// but-unlowered option (objectMode, construct, ...) throws the compile
// fence's runtime twin; a bad encoding throws ERR_UNKNOWN_ENCODING.
"stream.finished",
"stream.finishedDyn",
"stream.pipeline",
"stream.pipelineDyn",
"sp.finished",
"sp.pipeline",
"sc.text",
"sc.json",
"sc.buffer",
"readable.newDyn",
"writable.newDyn",
"duplex.newDyn",
"transform.newDyn",
"passthrough.newDyn",
"readable.initDyn",
"writable.initDyn",
"duplex.initDyn",
"transform.initDyn",
"passthrough.initDyn",
"readable.push",
"readable.pushStr",
"readable.pushStrEnc",
"readable.pushNull",
"readable.pushU",
"readable.pushDyn",
"readable.unshift",
"readable.unshiftStr",
"readable.read",
"readable.readDyn",
"stream.onDyn",
"readable.resume",
"readable.pipe",
"readable.unpipe",
"writable.write",
"writable.writeStr",
"writable.writeU",
"writable.writeDyn",
"writable.end",
"writable.uncork",
"stream.destroy",
"stream.destroyErr",
"stream.iteratorClose",
// setMaxListeners(n) throws Node's ERR_OUT_OF_RANGE RangeError for
// negative/NaN arguments; the static form's default-max write validates
// identically (Node's validateNumber(n, "setMaxListeners", 0)).
"emitter.setMax",
"emitter.setDefaultMax",
// The createConnection request forms run the caller's dialer closure
// synchronously — a throw there propagates like Node's.
"http.requestConn",
"http.requestConnCb",
"process.kill",
"process.killNum",
"process.setExitCode",
// cpuUsage(prev)'s field validation: negative/non-finite prev fields
// throw Node's ERR_INVALID_ARG_VALUE RangeError, catchably.
"process.cpuPrevValidate",
// Non-TTY stdin: Node's stdin has no setRawMode member, so the call is
// the exact catchable TypeError.
"process.stdinSetRawMode",
// The unguarded h2-only stream call: always throws Node's TypeError.
"http2.streamUndefCall",
"http.reqH2StreamOrThrow",
// A read of a declare-d const nothing defines: always throws Node's
// catchable ReferenceError.
"global.undefRead",
// The compiler-resolved Node-parity throw: always throws, catchably.
"error.nodeThrow",
// USVString coercion runs user toString/valueOf — throws propagate.
"dyn.toStringCoerce",
"dyn.stringConstructor",
"dyn.propertyKey",
"dyn.toNumeric",
"dyn.increment",
// Error messages use the same coercion protocol before installing cause.
"error.newOptions",
"error.ctorOptions",
"error.toString",
"error.stack",
"error.cause",
"error.setCause",
"error.deleteCause",
"dyn.objectTag",
// Numeric coercion runs user valueOf/toString — throws propagate.
"dyn.numberConstructor",
"dyn.bigintConstructor",
"dyn.toNumberCoerce",
"dyn.add",
"dyn.arithmetic",
"dyn.compare",
"dyn.bitwise",
"dyn.proxyNew",
"dyn.classBasePrototype",
"dyn.classInherit",
"dyn.classSuper",
"dyn.hasKey",
"child.kill",
// The caller's lookup runs synchronously inside the connect call — a
// throw there propagates like Node's.
"net.connectLookup",
"cp.execSync",
"cp.execCapture",
// The client request constructors validate the method token
// synchronously (Node's ClientRequest ctor throw:
// ERR_INVALID_HTTP_TOKEN).
"http.request",
"http.requestCb",
"http.requestConn",
"http.requestConnCb",
// The agent rows add the runtime agent-value gates (a non-Agent value
// is Node's ERR_INVALID_ARG_TYPE); agentNew's keepAlive: true throws
// the named pooling fence.
"http.agentNew",
"http.requestAgent",
"http.requestAgentCb",
"https.requestAgent",
"https.requestAgentCb",
"https.request",
"https.requestCb",
// The URL-string form's parse, exactly http.requestUrl's (an
// unparsable input, or a scheme that is not https).
"https.requestUrl",
"https.requestUrlCb",
"https.requestFn",
"https.requestFnCb",
"rl.question",
"island.eval",
"island.import",
"island.castFail",
"json.parse",
"json.parseReviver",
"json.stringifyReplacer",
"json.stringifyValue",
"date.nativeNew",
"date.checkedValue",
"util.parseArgs",
"util.parseEnv",
"util.getSystemErrorName",
"util.getSystemErrorMessage",
"util.stripVTControlCharacters",
"util.isDeepStrictEqual",
"util.styleText",
"util.toUSVString",
// decodeURIComponent throws the spec's URIError on bad hex/invalid
// UTF-8 octets (encodeURIComponent never throws — see the IrLibFn doc).
"str.decodeUriComponent",
"str.decodeUri",
// The base64 globals: atob/btoa throw the catchable DOMException
// InvalidCharacterError on malformed input; the zero-argument form
// always throws Node's TypeError [ERR_MISSING_ARGS].
"str.atob",
"str.btoa",
"str.b64Missing",
// queueMicrotask's checked-dynamic form throws ERR_INVALID_ARG_TYPE
// synchronously on a non-function argument (the closure form never
// throws — the callback's own throw surfaces at drain, uncaught).
"timers.queueMicrotaskDyn",
// structuredClone: option validation throws Node's TypeErrors,
// functions/handles the spec's DataCloneError, cycles the scriptc
// fence; the zero-argument form always throws ERR_MISSING_ARGS. The
// DOMException clone shares the option validation.
"dyn.structuredClone",
"dyn.cloneMissing",
"dyn.cloneTransferFail",
// the dyn Object walks throw on null/undefined receivers
"dyn.objKeys",
"dyn.forInKeys",
"dyn.hasOwn",
"dyn.propertyIsEnumerable",
"dyn.assign",
"dyn.copyDataProperties",
"dyn.objectRest",
// variadic Object.assign: spread flattening throws V8's spread-call
// TypeErrors; the final copy throws ToObject on a nullish target
"dyn.packPushSpread",
"dyn.packPushSpreadIter",
"dyn.assignAll",
"dyn.objCreate",
"dyn.objCreateWithProperties",
"dyn.getPrototype",
"dyn.setPrototype",
"dyn.getOwnPropertyNames",
"dyn.ownKeys",
"dyn.getOwnPropertySymbols",
"dyn.getOwnPropertyDescriptors",
"dyn.preventExtensions",
"dyn.isExtensible",
"dyn.seal",
"dyn.isSealed",
"dyn.objValues",
"dyn.objEntries",
"error.domClone",
// new RegExp compiles the pattern eagerly: an invalid pattern or flag
// throws Node's catchable SyntaxError at construction.
"regex.new",
"regex.resetLastIndex",
"regex.newChecked",
"dyn.keySet",
"dyn.keySetComputed",
"process.builtinId",
"process.builtinModule",
"process.builtinUnsupported",
"fs.callbackValue",
"fs.callbackCall",
"dyn.keyDelete",
"dyn.keyDeleteComputed",
"dyn.hasKeyComputed",
"dyn.hasOwnComputed",
"dyn.propertyIsEnumerableComputed",
// the destructuring pack throws V8's TypeError on non-iterable dyn kinds
"dyn.iterPack",
"dyn.arrayFromIterator",
"dyn.iterator",
"dyn.iteratorResult",
"dyn.iteratorStep",
"dyn.mapSeedEntries",
"dyn.mapSeedEntry",
"dyn.freeze",
"dyn.isFrozen",
"dyn.toString",
"string.fromCodePoint",
"dyn.defineProps",
"dyn.definePrototypeProps",
"dyn.bagGet",
"dyn.bagSet",
"dyn.defineProperty",
"dyn.getOwnPropertyDescriptor",
"dyn.arrayProtoCall",
"process.chdir",
"process.loadEnvFile",
"fs.realpathSync",
"fs.realpathNativeSync",
"fs.readFileSync",
"fs.writeFileSync",
"fs.appendFileSync",
"fs.mkdirSync",
"fs.rmSync",
"fs.rmdirSync",
"fs.readdirSync",
"fs.readdirTypesSync",
"url.new",
"url.newBase",
"url.set",
"url.setChecked",
"url.fileURLToPathUrl",
"url.fileURLToPathChecked",
"url.fileURLToPathOptions",
"url.fileURLToPathBuffer",
"url.pathToFileURLChecked",
"url.fileURLToPathStr",
// The win32-target flavor of pathToFileURL (same runtime entry point —
// the bridge dispatches by the binary's platform): Node's win32 arm
// raises ERR_INVALID_ARG_VALUE TypeErrors for malformed UNC inputs, so
// only THIS flavor seeds may-throw — posix emission stays untouched.
"url.pathToFileURLWin32",
"url.pathToFileURLPlatform",
// URLSearchParams from a string[][]: Node's ERR_INVALID_TUPLE TypeError
// on a row that is not a [name, value] pair. The rest of the sp family
// never throws.
"sp.fromPairs",
"sp.newChecked",
"fs.statSync",
"crypto.randomBytesToString",
"crypto.randomBytes",
"crypto.hashDigestStr",
"crypto.hashDigestBytes",
"crypto.hashNew",
"crypto.hmacNewStr",
"crypto.hmacNewBytes",
"crypto.hashUpdateStr",
"crypto.hashUpdateBytes",
"crypto.hmacUpdateStr",
"crypto.hmacUpdateBytes",
"crypto.hashCopy",
"crypto.hashDigestString",
"crypto.hashDigestBuffer",
"crypto.hmacDigestString",
"crypto.hmacDigestBuffer",
"crypto.timingSafeEqual",
"crypto.randomFill",
"crypto.randomFillRest",
"crypto.randomInt",
"crypto.pbkdf2",
"crypto.randomBytesCb",
"crypto.pbkdf2Cb",
"crypto.hkdf",
"crypto.hkdfCb",
"crypto.scrypt",
"crypto.scryptCb",
"buffer.concat",
"buffer.concatLen",
"buffer.fromDyn",
// The checked-dynamic compare/equals validators: Node's argument
// ladders throw ERR_INVALID_ARG_TYPE / ERR_OUT_OF_RANGE catchably.
"buffer.compareChk",
"bytes.equalsChk",
"bytes.compareChk",
"buffer.newStringFail",
"fs.toUnixTimestamp",
"fs.existsChk",
"fs.mkdtempChk",
"fs.mkdtempSyncChk",
"fs.readFileChk",
"fs.opendirChk",
"fs.watchFileChk",
"fs.lchmodChk",
"fs.lchmodSyncChk",
"fsp.lchmodChk",
"fs.readChk",
"fs.streamOptsChk",
"net.connectAttempt",
"net.connectOptsChk",
"net.setAutoSelTimeout",
"error.argTypeThrow",
"error.propTypeThrow",
"emitter.setMaxChk",
"emitter.setDefaultMaxChk",
"fs.readFileSyncBytes",
"fs.writeFileSyncBytes",
"fs.appendFileSyncBytes",
"zlib.inflateSync",
"zlib.inflateRawSync",
"zlib.gunzipSync",
"zlib.unzipSync",
"zlib.crc32",
"date.toISOString",
"date.toISOStringValue",
"fs.mkdirRecursiveSync",
"fs.rmOptsSync",
"fs.rmRetrySync",
"fs.mkdtempSync",
"fs.accessSync",
"fs.readFdSync",
"fs.readFdSyncBytes",
// node:dgram state errors (Node throws synchronously): bind on a bound
// socket, connect on a connected one, send/close/address on a closed or
// never-bound one.
"dgram.bind",
"dgram.bindCb",
"dgram.connect",
"dgram.connectCb",
"dgram.sendStr",
"dgram.sendBytes",
"dgram.sendChk",
"dgram.address",
"dgram.close",
"dgram.closeCb",
// The assert surface: every entry point except sameValue, bytesDeepEq,
// and the shape accumulator's begin/slot/test calls throws the
// catchable AssertionError on failure.
"assert.ok",
"assert.eqF64",
"assert.eqBigInt",
"assert.eqStr",
"assert.eqBool",
"assert.eqSym",
"assert.eqDyn",
"assert.looseResult",
"assert.deepResult",
"assert.match",
"assert.refEqBytes",
"assert.refEqFn",
"assert.throwsNone",
"assert.throwsMismatch",
"assert.throwsRegex",
"assert.shapeEnd",
"assert.unwantedRejection",
"assert.unwantedError",
"assert.noErrorPredicate",
"assert.regexDynTest",
"assert.expectsErrDyn",
"assert.ifErrorErr",
"assert.ifErrorF64",
"assert.ifErrorStr",
"assert.ifErrorBool",
"assert.ifErrorDyn",
// util.inspect of an island `any` composite: the runtime tag is all
// there is, so the honest answer is a catchable TypeError.
"insp.jsval",
// The wider sync fs slice: same catchable errno throws as the rest.
"fs.unlinkSync",
"fs.chmodSync",
"fs.chownSync",
"fs.copyFileSync",
"fs.renameSync",
"fs.lstatSync",
"fs.fstatSync",
"fs.fchmodSync",
"fs.fsyncSync",
"fs.fdatasyncSync",
"fs.ftruncateSync",
"fs.utimesSync",
"fs.futimesSync",
"fs.lutimesSync",
"fs.readvSync",
"fs.writevSync",
"fs.linkSync",
"fs.symlinkSync",
"fs.readlinkSync",
"fs.statfsSync",
"fs.readlinkSyncStr",
"fs.readlinkSyncBuffer",
"fs.writeFileModeSync",
"fs.mkdirModeSync",
"fs.mkdirRecursiveModeSync",
"fs.openSync",
"fs.openNumericSync",
"fs.readSync",
"fs.writeSync",
"fs.writeStrSync",
"fs.closeSync",
"fs.watch",
"fs.watchCb",
"crypto.x509Fingerprint",
"crypto.x509FingerprintStr",
"crypto.x509ValidFrom",
"crypto.x509ValidFromStr",
"crypto.x509ValidTo",
"crypto.x509ValidToStr",
]);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,65 @@
import type { IrRegexIntrinsicMethod, IrStrIntrinsicMethod, IrType } from "./ir.js";
import { arrayOf, BOOL, F64, REGEX, STRING, VOID } from "./ir.js";
/** Per-method signature for strIntrinsic: `argTypes` lists every argument
* position (optional ones included); `minArgs` is the minimum number of required arguments
* before optional trailing positions (backends fill the documented defaults). Shared with the
* frontend's lib-boundary pass (lib-boundary.ts), which coerces or fences
* checked-dynamic arguments against the same table the validator enforces. */
export const STR_INTRINSIC_SIGS: Record<
IrStrIntrinsicMethod,
{ argTypes: IrType[]; minArgs: number; result: IrType }
> = {
length: { argTypes: [], minArgs: 0, result: F64 },
charCodeAt: { argTypes: [F64], minArgs: 1, result: F64 },
charAt: { argTypes: [F64], minArgs: 1, result: STRING },
indexOf: { argTypes: [STRING, F64], minArgs: 1, result: F64 },
includes: { argTypes: [STRING, F64], minArgs: 1, result: BOOL },
startsWith: { argTypes: [STRING, F64], minArgs: 1, result: BOOL },
endsWith: { argTypes: [STRING, F64], minArgs: 1, result: BOOL },
slice: { argTypes: [F64, F64], minArgs: 0, result: STRING },
substring: { argTypes: [F64, F64], minArgs: 1, result: STRING },
repeat: { argTypes: [F64], minArgs: 1, result: STRING },
trim: { argTypes: [], minArgs: 0, result: STRING },
trimStart: { argTypes: [], minArgs: 0, result: STRING },
trimEnd: { argTypes: [], minArgs: 0, result: STRING },
split: { argTypes: [STRING, F64], minArgs: 2, result: arrayOf(STRING) },
padStart: { argTypes: [F64, STRING], minArgs: 2, result: STRING },
padEnd: { argTypes: [F64, STRING], minArgs: 2, result: STRING },
toLowerCase: { argTypes: [], minArgs: 0, result: STRING },
toUpperCase: { argTypes: [], minArgs: 0, result: STRING },
normalize: { argTypes: [STRING], minArgs: 1, result: STRING },
isWellFormed: { argTypes: [], minArgs: 0, result: BOOL },
toWellFormed: { argTypes: [], minArgs: 0, result: STRING },
cpAt: { argTypes: [F64], minArgs: 1, result: STRING },
};
/** Per-method signature for regexIntrinsic. test/source/flags take the
* regex as receiver; replace/replaceAll/split take the STRING as receiver
* with the regex as args[0] (mirroring the source syntax). All positions
* are required — the surface has no optionals. */
export const REGEX_INTRINSIC_SIGS: Record<
IrRegexIntrinsicMethod,
{ receiver: IrType; argTypes: IrType[]; result: IrType }
> = {
test: { receiver: REGEX, argTypes: [STRING], result: BOOL },
// match's result is the program-dependent `string[] | null` union —
// VOID here is the process.envGet sentinel; the regexIntrinsic case
// checks the union's arms.
match: { receiver: STRING, argTypes: [REGEX], result: VOID },
exec: { receiver: STRING, argTypes: [REGEX], result: VOID },
lastIndex: { receiver: REGEX, argTypes: [], result: F64 },
matchAll: { receiver: STRING, argTypes: [REGEX], result: arrayOf(arrayOf(STRING)) },
matchAllInto: {
receiver: STRING,
argTypes: [REGEX, arrayOf(F64)],
result: arrayOf(arrayOf(STRING)),
},
search: { receiver: STRING, argTypes: [REGEX], result: F64 },
source: { receiver: REGEX, argTypes: [], result: STRING },
flags: { receiver: REGEX, argTypes: [], result: STRING },
toString: { receiver: REGEX, argTypes: [], result: STRING },
replace: { receiver: STRING, argTypes: [REGEX, STRING], result: STRING },
replaceAll: { receiver: STRING, argTypes: [REGEX, STRING], result: STRING },
split: { receiver: STRING, argTypes: [REGEX, F64], result: arrayOf(STRING) },
};
+1 -107
View File
@@ -1,13 +1,5 @@
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,
@@ -307,101 +299,3 @@ describe("checked conversion graph traversal", () => {
).toBe(false);
});
});
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);
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,102 @@
import { describe, expect, test } from "vitest";
import { IR_VERSION } from "./serialize.js";
import { DYN, STRING, VOID, arrayOf, type IrModule, type IrLibFn } from "./ir.js";
import { moduleRuntimeFeatures, moduleUsesHttp2, moduleUsesTlsCa } from "./runtime-features.js";
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);
});
});
@@ -0,0 +1,620 @@
import type { IrExpr, IrModule, IrType } from "./ir.js";
import type { IrLibFn } from "./builtin-signatures.js";
import { RUNTIME_EMITTER_CLASS, RUNTIME_STREAM_CLASSES } from "./ir.js";
import { everyModuleNode } from "./traverse.js";
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<IrLibFn> = new Set([
"fs.callbackValue",
"fs.callbackCall",
"async.awaitDyn",
"timers.immediatePromise",
"crypto.native",
"dyn.promiseAll",
"process.onUncaughtException",
"process.offUncaughtException",
"process.onUnhandledRejection",
"process.offUnhandledRejection",
"process.onRejectionHandled",
"process.offRejectionHandled",
"process.onWarning",
"process.offWarning",
"process.emitWarning",
"util.styleText",
"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: mod.functions.some(
(fn) => fn.async === true && fn.generator === undefined && fn.returnType.kind === "dyn",
),
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 keepGoing = (): boolean => stopAt === undefined || !features[stopAt];
const expr = (node: IrExpr): boolean => {
if (node.kind === "libCall") {
const fn = node.fn;
if (
fn === "regexp.escape" ||
fn === "dyn.nativeRegexIs" ||
fn === "util.stripVTControlCharacters"
)
features.regex = true;
if (
fn === "text.decodeLegacy" ||
fn === "text.decodeLegacyOptions" ||
fn === "text.decodeStream"
)
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.definePrototypeProps" ||
fn === "dyn.defineProperty" ||
fn === "dyn.objCreateWithProperties" ||
fn === "dyn.arrayProtoCall" ||
fn === "dyn.arrayPrototype" ||
fn === "dyn.functionApply" ||
fn === "dyn.builtinMethod"
)
features.dynInvoke = true;
if (DYN_ASYNC_LIB_FNS.has(fn)) features.dynAsync = true;
if (
fn.startsWith("insp.") ||
fn === "console.native" ||
fn === "global.native" ||
fn === "util.styleText"
)
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.") || fn === "util.isDeepStrictEqual") features.symbol = true;
if (fn === "process.hrtimeValue") features.bigint = 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" || fn === "util.isDeepStrictEqual" || fn === "util.styleText")
features.parseArgs = true;
if (fn === "util.styleText") features.processEvents = 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 (node.kind) {
case "regexLit":
case "regexIntrinsic":
features.regex = true;
break;
case "strIntrinsic":
if (
node.method === "toLowerCase" ||
node.method === "toUpperCase" ||
node.method === "normalize"
)
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 "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;
}
}
}
return keepGoing();
};
const type = (node: IrType): boolean => {
switch (node.kind) {
case "regex":
features.regex = true;
break;
case "fileHandle":
features.fileHandle = true;
break;
case "procStream":
features.processEvents = 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;
}
return keepGoing();
};
if (keepGoing()) everyModuleNode(mod, { expr, stmt: keepGoing, type });
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. */
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 user code lowers static fetch or the embedded npm graph
* reaches the engine's global fetch — the link switch that pulls scr_fetch.c +
* its socket/tls/zlib dependencies into the binary (native-toolchain.ts) and has the
* emitted main call scr_fetch_install. The npm graph records the embedded
* source fact with a parser + binder, so comments, strings, property names,
* 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 {
return scanRuntimeFeatures(mod, "fetch").fetch;
}
/** 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.stdio",
"process.onSignal",
"process.offSignal",
"process.onExit",
"process.offExit",
"stdin.onData",
"stdin.onEnd",
"stdin.onError",
"stdin.nextChunk",
// node:readline rides the stdin unit (scr_readline.c links beside
// scr_events.c under the same gate).
"rl.create",
"rl.question",
"rl.close",
"rl.onClose",
]);
/** 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 embedded npm graph has an edge into `builtin` — the
* island shim needs the corresponding native bridge linked (zlib's is
* the first; the emitted main installs it before any island entry). */
export function moduleEmbedsBuiltin(mod: IrModule, builtin: string): boolean {
return mod.embedded !== undefined && mod.embedded.edges.some((e) => e.to === builtin);
}
/** Embedded module texts at least this long are DEFLATE-compressed into
* the emitted LLVM (island.ts; each stays plain when deflate does not
* shrink it) and inflated lazily by the island's module loader at first
* load. Below it the zlib round trip cannot pay for itself. */
export const NPM_COMPRESS_MIN = 1024;
/** True when the emitted npm tables will carry compressed module text —
* the SAME candidate test island.ts compresses by, so index.ts's
* zlib link switch and emitter.ts's inflater installation stay in
* lockstep with the emission (a candidate whose deflate happens not to
* shrink stays plain; the installed inflater is then just unused). */
export function moduleEmbedsCompressedNpm(mod: IrModule): boolean {
return (
mod.embedded !== undefined &&
mod.embedded.modules.some(
(m) => m.source.length >= NPM_COMPRESS_MIN || (m.esm ?? "").length >= NPM_COMPRESS_MIN,
)
);
}
/** True when the module contains any zlib libCall (the static lowering)
* OR the embedded npm graph imports node:zlib (the island shim) — the
* link switch that pulls scr_zlib.c + the system libz into the binary
* (native-toolchain.ts); zlib-free programs keep their exact link line. Same
* generic-walk shape as moduleUsesRegex: `kind`/`fn` discriminants live
* only on IR objects. */
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;
}
/** True when the module uses native util parsing, comparison, or styling.
* The utility units also follow their required symbol, inspection, and
* warning features, independently of the island's node:util shim. */
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 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;
}
/** 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",
]);
/** 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;
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -17,7 +17,7 @@
* - a fenced STATIC entry is policed against the COMPILED module graph:
* the emit pass lowers only reachability-discovered bodies, so the
* module IR *is* the reached graph — the same ground the sidecar's
* `deterministic` attestation stands on (ir/ir.ts's
* `deterministic` attestation stands on (library/ir-policy.ts's
* moduleLibNondeterministicSurface). Fence evaluation reuses that
* posture with one generic walk collecting the reached surface facts
* (libCall spellings, str/arr/map/set intrinsic methods); each fenced
+234
View File
@@ -0,0 +1,234 @@
import type { IrExpr, IrModule, IrStmt, SrcLoc } from "../ir/ir.js";
import { everyExprChild, everyModuleNode, everyStmtChild } from "../ir/traverse.js";
import { moduleRuntimeFeatures } from "../ir/runtime-features.js";
/* ── library mode's async_free gate ──────────────────────────────────────
* v1 library mode REQUIRES an async_free module graph (ratified): no async
* functions, no generators, no timers, no event-loop or ambient-process
* surface anywhere the entry reaches — a static fact of the graph, never a
* runtime observation. The detector below answers "what does this graph
* reach that a library artifact cannot link?", first offender with its source anchor;
* the structural consequence (scr_async.c / scr_child.c and every
* loop-hooked unit never join a library link) is safe exactly because this
* refusal ran first. */
/** libCall families a v1 library artifact refuses, with the surface name the SC4005
* teaching uses. Prefix match over IrLibFn spellings. */
const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [
// fs.exists is the one CALLBACK-async fs op with a real implementation:
// its fire rides the timer queue (scr_bytes_io.c), which library links
// exclude — refuse the surface like the rest of the event-loop family.
["fs.existsChk", "the async fs callback surface (fs.exists)"],
["fs.renameCb", "the async fs callback surface (fs.rename)"],
["fs.callback", "the async filesystem callback surface"],
["zlib.deflateCb", "the async node:zlib callback surface"],
["zlib.inflateCb", "the async node:zlib callback surface"],
["zlib.deflateRawCb", "the async node:zlib callback surface"],
["zlib.inflateRawCb", "the async node:zlib callback surface"],
["zlib.gzipCb", "the async node:zlib callback surface"],
["zlib.gunzipCb", "the async node:zlib callback surface"],
["zlib.unzipCb", "the async node:zlib callback surface"],
["process.stdoutWriteBytesCb", "process.stdout.write completion callbacks"],
["process.stderrWriteBytesCb", "process.stderr.write completion callbacks"],
["timers.", "the timers surface (setTimeout family)"],
["tp.", "the timers/promises surface"],
["cp.", "the child_process surface"],
["child.", "the child_process surface"],
["spawnRes.", "the child_process surface"],
["process.on", "process signal/exit listeners"],
["process.off", "process signal/exit listeners"],
["stdin.", "the stdin event surface"],
["rl.", "the node:readline surface"],
["net.", "the node:net surface"],
["http.", "the node:http surface"],
["https.", "the node:https surface"],
["http2.", "the node:http2 surface"],
["h2.", "the node:http2 surface"],
["dgram.", "the node:dgram surface"],
["dns.", "the node:dns surface"],
["tls.", "the node:tls surface"],
["fs.watch", "fs.watch"],
["watcher.", "fs.watch"],
["test.", "the node:test surface"],
["readable.", "the node:stream surface"],
["writable.", "the node:stream surface"],
["duplex.", "the node:stream surface"],
["transform.", "the node:stream surface"],
["passthrough.", "the node:stream surface"],
["stream.", "the node:stream surface"],
["als.", "AsyncLocalStorage"],
["urj.", "unhandled-rejection tracking"],
["dc.", "the diagnostics_channel surface"],
];
/** Value/type kinds whose mere presence means an excluded unit's code (or
* a fiber) would have to link. */
const LIB_MODE_REFUSED_KINDS: ReadonlyMap<string, string> = new Map([
["promise", "promise values"],
["generator", "generator values"],
["awaitExpr", "await"],
["awaitUnionExpr", "await"],
["yieldExpr", "yield"],
["child", "the child_process surface"],
["spawnRes", "the child_process surface"],
["childStream", "the child_process surface"],
["childWriter", "the child_process surface"],
["netServer", "the node:net surface"],
["netSocket", "the node:net surface"],
["http2Session", "the node:http2 surface"],
["http2Stream", "the node:http2 surface"],
["dgramSocket", "the node:dgram surface"],
["fsWatcher", "fs.watch"],
["testCtx", "the node:test surface"],
["httpReq", "the node:http surface"],
["httpRes", "the node:http surface"],
["httpClientReq", "the node:http surface"],
["secureCtx", "the node:tls surface"],
]);
/** First async/event-loop/ambient-process surface the module graph
* reaches, or null when the graph is async_free (the v1 library requirement).
* Typed slots use their declaration location and executable nodes use their
* own location, so the refusal anchors at the reaching construct; the
* coarse moduleUses* predicates are the safety net behind the fine-grained
* table (a surface reached only through a spelling the table misses still
* refuses, anchored at the entry). */
export function moduleLibAsyncSurface(mod: IrModule): { surface: string; loc: SrcLoc } | null {
for (const fn of mod.functions) {
if (fn.async === true)
return { surface: `an async function ('${fn.name.replace(/^%/, "")}')`, loc: fn.loc };
if (fn.generator !== undefined) {
return { surface: `a generator function ('${fn.name.replace(/^%/, "")}')`, loc: fn.loc };
}
}
const entryLoc: SrcLoc = { file: mod.sourceFile, start: 0, end: 0 };
let found: { surface: string; loc: SrcLoc } | null = null;
const checkKind = (kind: string, loc: SrcLoc): boolean => {
const surface = LIB_MODE_REFUSED_KINDS.get(kind);
if (surface === undefined) return true;
found = { surface, loc };
return false;
};
everyModuleNode(mod, {
type: (node, loc) => checkKind(node.kind, loc),
stmt: () => true,
expr: (node) => {
if (!checkKind(node.kind, node.loc)) return false;
if (node.kind === "libCall") {
for (const [prefix, surface] of LIB_MODE_REFUSED_PREFIXES) {
if (node.fn.startsWith(prefix)) {
found = { surface, loc: node.loc };
return false;
}
}
}
return true;
},
});
if (found !== null) return found;
// Safety net: the coarse unit predicates, entry-anchored. Every one of
// these units is excluded from library links, so a true answer that the
// fine-grained table missed must still refuse.
const features = moduleRuntimeFeatures(mod);
const coarse: [boolean, string][] = [
[features.processEvents, "process signal/exit listeners or the stdin event surface"],
[features.net, "the node:net surface"],
[features.http, "the node:http surface"],
[features.http2, "the node:http2 surface"],
[features.dgram, "the node:dgram surface"],
[features.fsWatch, "fs.watch"],
[features.stream, "the node:stream surface"],
[features.tls, "the node:tls surface"],
[features.fetch, "fetch"],
[features.nodeTest, "the node:test surface"],
[features.dynAsync, "the checked-dynamic async surface"],
[features.dc, "the diagnostics_channel surface"],
];
for (const [on, surface] of coarse) {
if (on) return { surface, loc: entryLoc };
}
return null;
}
/* ── the sidecar's determinism attestation ───────────────────────────────
* `deterministic` is true exactly when the compiled module graph reaches
* no ambient-nondeterminism or ambient-authority surface (random, live
* clock, environment, filesystem, machine identity) — a static fact of
* the graph, proven at compile time, never a runtime hope. Network,
* timers, scheduling, and child processes are already impossible here:
* the SC4005 async_free gate refused them before any library artifact
* emitted. The scan is CONSERVATIVE by design: an ambient family reached
* anywhere in the graph demotes the attestation even when a finer
* analysis might prove the specific call pure (e.g. date formatting of a
* stored value) — the attestation may honestly under-claim, never
* over-claim (schema rule V14: computed, never defaulted). */
/** libCall families that demote `deterministic` (prefix match over
* IrLibFn spellings). process.stdout/stderr writes are deliberately NOT
* here: output is an effect, not a nondeterminism input, and console
* policy is the profile's ask-5 business. process.platform/arch and the
* version constants fold at compile time, so they are per-binary
* constants, not ambient reads. Exported for the attestation-parity test
* (tests/harness/surface-manifest.test.ts): every spelling this table
* demotes on must be deniable by a manifest-id fence, or the ask-5 §4
* invariant (compiles under full fences ⇒ deterministic) cannot be
* stated. */
export const LIB_NONDETERMINISTIC_PREFIXES: readonly [string, string][] = [
["math.random", "Math.random"],
["crypto.random", "crypto randomness"],
["date.", "the live clock (Date)"],
["perf.", "the live clock (performance)"],
["process.env", "environment reads"],
["process.argv", "process.argv"],
["process.cwd", "process.cwd"],
["process.chdir", "process.chdir"],
["process.pid", "process identity"],
["process.getuid", "process identity"],
["process.getgid", "process identity"],
["process.execPath", "process identity"],
["process.uptime", "the live clock (process.uptime)"],
["process.availableMemory", "machine memory state"],
["process.constrainedMemory", "machine memory state"],
// process.memoryUsage carries NO row: nothing lowers it — no IrLibFn
// spelling exists for it, so a prefix here would be dead. If a lowering
// ever lands, its spellings must join this table AND the manifest's
// ambient projection (the parity test fails until both agree).
["process.rusage", "machine resource usage (process.resourceUsage)"],
["process.cpu", "the process CPU clock (process.cpuUsage)"],
["process.threadCpu", "the thread CPU clock (process.threadCpuUsage)"],
["process.isTTY", "terminal attachment (isTTY)"],
["process.columns", "terminal geometry (columns)"],
["process.rows", "terminal geometry (rows)"],
["process.kill", "process authority (kill)"],
["process.umask", "process authority (umask)"],
["process.exit", "process authority (exit)"],
["process.setExitCode", "process authority (exit status)"],
["fs.", "the filesystem"],
["os.", "machine/OS identity"],
// The CA-store surface reads the host's certificate bundle (and the
// NODE_EXTRA_CA_CERTS environment) — machine identity by another name.
["tlsca.", "the host CA store"],
];
/** First ambient-nondeterminism or ambient-authority surface the module
* graph reaches, or null when the graph is clean (the sidecar then
* attests `deterministic: true`). */
export function moduleLibNondeterministicSurface(mod: IrModule): string | null {
let found: string | null = null;
const expr = (node: IrExpr): boolean => {
if (node.kind === "libCall") {
for (const [prefix, surface] of LIB_NONDETERMINISTIC_PREFIXES) {
if (node.fn.startsWith(prefix)) {
found = surface;
return false;
}
}
}
return everyExprChild(node, expr, stmt);
};
const stmt = (node: IrStmt): boolean => everyStmtChild(node, expr, stmt);
for (const fn of mod.functions) {
if (!fn.body.every(stmt)) break;
}
return found;
}
+1 -2
View File
@@ -47,14 +47,13 @@ import type { ContractFacts } from "../frontend/lib-contract.js";
import type { LowerResult } from "../frontend/lowering/lowerer.js";
import type { FrontendFactory } from "../frontend/pipeline.js";
import {
moduleLibAsyncSurface,
moduleLibNondeterministicSurface,
type IrFfiImport,
type IrLibSection,
type IrModule,
type IrRecordShape,
type IrType,
} from "../ir/ir.js";
import { moduleLibAsyncSurface, moduleLibNondeterministicSurface } from "./ir-policy.js";
import { validateModule } from "../ir/validate.js";
/** The marshalling-class fit over IR types (design §4.2 + the ratified
+1 -1
View File
@@ -45,7 +45,7 @@ import { InternalCompilerError } from "../errors.js";
* reach that slot proved whole-in-range. Undeclared numeric slots keep
* spelling `f64` and the empty list stays a valid attestation that no
* slot was integer-classed. `deterministic` is computed from the module
* graph (ir/ir.ts's conservative ambient-surface scan), never
* graph (ir-policy.ts's conservative ambient-surface scan), never
* defaulted. */
import { Buffer } from "node:buffer";
import { readFileSync } from "node:fs";
+1 -1
View File
@@ -1,6 +1,6 @@
import { readFileSync } from "node:fs";
import { deserializeModule } from "../../../packages/compiler/src/ir/serialize.js";
import { moduleRuntimeFeatures } from "../../../packages/compiler/src/ir/ir.js";
import { moduleRuntimeFeatures } from "../../../packages/compiler/src/ir/runtime-features.js";
const mod = deserializeModule(readFileSync(process.argv[2]!, "utf8"));
console.log(JSON.stringify(moduleRuntimeFeatures(mod)));
+7
View File
@@ -47,6 +47,8 @@ test("compiler import fences reject cross-layer and cross-parser imports", () =>
const cases = [
["packages/compiler/src/backend/probe.ts", "../frontend/program.js"],
["packages/compiler/src/frontend/probe.ts", "../backend/llvm/emitter.js"],
["packages/compiler/src/frontend/lowering/probe.ts", "../../ir/validate.js"],
["packages/compiler/src/frontend/cjs-lexer.ts", "../ir/validate.js"],
["packages/compiler/src/ir/probe.ts", "typescript5"],
["packages/compiler/src/frontend/ts7/probe.ts", "typescript5/lib/typescript.js"],
["packages/compiler/src/frontend/npm.ts", "../backend/llvm/emitter.js"],
@@ -67,6 +69,11 @@ test("IR imports and the explicit parser islands remain usable", () => {
const work = workspace();
source(work, "packages/compiler/src/backend/probe.ts", 'export { value } from "../ir/ir.js";\n');
source(work, "packages/compiler/src/frontend/probe.ts", 'export { value } from "../ir/ir.js";\n');
source(
work,
"packages/compiler/src/frontend/lowering/probe.ts",
'export { LIB_FN_SIGS } from "../../ir/builtin-signatures.js";\nexport { STR_INTRINSIC_SIGS } from "../../ir/intrinsic-signatures.js";\n',
);
for (const name of [
"frontend/npm.ts",
"frontend/cjs-lexer.ts",
@@ -15,13 +15,13 @@ import {
type AnalyzeResult,
} from "@scriptc/compiler";
import { everyStmtList } from "../../packages/compiler/src/ir/traverse.js";
import { type IrModule } from "../../packages/compiler/src/ir/ir.js";
import {
moduleUsesInspect,
moduleUsesDynInvoke,
moduleUsesRegex,
moduleUsesCopying,
type IrModule,
} from "../../packages/compiler/src/ir/ir.js";
} from "../../packages/compiler/src/ir/runtime-features.js";
import { backendAnalysisCases } from "./self-hosting-backend-cases.js";
const root = fileURLToPath(new URL("../..", import.meta.url));
+2 -2
View File
@@ -8,13 +8,13 @@ import { analyze, compile, compileC, deserializeModule, serializeModule } from "
import { emitLlvmModule } from "../../packages/compiler/src/backend/llvm/emitter.js";
import { scalarizeNumericRecords } from "../../packages/compiler/src/ir/scalar-records.js";
import { everyStmtList } from "../../packages/compiler/src/ir/traverse.js";
import { type IrModule } from "../../packages/compiler/src/ir/ir.js";
import {
moduleUsesCopying,
moduleUsesDynInvoke,
moduleUsesInspect,
moduleUsesRegex,
type IrModule,
} from "../../packages/compiler/src/ir/ir.js";
} from "../../packages/compiler/src/ir/runtime-features.js";
import {
emissionCases,
emissionModule,
@@ -13,6 +13,7 @@ import {
validateModule,
} from "@scriptc/compiler";
import { emitLlvmModule } from "../../packages/compiler/src/backend/llvm/emitter.js";
import { type IrModule } from "../../packages/compiler/src/ir/ir.js";
import {
moduleUsesAssert,
moduleUsesBigInt,
@@ -25,8 +26,7 @@ import {
moduleUsesStream,
moduleUsesSymbol,
moduleUsesZlib,
type IrModule,
} from "../../packages/compiler/src/ir/ir.js";
} from "../../packages/compiler/src/ir/runtime-features.js";
import {
llvmEmitterCases,
llvmEmitterOptions,
@@ -9,6 +9,7 @@ import { compileC, deserializeModule, emitLlvmModule, validateModule } from "@sc
import type { compile } from "@scriptc/compiler";
import type { IrModule } from "../../packages/compiler/src/ir/ir.js";
import { ts7Executable } from "../../packages/compiler/src/frontend/ts7/rpc-api.js";
import {
moduleUsesAssert,
moduleUsesBigInt,
@@ -18,7 +19,7 @@ import {
moduleUsesRegex,
moduleUsesSymbol,
moduleUsesZlib,
} from "../../packages/compiler/src/ir/ir.js";
} from "../../packages/compiler/src/ir/runtime-features.js";
const root = join(import.meta.dirname, "../..");
const entry = join(root, "tests/fixtures/self-hosting/frontend-pipeline.ts");
+1 -1
View File
@@ -738,7 +738,7 @@ test("constructing a spread RequestInit does not apply fetch conversion fences",
/* ── attestation ↔ fence parity: the ask-5 §4 invariant's ground ─────────
* The library sidecar's `deterministic` attestation demotes on libCall
* spellings by prefix (ir/ir.ts's LIB_NONDETERMINISTIC_PREFIXES); the
* spellings by prefix (library/ir-policy.ts's LIB_NONDETERMINISTIC_PREFIXES); the
* profile's determinism fences deny surfaces by manifest id. The §4
* invariant — a program that compiles under full fences attests
* deterministic: true — holds only if every spelling the attestation