mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-22 05:59:31 +08:00
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:
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
@@ -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,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);
|
||||
});
|
||||
});
|
||||
|
||||
+2
-4421
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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user