Files
scriptc/packages/compiler/src/ir/validate.ts
T
Chris Tate 93af33085e Handle positioned string affix calls in static mode (#421)
* Handle positioned string affix calls in static mode

* Record preflight baselines for string affix fixtures
2026-09-25 07:58:24 -05:00

5960 lines
298 KiB
TypeScript

/* IR validation — runs on every compile as the backstop for frontend bugs.
* A violation is an internal compiler error (the frontend should never
* produce invalid IR), but it still carries the user's source location:
* an ICE that points at source is a gift to whoever debugs it.
*/
import type {
IrClassDef,
IrExpr,
IrFunction,
IrGlobal,
IrLibFn,
IrModule,
IrRecordShape,
IrRegexIntrinsicMethod,
IrStmt,
IrStrIntrinsicMethod,
IrType,
IrUnionDef,
SrcLoc,
} from "./ir.js";
import { arrayOf, BOOL, BYTES_U8, bytesOf, canAdaptDynFuncTo, canConvertToDyn, canExitIslandToType, canMarshalIntoIsland, canMarshalTypedFuncIntoIsland, CHILD_T, CHILDSTREAM_T, CHILDWRITER_T, CRYPTOHASH_T, CRYPTOHMAC_T, DATE_T, DGRAMSOCK_T, DYN, DYN_HANDLE_KINDS, F64, ffiClassType, ffiSourceParamTypes, FILEHANDLE_T, FSWATCHER_T, HTTP2SESSION_T, HTTP2STREAM_T, HTTPCLIENTREQ_T, HTTPREQ_T, HTTPRES_T, islandPromisePayloadTag, isDynTypedRefType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isJsonSafeType, isJsonStringifySafeType, isRefCounted, isSupportedArrayElem, isSupportedIndexValue, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, jsOpResultKind, JSVAL, NETSERVER_T, NETSOCKET_T, PROCSTREAM_T, REF_TRUTHY_KINDS, REGEX, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, SEARCH_PARAMS_T, SECURECTX_T, shapeHasAccessorSlots, SPAWNRES_T, STATS_T, STRING, SYMBOL_T, TESTCTX_T, typeEquals, typeKey, unionFuncSetArmsOk, URL_T, VOID } from "./ir.js";
import { BIGINT_T } from "./ir.js";
/** Per-method signature for strIntrinsic: `argTypes` lists every argument
* position (optional ones included); `minArgs` is how many may be omitted
* from the end (backends fill the documented defaults). Exported for 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 },
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 },
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 },
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) },
};
/** Closed-union signature table for `libCall` (mirrors ambient/scriptc.d.ts).
* All argument positions are required — the surface has no optionals.
* readFileSync's second argument is the (always-"utf8") encoding: it is
* evaluated for JS-exact side-effect order and ignored by the runtime.
* A `null` argument slot is program-dependent (a builtin-error receiver) —
* the libCall case checks it specially, like process.envGet's result.
* Exported for the frontend's lib-boundary pass (lib-boundary.ts). */
export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result: IrType }> = {
"fetch.start": { argTypes: [STRING, DYN], result: { kind: "promise", inner: DYN } },
"fetch.responseNew": { argTypes: [DYN, DYN], result: DYN },
"fetch.responseJson": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
"fetch.responseText": { argTypes: [DYN], result: { kind: "promise", inner: STRING } },
"fetch.responseBytes": { argTypes: [DYN], result: { kind: "promise", inner: BYTES_U8 } },
"fetch.abortControllerNew": { argTypes: [], result: DYN },
"fetch.abortTimeout": { argTypes: [DYN], result: DYN },
"fetch.abortNow": { argTypes: [DYN], result: DYN },
"fetch.abortAny": { argTypes: [DYN], result: DYN },
"fetch.streamNew": { argTypes: [DYN], result: DYN },
// Program-dependent iterable: typed arrays/bytes/string stay intact so
// the native stream can pull lazily; checked-dynamic values are the
// fallback. The libCall validator below checks the closed set.
"fetch.streamFrom": { argTypes: [null], result: DYN },
// The chunk/result record depends on ReadableStream<T>; validated below.
"fetch.readerRead": { argTypes: [DYN], result: VOID },
"island.eval": { argTypes: [STRING], result: STRING },
"island.import": { argTypes: [STRING, STRING, STRING], result: JSVAL },
"island.importDyn": { argTypes: [STRING], result: JSVAL },
// Result is the cast's mapped PROMISE target (program-dependent) —
// checked in the libCall case, like error.new.
"island.castFail": { argTypes: [JSVAL, STRING], result: VOID },
"json.parse": { argTypes: [STRING], result: DYN },
"dyn.keySet": { argTypes: [DYN, STRING, DYN], result: VOID },
"dyn.iterPack": { argTypes: [DYN, STRING], result: DYN },
"dyn.arrLen": { argTypes: [DYN], result: F64 },
"dyn.arrAt": { argTypes: [DYN, F64], result: DYN },
"dyn.hasKey": { argTypes: [DYN, STRING], result: BOOL },
"dyn.toString": { argTypes: [DYN, STRING, STRING], result: STRING },
"dyn.defineProps": { argTypes: [DYN, DYN], result: DYN },
"dyn.typeof": { argTypes: [DYN], result: STRING },
"module.registryInit": { argTypes: [F64], result: VOID },
"module.define": { argTypes: [F64, STRING, STRING, STRING, arrayOf(STRING), BOOL], result: VOID },
"module.enter": { argTypes: [F64], result: VOID },
"module.link": { argTypes: [F64, F64], result: VOID },
"module.finish": { argTypes: [F64], result: VOID },
"module.fail": { argTypes: [F64], result: VOID },
"module.filename": { argTypes: [F64], result: STRING },
"module.id": { argTypes: [F64], result: STRING },
"module.path": { argTypes: [F64], result: STRING },
"module.paths": { argTypes: [F64], result: arrayOf(STRING) },
"module.children": { argTypes: [F64], result: arrayOf(F64) },
"module.parent": { argTypes: [F64], result: F64 },
"module.loaded": { argTypes: [F64], result: BOOL },
"module.cacheGet": { argTypes: [STRING], result: F64 },
"module.cacheHas": { argTypes: [STRING], result: BOOL },
"module.cacheKeys": { argTypes: [], result: arrayOf(STRING) },
"timers.setTimeout": { argTypes: [{ kind: "func", params: [], ret: VOID }, F64], result: VOID },
"timers.setInterval": { argTypes: [{ kind: "func", params: [], ret: VOID }, F64], result: F64 },
"timers.clearInterval": { argTypes: [F64], result: VOID },
"timers.setTimeoutHandle": { argTypes: [{ kind: "func", params: [], ret: VOID }, F64], result: F64 },
"timers.clearTimeout": { argTypes: [F64], result: VOID },
"timers.unref": { argTypes: [F64], result: F64 },
"timers.ref": { argTypes: [F64], result: F64 },
"timers.hasRef": { argTypes: [F64], result: BOOL },
"timers.refresh": { argTypes: [F64], result: F64 },
"timers.setImmediate": { argTypes: [{ kind: "func", params: [], ret: VOID }], result: F64 },
"timers.queueMicrotask": { argTypes: [{ kind: "func", params: [], ret: VOID }], result: VOID },
"timers.queueMicrotaskDyn": { argTypes: [DYN], result: VOID },
"timers.clearImmediate": { argTypes: [F64], result: VOID },
"process.nextTick": { argTypes: [{ kind: "func", params: [], ret: VOID }], result: VOID },
"process.uptime": { argTypes: [], result: F64 },
"perf.now": { argTypes: [], result: F64 },
"process.availableMemory": { argTypes: [], result: F64 },
"process.constrainedMemory": { argTypes: [], result: F64 },
"process.cpuUser": { argTypes: [], result: F64 },
"process.cpuSystem": { argTypes: [], result: F64 },
"process.cpuUserDiff": { argTypes: [F64], result: F64 },
"process.cpuSystemDiff": { argTypes: [F64], result: F64 },
"process.threadCpuUser": { argTypes: [], result: F64 },
"process.threadCpuSystem": { argTypes: [], result: F64 },
"process.threadCpuUserDiff": { argTypes: [F64], result: F64 },
"process.threadCpuSystemDiff": { argTypes: [F64], result: F64 },
"process.cpuPrevValidate": { argTypes: [F64, F64], result: VOID },
"process.rusage": { argTypes: [F64], result: F64 },
"process.activeResources": { argTypes: [], result: arrayOf(STRING) },
"timers.immediateUnref": { argTypes: [F64], result: F64 },
"timers.immediateRef": { argTypes: [F64], result: F64 },
"timers.immediateHasRef": { argTypes: [F64], result: BOOL },
"timers.clearNoop": { argTypes: [], result: VOID },
// Signal listeners are zero-param (the ambient shape); exit/stdin
// callbacks carry program-dependent one-param shapes — null slots, the
// libCall case checks them (child.onExit precedent).
"process.onSignal": { argTypes: [F64, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
"process.offSignal": { argTypes: [F64, { kind: "func", params: [], ret: VOID }], result: VOID },
"process.onExit": { argTypes: [null, BOOL], result: VOID },
"process.offExit": { argTypes: [null], result: VOID },
"stdin.onData": { argTypes: [null, BOOL], result: VOID },
"stdin.onEnd": { argTypes: [{ kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
"stdin.onError": { argTypes: [null, BOOL], result: VOID },
"stdin.nextChunk": { argTypes: [], result: { kind: "promise", inner: BYTES_U8 } },
"fs.readFileSync": { argTypes: [STRING, STRING], result: STRING },
"fs.readFileSyncBuf": { argTypes: [STRING], result: BYTES_U8 },
"fs.readFileSyncDyn": { argTypes: [STRING, DYN], result: DYN },
"fs.writeFileSync": { argTypes: [STRING, STRING], result: VOID },
"fs.appendFileSync": { argTypes: [STRING, STRING], result: VOID },
"fs.existsSync": { argTypes: [STRING], result: BOOL },
"fs.mkdirSync": { argTypes: [STRING], result: VOID },
"fs.rmSync": { argTypes: [STRING], result: VOID },
"fs.rmdirSync": { argTypes: [STRING], result: VOID },
"fs.readdirSync": { argTypes: [STRING], result: arrayOf(STRING) },
// Result is the call site's Dirent record array (VOID is the
// structure-checked sentinel, the os.networkInterfaces pattern).
"fs.readdirTypesSync": { argTypes: [STRING], result: VOID },
"path.join": { argTypes: [arrayOf(STRING)], result: STRING },
"path.resolve": { argTypes: [arrayOf(STRING)], result: STRING },
"path.normalize": { argTypes: [STRING], result: STRING },
"path.dirname": { argTypes: [STRING], result: STRING },
"path.basename": { argTypes: [STRING, STRING], result: STRING },
"path.extname": { argTypes: [STRING], result: STRING },
"path.isAbsolute": { argTypes: [STRING], result: BOOL },
"path.relative": { argTypes: [STRING, STRING], result: STRING },
"path.toNamespacedPath": { argTypes: [STRING], result: STRING },
"path.win32Join": { argTypes: [arrayOf(STRING)], result: STRING },
"path.win32Resolve": { argTypes: [arrayOf(STRING)], result: STRING },
"path.win32Normalize": { argTypes: [STRING], result: STRING },
"path.win32Dirname": { argTypes: [STRING], result: STRING },
"path.win32Basename": { argTypes: [STRING, STRING], result: STRING },
"path.win32Extname": { argTypes: [STRING], result: STRING },
"path.win32IsAbsolute": { argTypes: [STRING], result: BOOL },
"path.win32Relative": { argTypes: [STRING, STRING], result: STRING },
"path.win32ToNamespacedPath": { argTypes: [STRING], result: STRING },
"os.homedir": { argTypes: [], result: STRING },
"os.release": { argTypes: [], result: STRING },
"os.type": { argTypes: [], result: STRING },
"os.totalmem": { argTypes: [], result: F64 },
"process.umask": { argTypes: [F64], result: F64 },
"process.chdir": { argTypes: [STRING], result: VOID },
"process.exiting": { argTypes: [], result: BOOL },
"net.getAutoSelTimeout": { argTypes: [], result: F64 },
"net.setAutoSelTimeout": { argTypes: [F64], result: VOID },
"fs.realpathSync": { argTypes: [STRING], result: STRING },
"os.userName": { argTypes: [], result: STRING },
"os.userShell": { argTypes: [], result: STRING },
"os.userHomedir": { argTypes: [], result: STRING },
"os.tmpdir": { argTypes: [], result: STRING },
// Result is the call site's Dict<NetworkInterfaceInfo[]> record (VOID is
// the dgram.address sentinel — the libCall case checks the structure).
"os.networkInterfaces": { argTypes: [], result: VOID },
"math.maxArr": { argTypes: [arrayOf(F64)], result: F64 },
"math.minArr": { argTypes: [arrayOf(F64)], result: F64 },
"math.hypotArr": { argTypes: [arrayOf(F64)], result: F64 },
"math.floor": { argTypes: [F64], result: F64 },
"math.abs": { argTypes: [F64], result: F64 },
"math.round": { argTypes: [F64], result: F64 },
"math.trunc": { argTypes: [F64], result: F64 },
"math.ceil": { argTypes: [F64], result: F64 },
"math.sin": { argTypes: [F64], result: F64 },
"math.cos": { argTypes: [F64], result: F64 },
"math.tan": { argTypes: [F64], result: F64 },
"math.asin": { argTypes: [F64], result: F64 },
"math.acos": { argTypes: [F64], result: F64 },
"math.atan": { argTypes: [F64], result: F64 },
"math.cbrt": { argTypes: [F64], result: F64 },
"math.sign": { argTypes: [F64], result: F64 },
"math.exp": { argTypes: [F64], result: F64 },
"math.sqrt": { argTypes: [F64], result: F64 },
"math.log": { argTypes: [F64], result: F64 },
"math.log2": { argTypes: [F64], result: F64 },
"math.log10": { argTypes: [F64], result: F64 },
"math.atan2": { argTypes: [F64, F64], result: F64 },
"math.pow": { argTypes: [F64, F64], result: F64 },
"math.min": { argTypes: [F64, F64], result: F64 },
"math.max": { argTypes: [F64, F64], result: F64 },
"math.random": { argTypes: [], result: F64 },
"num.parseInt": { argTypes: [STRING, F64], result: F64 },
"num.parseFloat": { argTypes: [STRING], result: F64 },
"num.fromString": { argTypes: [STRING], result: F64 },
"num.isNaN": { argTypes: [F64], result: BOOL },
"str.encodeUriComponent": { argTypes: [STRING], result: STRING },
// The base64 globals: the argument is a dyn value (WebIDL ToString
// runs in the runtime); the zero-argument form always throws.
"str.atob": { argTypes: [DYN], result: STRING },
"str.btoa": { argTypes: [DYN], result: STRING },
"str.b64Missing": { argTypes: [], result: STRING },
"str.decodeUriComponent": { argTypes: [STRING], result: STRING },
"str.encodeUri": { argTypes: [STRING], result: STRING },
"regexp.escape": { argTypes: [STRING], result: STRING },
"num.toExponential": { argTypes: [F64], result: STRING },
"num.toFixed0": { argTypes: [F64], result: STRING },
"num.toFixed": { argTypes: [F64, F64], result: STRING },
"num.sameValue": { argTypes: [F64, F64], result: BOOL },
"intl.numFormatEnUs": { argTypes: [F64], result: STRING },
"sym.new": { argTypes: [STRING], result: SYMBOL_T },
"sym.newAnon": { argTypes: [], result: SYMBOL_T },
"sym.for": { argTypes: [STRING], result: SYMBOL_T },
// Result is the interned `string | undefined` union — the libCall case
// checks the arms (the spawnRes.error pattern).
"sym.keyFor": { argTypes: [SYMBOL_T], result: VOID },
"sym.desc": { argTypes: [SYMBOL_T], result: VOID },
"sym.toString": { argTypes: [SYMBOL_T], result: STRING },
"bigint.parse": { argTypes: [STRING], result: BIGINT_T },
"bigint.fromF64": { argTypes: [F64], result: BIGINT_T },
"bigint.neg": { argTypes: [BIGINT_T], result: BIGINT_T },
"bigint.not": { argTypes: [BIGINT_T], result: BIGINT_T },
"bigint.add": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T },
"bigint.sub": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T },
"bigint.mul": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T },
"bigint.div": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T },
"bigint.mod": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T },
"bigint.pow": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T },
"bigint.and": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T },
"bigint.or": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T },
"bigint.xor": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T },
"bigint.shl": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T },
"bigint.shr": { argTypes: [BIGINT_T, BIGINT_T], result: BIGINT_T },
"bigint.eq": { argTypes: [BIGINT_T, BIGINT_T], result: BOOL },
"bigint.eqString": { argTypes: [BIGINT_T, STRING], result: BOOL },
"bigint.cmp": { argTypes: [BIGINT_T, BIGINT_T], result: F64 },
"bigint.cmpNumber": { argTypes: [BIGINT_T, F64], result: F64 },
"bigint.truthy": { argTypes: [BIGINT_T], result: BOOL },
"bigint.toString": { argTypes: [BIGINT_T, F64], result: STRING },
"bigint.inspect": { argTypes: [BIGINT_T], result: STRING },
"bigint.toF64": { argTypes: [BIGINT_T], result: F64 },
"bigint.asUintN": { argTypes: [F64, BIGINT_T], result: BIGINT_T },
"bigint.asIntN": { argTypes: [F64, BIGINT_T], result: BIGINT_T },
"bigint.bufferRead": { argTypes: [BYTES_U8, F64, BOOL, BOOL], result: BIGINT_T },
"bigint.bufferWrite": { argTypes: [BYTES_U8, BIGINT_T, F64, BOOL, BOOL], result: F64 },
"bigint.dataViewGet": { argTypes: [BYTES_U8, F64, BOOL, BOOL], result: BIGINT_T },
"bigint.dataViewSet": { argTypes: [BYTES_U8, F64, BIGINT_T, BOOL], result: VOID },
"url.new": { argTypes: [STRING], result: URL_T },
"url.protocol": { argTypes: [URL_T], result: STRING },
"url.origin": { argTypes: [URL_T], result: STRING },
"url.username": { argTypes: [URL_T], result: STRING },
"url.password": { argTypes: [URL_T], result: STRING },
"url.host": { argTypes: [URL_T], result: STRING },
"url.hostname": { argTypes: [URL_T], result: STRING },
"url.port": { argTypes: [URL_T], result: STRING },
"url.pathname": { argTypes: [URL_T], result: STRING },
"url.href": { argTypes: [URL_T], result: STRING },
"url.fileURLToPathUrl": { argTypes: [URL_T], result: STRING },
"url.fileURLToPathStr": { argTypes: [STRING], result: STRING },
"url.pathToFileURL": { argTypes: [STRING], result: URL_T },
"url.pathToFileURLWin32": { argTypes: [STRING], result: URL_T },
"sp.new": { argTypes: [], result: SEARCH_PARAMS_T },
"sp.parse": { argTypes: [STRING], result: SEARCH_PARAMS_T },
"sp.copy": { argTypes: [SEARCH_PARAMS_T], result: SEARCH_PARAMS_T },
// The pairs argument is string[][] — checked structurally below (the
// generic array-of slot has no named constant here).
"sp.fromPairs": { argTypes: [null], result: SEARCH_PARAMS_T },
"sp.with": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: SEARCH_PARAMS_T },
"url.searchParams": { argTypes: [URL_T], result: SEARCH_PARAMS_T },
"url.search": { argTypes: [URL_T], result: STRING },
"url.hash": { argTypes: [URL_T], result: STRING },
// Result is the interned `string | null` union — the libCall case
// checks the arms (the spawnRes.signal pattern).
"sp.get": { argTypes: [SEARCH_PARAMS_T, STRING], result: VOID },
"sp.getAll": { argTypes: [SEARCH_PARAMS_T, STRING], result: arrayOf(STRING) },
"sp.append": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: VOID },
"sp.set": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: VOID },
"sp.delete": { argTypes: [SEARCH_PARAMS_T, STRING], result: VOID },
"sp.deleteValue": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: VOID },
"sp.has": { argTypes: [SEARCH_PARAMS_T, STRING], result: BOOL },
"sp.hasValue": { argTypes: [SEARCH_PARAMS_T, STRING, STRING], result: BOOL },
"sp.sort": { argTypes: [SEARCH_PARAMS_T], result: VOID },
"sp.size": { argTypes: [SEARCH_PARAMS_T], result: F64 },
"sp.toString": { argTypes: [SEARCH_PARAMS_T], result: STRING },
"sp.keyAt": { argTypes: [SEARCH_PARAMS_T, F64], result: STRING },
"sp.valAt": { argTypes: [SEARCH_PARAMS_T, F64], result: STRING },
// node:querystring. qs.parse's result is the call site's ParsedUrlQuery
// dictionary record (VOID is the networkInterfaces sentinel — the
// libCall case checks the structure); qs.stringify's object argument
// is a dyn value (the frontend dynFroms typed records).
"qs.parse": { argTypes: [STRING, STRING, STRING, F64], result: VOID },
"qs.stringify": { argTypes: [DYN, STRING, STRING], result: STRING },
"qs.escape": { argTypes: [STRING], result: STRING },
"qs.unescape": { argTypes: [STRING], result: STRING },
"util.parseArgs": { argTypes: [DYN], result: DYN },
"fs.statSync": { argTypes: [STRING], result: STATS_T },
"fs.lstatSync": { argTypes: [STRING], result: STATS_T },
"fs.fstatSync": { argTypes: [F64], result: STATS_T },
"fs.fchmodSync": { argTypes: [F64, F64], result: VOID },
"fs.fsyncSync": { argTypes: [F64], result: VOID },
"fs.linkSync": { argTypes: [STRING, STRING], result: VOID },
"fs.openSync": { argTypes: [STRING, STRING], result: F64 },
"fs.openNumericSync": { argTypes: [STRING, F64, F64], result: F64 },
"fs.readSync": { argTypes: [F64, BYTES_U8, F64, F64, F64], result: F64 },
"fs.writeSync": { argTypes: [F64, BYTES_U8, F64, F64, F64], result: F64 },
"fs.writeStrSync": { argTypes: [F64, STRING, F64, STRING], result: F64 },
// fs.watch's callback func type is program-dependent (zero params, or
// the eventType string) — the slot pins arity and the path/receiver.
"fs.watch": { argTypes: [STRING], result: FSWATCHER_T },
"fs.watchCb": { argTypes: [STRING, null], result: FSWATCHER_T },
"watcher.close": { argTypes: [FSWATCHER_T], result: VOID },
"crypto.x509Fingerprint": { argTypes: [BYTES_U8], result: STRING },
"crypto.x509FingerprintStr": { argTypes: [STRING], result: STRING },
"crypto.x509ValidFrom": { argTypes: [BYTES_U8], result: STRING },
"crypto.x509ValidFromStr": { argTypes: [STRING], result: STRING },
"crypto.x509ValidTo": { argTypes: [BYTES_U8], result: STRING },
"crypto.x509ValidToStr": { argTypes: [STRING], result: STRING },
"fs.closeSync": { argTypes: [F64], result: VOID },
"stats.isFile": { argTypes: [STATS_T], result: BOOL },
"stats.isDirectory": { argTypes: [STATS_T], result: BOOL },
"stats.isSymbolicLink": { argTypes: [STATS_T], result: BOOL },
"stats.size": { argTypes: [STATS_T], result: F64 },
"stats.dev": { argTypes: [STATS_T], result: F64 },
"stats.ino": { argTypes: [STATS_T], result: F64 },
"stats.blocks": { argTypes: [STATS_T], result: F64 },
"stats.nlink": { argTypes: [STATS_T], result: F64 },
"stats.atimeMs": { argTypes: [STATS_T], result: F64 },
"stats.mtimeMs": { argTypes: [STATS_T], result: F64 },
"stats.ctimeMs": { argTypes: [STATS_T], result: F64 },
// The wider sync fs slice (unlink/chmod/chown/copyfile and the
// mode-carrying write/mkdir forms).
"fs.unlinkSync": { argTypes: [STRING], result: VOID },
"fs.chmodSync": { argTypes: [STRING, F64], result: VOID },
"fs.chownSync": { argTypes: [STRING, F64, F64], result: VOID },
"fs.copyFileSync": { argTypes: [STRING, STRING], result: VOID },
"fs.renameSync": { argTypes: [STRING, STRING], result: VOID },
// Callback type is program-dependent (zero params or Error | null).
"fs.renameCb": { argTypes: [STRING, STRING, null], result: VOID },
"fs.writeFileModeSync": { argTypes: [STRING, STRING, F64], result: VOID },
"fs.mkdirModeSync": { argTypes: [STRING, F64], result: VOID },
"fs.mkdirRecursiveModeSync": { argTypes: [STRING, F64], result: VOID },
// Atomics.wait over an Int32Array — the synchronous-sleep idiom.
"atomics.wait": { argTypes: [bytesOf("i32"), F64, F64, F64], result: STRING },
"cp.spawnSync": { argTypes: [STRING, arrayOf(STRING)], result: SPAWNRES_T },
// spawnSync's options form: timeout, killSignal name ("" = SIGTERM),
// and the three stdio modes.
"cp.spawnSyncOpts": {
argTypes: [STRING, arrayOf(STRING), F64, STRING, F64, F64, F64],
result: SPAWNRES_T,
},
"cp.spawnSyncStdioStr": {
argTypes: [STRING, arrayOf(STRING), F64, STRING, STRING],
result: SPAWNRES_T,
},
// node:net (scr_net.c). Listener slots are program-dependent closures —
// null slots, checked in the libCall case (the child.onExit precedent);
// the zero-param callbacks pin the exact func type here. The trailing
// BOOL on registrations is the once flag.
"net.createServer": { argTypes: [], result: NETSERVER_T },
"net.createServerCb": { argTypes: [null], result: NETSERVER_T },
"net.listen": { argTypes: [NETSERVER_T, F64], result: VOID },
"net.listenCb": { argTypes: [NETSERVER_T, F64, { kind: "func", params: [], ret: VOID }], result: VOID },
"net.listenOpts": { argTypes: [NETSERVER_T, F64, STRING, BOOL], result: VOID },
// The callback slot is a zero-param void closure OR its
// `(() => void) | undefined` optional-binding union (checked specially).
"net.listenOptsCb": { argTypes: [NETSERVER_T, F64, STRING, BOOL, null], result: VOID },
"net.listenOptsReusePort": { argTypes: [NETSERVER_T, F64, STRING, BOOL, BOOL], result: VOID },
// The callback is a zero-param void closure OR its optional-binding union;
// the callback follows the reusePort BOOL in this additive ABI.
"net.listenOptsReusePortCb": { argTypes: [NETSERVER_T, F64, STRING, BOOL, BOOL, null], result: VOID },
"net.serverPort": { argTypes: [NETSERVER_T], result: F64 },
"net.serverListening": { argTypes: [NETSERVER_T], result: BOOL },
// net.serverAddress's record result is shape-checked in the libCall case
// (the dgram.address sentinel pattern).
"net.serverAddress": { argTypes: [NETSERVER_T], result: VOID },
"net.serverClose": { argTypes: [NETSERVER_T], result: VOID },
"net.serverCloseCb": { argTypes: [NETSERVER_T, { kind: "func", params: [], ret: VOID }], result: VOID },
"net.serverOnError": { argTypes: [NETSERVER_T, null, BOOL], result: VOID },
"net.serverOnClose": { argTypes: [NETSERVER_T, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
"net.serverOnConnection": { argTypes: [NETSERVER_T, null, BOOL], result: VOID },
"net.serverOnSecureConnection": { argTypes: [NETSERVER_T, null, BOOL], result: VOID },
"net.connect": { argTypes: [F64, STRING], result: NETSOCKET_T },
"net.connectAttempt": { argTypes: [F64, STRING, DYN], result: NETSOCKET_T },
"net.connectOptsChk": { argTypes: [DYN, STRING], result: VOID },
"net.connectCb": { argTypes: [F64, STRING, { kind: "func", params: [], ret: VOID }], result: NETSOCKET_T },
// The lookup's exact func shape is program data (its answer callback's
// union/record types) — checked specially in the libCall case.
"net.connectLookup": { argTypes: [F64, STRING, null], result: NETSOCKET_T },
"net.sockWrite": { argTypes: [NETSOCKET_T, STRING], result: VOID },
"net.sockWriteBytes": { argTypes: [NETSOCKET_T, BYTES_U8], result: VOID },
"net.sockEnd": { argTypes: [NETSOCKET_T], result: VOID },
"net.sockEndStr": { argTypes: [NETSOCKET_T, STRING], result: VOID },
"net.sockEndBytes": { argTypes: [NETSOCKET_T, BYTES_U8], result: VOID },
"net.sockWriteDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID },
"net.sockEndDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID },
"net.sockDestroy": { argTypes: [NETSOCKET_T], result: VOID },
"net.sockPipe": { argTypes: [NETSOCKET_T, NETSOCKET_T], result: VOID },
"net.sockPipeRes": { argTypes: [NETSOCKET_T, HTTPRES_T], result: VOID },
"net.sockOnData": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID },
"net.sockOnEnd": { argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
"net.sockOnClose": { argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
"net.sockOnError": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID },
"net.sockOnConnect": { argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
// node:dgram + node:dns (scr_dgram.c). The message/error listeners and
// dns.lookup's callback are program-dependent closures (null slots,
// checked in the libCall case); dgram.address's record result is
// program-dependent too (VOID here is the envGet sentinel — the libCall
// case checks the {address, family, port} shape).
"dgram.createSocket": { argTypes: [BOOL], result: DGRAMSOCK_T },
"dgram.bind": { argTypes: [DGRAMSOCK_T, F64, STRING], result: VOID },
"dgram.bindCb": { argTypes: [DGRAMSOCK_T, F64, STRING, { kind: "func", params: [], ret: VOID }], result: VOID },
"dgram.connect": { argTypes: [DGRAMSOCK_T, F64, STRING], result: VOID },
"dgram.connectCb": { argTypes: [DGRAMSOCK_T, F64, STRING, { kind: "func", params: [], ret: VOID }], result: VOID },
"dgram.sendStr": { argTypes: [DGRAMSOCK_T, STRING, F64, STRING], result: VOID },
"dgram.sendBytes": { argTypes: [DGRAMSOCK_T, BYTES_U8, F64, STRING], result: VOID },
"dgram.sendChk": { argTypes: [DGRAMSOCK_T, DYN, DYN, DYN, DYN, DYN, STRING], result: VOID },
"dgram.address": { argTypes: [DGRAMSOCK_T], result: VOID },
"dgram.close": { argTypes: [DGRAMSOCK_T], result: VOID },
"dgram.closeCb": { argTypes: [DGRAMSOCK_T, { kind: "func", params: [], ret: VOID }], result: VOID },
"dgram.unref": { argTypes: [DGRAMSOCK_T], result: VOID },
"dgram.ref": { argTypes: [DGRAMSOCK_T], result: VOID },
"dgram.onMessage": { argTypes: [DGRAMSOCK_T, null, BOOL], result: VOID },
"dgram.onError": { argTypes: [DGRAMSOCK_T, null, BOOL], result: VOID },
"dgram.onListening": { argTypes: [DGRAMSOCK_T, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
"dgram.onClose": { argTypes: [DGRAMSOCK_T, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
"dgram.onConnect": { argTypes: [DGRAMSOCK_T, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
"dns.lookup": { argTypes: [STRING, F64, null], result: VOID },
// node:test (scr_test.c). Bodies are program-dependent closures (0 or
// 1 testCtx param, void or Promise<void> result — the spoke pinned the
// shape): null slots. sub's result is the settled Promise<void> the
// await consumes.
"test.register": { argTypes: [STRING, F64, STRING, null, F64, STRING], result: VOID },
"test.registerEmpty": { argTypes: [STRING, F64, STRING, F64, STRING], result: VOID },
"test.suite": { argTypes: [STRING, F64, STRING, { kind: "func", params: [], ret: VOID }, STRING], result: VOID },
"test.hook": { argTypes: [F64, null, F64], result: VOID },
"test.sub": { argTypes: [TESTCTX_T, STRING, F64, STRING, null, F64, STRING], result: { kind: "promise", inner: VOID } },
"test.subEmpty": { argTypes: [TESTCTX_T, STRING, F64, STRING, STRING], result: VOID },
"test.ctxSkip": { argTypes: [TESTCTX_T, STRING], result: VOID },
"test.ctxTodo": { argTypes: [TESTCTX_T, STRING], result: VOID },
"test.ctxDiagnostic": { argTypes: [TESTCTX_T, STRING], result: VOID },
"test.ctxName": { argTypes: [TESTCTX_T], result: STRING },
// node:http (scr_http.c over scr_net.c). The handler and the data
// listeners are program-dependent closures (null slots, checked in the
// libCall case); reqHeader's result is the interned string|undefined
// union, checked like process.envGet.
"http.createServer": { argTypes: [null], result: NETSERVER_T },
"http.createServerEmpty": { argTypes: [], result: NETSERVER_T },
"http.validateHeaderName": { argTypes: [STRING, STRING], result: VOID },
"http.validateHeaderValue": { argTypes: [STRING, DYN], result: VOID },
"http.serverJoinDupHeaders": { argTypes: [NETSERVER_T], result: VOID },
"http.serverAllowMissingHostHeader": { argTypes: [NETSERVER_T], result: VOID },
"http.serverTimeoutGet": { argTypes: [NETSERVER_T, F64], result: F64 },
"http.serverSetTimeout": { argTypes: [NETSERVER_T, F64], result: VOID },
"http.serverSetTimeoutCb": { argTypes: [NETSERVER_T, F64, null], result: VOID },
"http.serverOnTimeout": { argTypes: [NETSERVER_T, null, BOOL], result: VOID },
"http.serverCloseAllConnections": { argTypes: [NETSERVER_T], result: VOID },
"http.serverCloseIdleConnections": { argTypes: [NETSERVER_T], result: VOID },
"http.serverTimeoutSet": { argTypes: [NETSERVER_T, F64, F64], result: VOID },
"http.serverTimeoutOptionSet": { argTypes: [NETSERVER_T, F64, DYN], result: VOID },
"net.serverOnListening": { argTypes: [NETSERVER_T, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
"http.resStatusGet": { argTypes: [HTTPRES_T], result: F64 },
"http.resStatusSet": { argTypes: [HTTPRES_T, F64], result: VOID },
"http.resStatusMsgGet": { argTypes: [HTTPRES_T], result: VOID },
"http.resStatusMsgSet": { argTypes: [HTTPRES_T, STRING], result: VOID },
"http.resRequest": { argTypes: [HTTPRES_T], result: HTTPREQ_T },
"http.resSocket": { argTypes: [HTTPRES_T], result: VOID },
"http.resWritableFinished": { argTypes: [HTTPRES_T], result: BOOL },
"http.resSendDateGet": { argTypes: [HTTPRES_T], result: BOOL },
"http.resSendDateSet": { argTypes: [HTTPRES_T, BOOL], result: VOID },
"http.resStrictContentLengthGet": { argTypes: [HTTPRES_T], result: BOOL },
"http.resStrictContentLengthSet": { argTypes: [HTTPRES_T, BOOL], result: VOID },
"http.resSetTimeout": { argTypes: [HTTPRES_T, F64], result: VOID },
"http.resSetTimeoutCb": { argTypes: [HTTPRES_T, F64, { kind: "func", params: [], ret: VOID }], result: VOID },
// resGetHeader answers the interned `string | undefined` union — the
// reqHeader/envGet sentinel pattern (VOID here, checked specially).
"http.resGetHeader": { argTypes: [HTTPRES_T, STRING], result: VOID },
"http.resGetHeaderNames": { argTypes: [HTTPRES_T], result: arrayOf(STRING) },
"http.resGetRawHeaderNames": { argTypes: [HTTPRES_T], result: arrayOf(STRING) },
"http.resGetHeaders": { argTypes: [HTTPRES_T], result: DYN },
"http.resHasHeader": { argTypes: [HTTPRES_T, STRING], result: BOOL },
"http.resRemoveHeader": { argTypes: [HTTPRES_T, STRING], result: VOID },
"http.resOnFinish": { argTypes: [HTTPRES_T, { kind: "func", params: [], ret: VOID }], result: VOID },
"http.reqUrl": { argTypes: [HTTPREQ_T], result: STRING },
"http.reqMethod": { argTypes: [HTTPREQ_T], result: STRING },
"http.reqHeader": { argTypes: [HTTPREQ_T, STRING], result: VOID },
"http.reqTrailer": { argTypes: [HTTPREQ_T, STRING], result: VOID },
"http.reqHeaderValues": { argTypes: [HTTPREQ_T, STRING], result: VOID },
"http.reqTrailerValues": { argTypes: [HTTPREQ_T, STRING], result: VOID },
"http.reqOnData": { argTypes: [HTTPREQ_T, null, BOOL], result: VOID },
"http.reqOnEnd": { argTypes: [HTTPREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
"http.resSetHeader": { argTypes: [HTTPRES_T, STRING, STRING], result: VOID },
"http.resWriteHead": { argTypes: [HTTPRES_T, F64], result: VOID },
"http.resWriteHeadN": { argTypes: [HTTPRES_T, F64, arrayOf(STRING), arrayOf(STRING)], result: VOID },
"http.resWriteContinue": { argTypes: [HTTPRES_T], result: VOID },
"http.resWriteProcessing": { argTypes: [HTTPRES_T], result: VOID },
"http.resWriteEarlyHints": { argTypes: [HTTPRES_T, arrayOf(STRING)], result: VOID },
"http.resWrite": { argTypes: [HTTPRES_T, STRING], result: VOID },
"http.resWriteBytes": { argTypes: [HTTPRES_T, BYTES_U8], result: VOID },
"http.resEnd": { argTypes: [HTTPRES_T], result: VOID },
"http.resEndStr": { argTypes: [HTTPRES_T, STRING], result: VOID },
"http.resEndBytes": { argTypes: [HTTPRES_T, BYTES_U8], result: VOID },
"http.resWriteDyn": { argTypes: [HTTPRES_T, DYN], result: VOID },
"http.resEndDyn": { argTypes: [HTTPRES_T, DYN], result: VOID },
"http.resHeadersSent": { argTypes: [HTTPRES_T], result: BOOL },
"http.resWritableEnded": { argTypes: [HTTPRES_T], result: BOOL },
"http.resFlushHeaders": { argTypes: [HTTPRES_T], result: VOID },
"http.resAddTrailers": { argTypes: [HTTPRES_T, arrayOf(STRING)], result: VOID },
"http.resCork": { argTypes: [HTTPRES_T], result: VOID },
"http.resUncork": { argTypes: [HTTPRES_T], result: VOID },
"http.resWritableCorked": { argTypes: [HTTPRES_T], result: F64 },
// The member follow-ups: reqStatusCode's `number | undefined` and
// sockRemoteAddress's `string | undefined` results are shape-checked in
// the special cases below (like reqHeader/columns).
"http.reqStatusCode": { argTypes: [HTTPREQ_T], result: VOID },
// reqStatusMessage's `string | undefined` is program-interned like
// reqHeader's (the VOID here is the envGet sentinel).
"http.reqStatusMessage": { argTypes: [HTTPREQ_T], result: VOID },
"http.reqRawHeaders": { argTypes: [HTTPREQ_T], result: arrayOf(STRING) },
"http.reqHeaderPairs": { argTypes: [HTTPREQ_T], result: arrayOf(STRING) },
"http.reqRawTrailers": { argTypes: [HTTPREQ_T], result: arrayOf(STRING) },
"http.reqTrailerPairs": { argTypes: [HTTPREQ_T], result: arrayOf(STRING) },
"net.sockDestroyed": { argTypes: [NETSOCKET_T], result: BOOL },
"net.sockWritable": { argTypes: [NETSOCKET_T], result: BOOL },
// The 'upgrade' registrations: the callback shapes are program-typed
// ((req, socket, head) and shorter prefixes — the libCall case checks).
"http.serverOnUpgrade": { argTypes: [NETSERVER_T, null, BOOL], result: VOID },
"http.serverOnConnect": { argTypes: [NETSERVER_T, null, BOOL], result: VOID },
"http.clientOnUpgrade": { argTypes: [HTTPCLIENTREQ_T, null, BOOL], result: VOID },
"http.reqSocket": { argTypes: [HTTPREQ_T], result: NETSOCKET_T },
// reqH2Stream's program-interned Http2Stream | undefined result is
// checked in the special libCall cases below.
"http.reqH2Stream": { argTypes: [HTTPREQ_T], result: VOID },
"http.reqH2StreamOrThrow": { argTypes: [HTTPREQ_T, STRING], result: HTTP2STREAM_T },
"http.reqPipeRes": { argTypes: [HTTPREQ_T, HTTPRES_T], result: VOID },
"http.reqPipeClient": { argTypes: [HTTPREQ_T, HTTPCLIENTREQ_T], result: VOID },
"http.reqPipeSock": { argTypes: [HTTPREQ_T, NETSOCKET_T], result: VOID },
"http.reqResume": { argTypes: [HTTPREQ_T], result: VOID },
"http.reqDestroy": { argTypes: [HTTPREQ_T], result: VOID },
"http.reqOnError": { argTypes: [HTTPREQ_T, null, BOOL], result: VOID },
"http.reqOnClose": { argTypes: [HTTPREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
"http.reqOnAborted": { argTypes: [HTTPREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
"http.reqHttpVersion": { argTypes: [HTTPREQ_T], result: STRING },
"http.reqHttpVersionMajor": { argTypes: [HTTPREQ_T], result: F64 },
"http.reqHttpVersionMinor": { argTypes: [HTTPREQ_T], result: F64 },
"http.reqAborted": { argTypes: [HTTPREQ_T], result: BOOL },
"http.reqComplete": { argTypes: [HTTPREQ_T], result: BOOL },
"http.resDestroy": { argTypes: [HTTPRES_T], result: VOID },
"http.resOnClose": { argTypes: [HTTPRES_T, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
"http.resWriteHeadPairs": { argTypes: [HTTPRES_T, F64, arrayOf(STRING)], result: VOID },
"http.resWriteHeadDyn": { argTypes: [HTTPRES_T, F64, DYN], result: VOID },
"net.sockSetTimeout": { argTypes: [NETSOCKET_T, F64], result: VOID },
"net.sockSetEncoding": { argTypes: [NETSOCKET_T, STRING], result: VOID },
"http.reqSetEncoding": { argTypes: [HTTPREQ_T, STRING], result: VOID },
"net.sockOnTimeout": { argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
"net.sockRemoteAddress": { argTypes: [NETSOCKET_T], result: VOID },
"net.sockEncrypted": { argTypes: [NETSOCKET_T], result: VOID },
// node:http, the client slice. The response callback shapes are
// program-dependent (checked below); header pairs arrive flat.
"http.request": { argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL], result: HTTPCLIENTREQ_T },
"http.requestCb": { argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, null], result: HTTPCLIENTREQ_T },
"http.agentNew": { argTypes: [BOOL, BOOL, F64, F64, F64, F64, F64], result: DYN },
"http.requestAgent": { argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, DYN], result: HTTPCLIENTREQ_T },
"http.requestAgentCb": { argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, DYN, null], result: HTTPCLIENTREQ_T },
"http.requestUrl": { argTypes: [STRING, STRING, BOOL], result: HTTPCLIENTREQ_T },
"http.requestUrlCb": { argTypes: [STRING, STRING, BOOL, null], result: HTTPCLIENTREQ_T },
"net.sockOnReadable": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID },
// sockRead's result is the interned `Buffer | null` union — checked
// specially below (the reqHeader/envGet pattern; VOID here is a
// placeholder the special case overrides).
"net.sockRead": { argTypes: [NETSOCKET_T, F64], result: VOID },
"net.sockUnshift": { argTypes: [NETSOCKET_T, BYTES_U8], result: VOID },
"net.sockPause": { argTypes: [NETSOCKET_T], result: NETSOCKET_T },
"net.sockResume": { argTypes: [NETSOCKET_T], result: NETSOCKET_T },
"net.sockSetNoDelay": { argTypes: [NETSOCKET_T, BOOL], result: NETSOCKET_T },
"net.sockDestroySoon": { argTypes: [NETSOCKET_T], result: VOID },
"net.sockBytesWritten": { argTypes: [NETSOCKET_T], result: F64 },
"net.sockReadable": { argTypes: [NETSOCKET_T], result: BOOL },
"net.sockOnFinish": { argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }], result: VOID },
"net.serverEmitConnection": { argTypes: [NETSERVER_T, NETSOCKET_T], result: VOID },
// tls/https: cert/key/ca PEM arguments are strings OR Buffers (null =
// both accepted; the emitter passes data+len either way).
"tls.createServer": { argTypes: [null, null], result: NETSERVER_T },
"tls.createServerCb": { argTypes: [null, null, null], result: NETSERVER_T },
// The runtime options records (divergence 66's stance): a dyn options
// value whose members read at runtime; pemDyn extracts a runtime-valued
// cert/key member (arg 1 is the precomposed fence label).
"tls.pemDyn": { argTypes: [DYN, STRING], result: BYTES_U8 },
"tls.createServerDyn": { argTypes: [DYN], result: NETSERVER_T },
"tls.createServerDynCb": { argTypes: [DYN, null], result: NETSERVER_T },
"https.createServerDyn": { argTypes: [DYN], result: NETSERVER_T },
"https.createServerDynCb": { argTypes: [DYN, null], result: NETSERVER_T },
"http2.createSecureServerReq": { argTypes: [null, null, null, BOOL], result: NETSERVER_T },
"http2.createSecureServerH2Req": { argTypes: [null, null, null, BOOL], result: NETSERVER_T },
"http2.createSecureServerDyn": { argTypes: [DYN], result: NETSERVER_T },
"http2.createSecureServerDynCb": { argTypes: [DYN, null], result: NETSERVER_T },
// tls.connect(port, host, opts[, cb]) — port -1 / host "" read the
// options record; the callback fires post-handshake (secureConnect).
"tls.connect": { argTypes: [F64, STRING, DYN], result: NETSOCKET_T },
"tls.connectCb": { argTypes: [F64, STRING, DYN, null], result: NETSOCKET_T },
// The TLSSocket member surface (authError's `string | null` union is
// shape-checked in the special cases below — the reqHeader pattern).
"tls.sockAuthorized": { argTypes: [NETSOCKET_T], result: BOOL },
"tls.sockAuthError": { argTypes: [NETSOCKET_T], result: VOID },
"tls.sockOnSecureConnect": { argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
"tls.sockOnSession": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID },
// createSecureContext({ cert, key }) mints the opaque SNI-answer handle.
"tls.createSecureContext": { argTypes: [null, null], result: SECURECTX_T },
"tls.createSecureContextDyn": { argTypes: [DYN], result: SECURECTX_T },
"tls.caCertsChk": { argTypes: [DYN, STRING], result: VOID },
// The CA-store introspection unit (scr_tls_ca.c): per-type cached PEM
// string arrays and the default-set replacement.
"tlsca.get": { argTypes: [STRING], result: arrayOf(STRING) },
"tlsca.root": { argTypes: [], result: arrayOf(STRING) },
"tlsca.set": { argTypes: [arrayOf(STRING)], result: VOID },
"https.createServer": { argTypes: [null, null, null], result: NETSERVER_T },
// http2's allowHTTP1 compatibility server (divergence 57): cert/key
// like tls.createServer; the 'request' handler arrives separately via
// http.serverOnRequest (shape checked in the libCall case, like
// http.createServer's). sessionError callback shape is checked below.
"http2.createSecureServer": { argTypes: [null, null, BOOL], result: NETSERVER_T },
// The SNI-callback form: arg 2 is the JS SNICallback closure — a
// `(servername, cb) => void` func, or its `| undefined` union from the
// conditional-spread spelling (the libCall case checks the shape).
"http2.createSecureServerSni": { argTypes: [null, null, null, BOOL], result: NETSERVER_T },
// The ALPN=h2 server (createSecureServer without allowHTTP1): the real
// h2 session machinery behind the TLS handshake.
"http2.createSecureServerH2": { argTypes: [null, null, BOOL], result: NETSERVER_T },
"http.serverOnRequest": { argTypes: [NETSERVER_T, null, BOOL], result: VOID },
"http2.serverOnSessionError": { argTypes: [NETSERVER_T, null, BOOL], result: VOID },
"http2.streamNoop": { argTypes: [], result: VOID },
"http2.streamUndefCall": { argTypes: [STRING], result: VOID },
// The REAL h2c surface (scr_http2.c). Callback slots are null (the
// emitter picks the thunk); pairs slots are string arrays; the
// endStream tri-state and event flags ride as f64.
"http2.createServer": { argTypes: [], result: NETSERVER_T },
"http2.createServerReq": { argTypes: [null], result: NETSERVER_T },
"http2.serverOnStream": { argTypes: [NETSERVER_T, null, BOOL], result: VOID },
"http2.serverOnSession": { argTypes: [NETSERVER_T, null, BOOL], result: VOID },
// connect(authority, reject, ca[, cb]): the TLS client knobs an https
// authority reads (inert on http ones, exactly Node); ca is a PEM
// string/Buffer ("" = the system anchors).
"http2.connect": { argTypes: [STRING, BOOL, null], result: HTTP2SESSION_T },
"http2.connectCb": { argTypes: [STRING, BOOL, null, null], result: HTTP2SESSION_T },
"http2.sessionRequest": { argTypes: [HTTP2SESSION_T, arrayOf(STRING), F64], result: HTTP2STREAM_T },
"http2.sessionClose": { argTypes: [HTTP2SESSION_T], result: VOID },
"http2.sessionCloseCb": { argTypes: [HTTP2SESSION_T, null], result: VOID },
"http2.sessionDestroy": { argTypes: [HTTP2SESSION_T], result: VOID },
"http2.sessionOnClose": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID },
"http2.sessionOnError": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID },
"http2.sessionOnConnect": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID },
"http2.sessionOnStream": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID },
"http2.sessionOnGoaway": { argTypes: [HTTP2SESSION_T, null, BOOL], result: VOID },
"http2.sessionClosed": { argTypes: [HTTP2SESSION_T], result: BOOL },
"http2.sessionDestroyed": { argTypes: [HTTP2SESSION_T], result: BOOL },
"http2.sessionEncrypted": { argTypes: [HTTP2SESSION_T], result: BOOL },
"http2.sessionType": { argTypes: [HTTP2SESSION_T], result: F64 },
"http2.sessionAlpn": { argTypes: [HTTP2SESSION_T], result: STRING },
"http2.sessionSocket": { argTypes: [HTTP2SESSION_T], result: NETSOCKET_T },
"http2.streamRespond": { argTypes: [HTTP2STREAM_T, arrayOf(STRING), BOOL], result: VOID },
"http2.streamWrite": { argTypes: [HTTP2STREAM_T, STRING], result: VOID },
"http2.streamWriteBytes": { argTypes: [HTTP2STREAM_T, BYTES_U8], result: VOID },
"http2.streamEnd": { argTypes: [HTTP2STREAM_T], result: VOID },
"http2.streamEndStr": { argTypes: [HTTP2STREAM_T, STRING], result: VOID },
"http2.streamEndBytes": { argTypes: [HTTP2STREAM_T, BYTES_U8], result: VOID },
"http2.streamClose": { argTypes: [HTTP2STREAM_T, F64], result: VOID },
"http2.streamCloseCb": { argTypes: [HTTP2STREAM_T, F64, null], result: VOID },
"http2.streamDestroy": { argTypes: [HTTP2STREAM_T], result: VOID },
"http2.sessionSettings0": { argTypes: [HTTP2SESSION_T], result: VOID },
"http2.sessionSettings": { argTypes: [HTTP2SESSION_T, DYN], result: VOID },
"http2.sessionSettingsDynCb": { argTypes: [HTTP2SESSION_T, DYN, DYN], result: VOID },
"http2.sessionSettingsCb0": { argTypes: [HTTP2SESSION_T, DYN, { kind: "func", params: [], ret: VOID }], result: VOID },
"http2.sessionOnSettingsDyn": { argTypes: [HTTP2SESSION_T, DYN, BOOL, BOOL], result: VOID },
"http2.sessionOnSettings0": { argTypes: [HTTP2SESSION_T, { kind: "func", params: [], ret: VOID }, BOOL, BOOL], result: VOID },
"http2.sessionSettingsGet": { argTypes: [HTTP2SESSION_T, BOOL], result: DYN },
"http2.sessionPendingSettingsAck": { argTypes: [HTTP2SESSION_T], result: BOOL },
"http2.getDefaultSettings": { argTypes: [], result: DYN },
"http2.streamSetEncoding": { argTypes: [HTTP2STREAM_T, STRING], result: VOID },
"http2.streamSetEncodingRet": { argTypes: [HTTP2STREAM_T, STRING], result: HTTP2STREAM_T },
"http2.streamResume": { argTypes: [HTTP2STREAM_T], result: VOID },
"http2.streamPause": { argTypes: [HTTP2STREAM_T], result: VOID },
"http2.streamOnData": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID },
"http2.streamOnEnd": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID },
"http2.streamOnClose": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID },
"http2.streamOnAborted": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID },
"http2.streamOnError": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID },
"http2.streamOnResponse": { argTypes: [HTTP2STREAM_T, null, BOOL], result: VOID },
"http2.streamId": { argTypes: [HTTP2STREAM_T], result: F64 },
"http2.streamRstCode": { argTypes: [HTTP2STREAM_T], result: F64 },
"http2.streamDestroyed": { argTypes: [HTTP2STREAM_T], result: BOOL },
"http2.streamClosed": { argTypes: [HTTP2STREAM_T], result: BOOL },
"http2.streamAborted": { argTypes: [HTTP2STREAM_T], result: BOOL },
"http2.streamPending": { argTypes: [HTTP2STREAM_T], result: BOOL },
"http2.streamSession": { argTypes: [HTTP2STREAM_T], result: HTTP2SESSION_T },
// The createConnection forms: arg 0 is the dialer closure (() =>
// Socket — the libCall case checks), then path/method/timeout/headers/
// autoEnd like http.request.
"http.requestConn": { argTypes: [null, STRING, STRING, F64, arrayOf(STRING), BOOL], result: HTTPCLIENTREQ_T },
"http.requestConnCb": { argTypes: [null, STRING, STRING, F64, arrayOf(STRING), BOOL, null], result: HTTPCLIENTREQ_T },
"https.request": { argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null], result: HTTPCLIENTREQ_T },
"https.requestCb": { argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, null], result: HTTPCLIENTREQ_T },
"https.requestUrl": { argTypes: [STRING, STRING, BOOL], result: HTTPCLIENTREQ_T },
"https.requestUrlCb": { argTypes: [STRING, STRING, BOOL, null], result: HTTPCLIENTREQ_T },
"https.requestAgent": { argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, DYN], result: HTTPCLIENTREQ_T },
"https.requestAgentCb": { argTypes: [STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, DYN, null], result: HTTPCLIENTREQ_T },
// The requestFn binding's runtime-secure rows: https.request's shape
// with the leading `secure` bool.
"https.requestFn": { argTypes: [BOOL, STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null], result: HTTPCLIENTREQ_T },
"https.requestFnCb": { argTypes: [BOOL, STRING, F64, STRING, STRING, F64, arrayOf(STRING), BOOL, BOOL, null, null], result: HTTPCLIENTREQ_T },
"http.clientWrite": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID },
"http.clientSetHeader": { argTypes: [HTTPCLIENTREQ_T, STRING, STRING], result: VOID },
"http.clientGetHeader": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID },
"http.clientHasHeader": { argTypes: [HTTPCLIENTREQ_T, STRING], result: BOOL },
"http.clientRemoveHeader": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID },
"http.clientGetHeaderNames": { argTypes: [HTTPCLIENTREQ_T], result: arrayOf(STRING) },
"http.clientGetRawHeaderNames": { argTypes: [HTTPCLIENTREQ_T], result: arrayOf(STRING) },
"http.clientGetHeaders": { argTypes: [HTTPCLIENTREQ_T], result: DYN },
"http.clientWriteBytes": { argTypes: [HTTPCLIENTREQ_T, BYTES_U8], result: VOID },
"http.clientEnd": { argTypes: [HTTPCLIENTREQ_T], result: VOID },
"http.clientEndStr": { argTypes: [HTTPCLIENTREQ_T, STRING], result: VOID },
"http.clientEndBytes": { argTypes: [HTTPCLIENTREQ_T, BYTES_U8], result: VOID },
"http.clientWriteDyn": { argTypes: [HTTPCLIENTREQ_T, DYN], result: VOID },
"http.clientEndDyn": { argTypes: [HTTPCLIENTREQ_T, DYN], result: VOID },
"http.clientFlushHeaders": { argTypes: [HTTPCLIENTREQ_T], result: VOID },
"http.clientAddTrailers": { argTypes: [HTTPCLIENTREQ_T, arrayOf(STRING)], result: VOID },
"http.clientCork": { argTypes: [HTTPCLIENTREQ_T], result: VOID },
"http.clientUncork": { argTypes: [HTTPCLIENTREQ_T], result: VOID },
"http.clientWritableCorked": { argTypes: [HTTPCLIENTREQ_T], result: F64 },
"http.clientMethod": { argTypes: [HTTPCLIENTREQ_T], result: STRING },
"http.clientPath": { argTypes: [HTTPCLIENTREQ_T], result: STRING },
"http.clientHost": { argTypes: [HTTPCLIENTREQ_T], result: STRING },
"http.clientProtocol": { argTypes: [HTTPCLIENTREQ_T], result: STRING },
"http.clientHeadersSent": { argTypes: [HTTPCLIENTREQ_T], result: BOOL },
"http.clientWritableEnded": { argTypes: [HTTPCLIENTREQ_T], result: BOOL },
"http.clientWritableFinished": { argTypes: [HTTPCLIENTREQ_T], result: BOOL },
"http.clientSocket": { argTypes: [HTTPCLIENTREQ_T], result: NETSOCKET_T },
"http.clientReusedSocket": { argTypes: [HTTPCLIENTREQ_T], result: BOOL },
"http.clientSetNoDelay": { argTypes: [HTTPCLIENTREQ_T, BOOL], result: VOID },
"http.clientSetSocketKeepAlive": { argTypes: [HTTPCLIENTREQ_T, BOOL, F64], result: VOID },
"http.clientSetTimeout": { argTypes: [HTTPCLIENTREQ_T, F64], result: VOID },
"http.clientSetTimeoutCb": { argTypes: [HTTPCLIENTREQ_T, F64, { kind: "func", params: [], ret: VOID }], result: VOID },
"http.statusCodes": { argTypes: [], result: DYN },
"http.methods": { argTypes: [], result: arrayOf(STRING) },
"http.reqSetTimeout": { argTypes: [HTTPREQ_T, F64], result: VOID },
"http.reqSetTimeoutCb": { argTypes: [HTTPREQ_T, F64, { kind: "func", params: [], ret: VOID }], result: VOID },
"http.clientDestroy": { argTypes: [HTTPCLIENTREQ_T], result: VOID },
"http.clientAbort": { argTypes: [HTTPCLIENTREQ_T], result: VOID },
"http.clientAborted": { argTypes: [HTTPCLIENTREQ_T], result: BOOL },
"http.clientDestroyed": { argTypes: [HTTPCLIENTREQ_T], result: BOOL },
"http.clientOnResponse": { argTypes: [HTTPCLIENTREQ_T, null, BOOL], result: VOID },
"http.clientOnSocket": { argTypes: [HTTPCLIENTREQ_T, null, BOOL], result: VOID },
"http.clientOnFinish": { argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
"http.clientOnError": { argTypes: [HTTPCLIENTREQ_T, null, BOOL], result: VOID },
"http.clientOnTimeout": { argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
"http.clientOnClose": { argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
"http.clientOnAbort": { argTypes: [HTTPCLIENTREQ_T, { kind: "func", params: [], ret: VOID }, BOOL], result: VOID },
"cp.execSync": {
argTypes: [STRING, arrayOf(STRING), BOOL, STRING, BOOL, STRING, BOOL, arrayOf(STRING), F64, F64, F64],
result: STRING,
},
"cp.execCapture": {
argTypes: [STRING, arrayOf(STRING), STRING, BOOL, arrayOf(STRING), F64],
result: SPAWNRES_T,
},
"cp.spawn": { argTypes: [STRING, arrayOf(STRING)], result: CHILD_T },
"process.forkTarget": { argTypes: [F64], result: F64 },
"cp.fork": {
argTypes: [F64, arrayOf(STRING), F64, F64, F64, BOOL, arrayOf(STRING), STRING],
result: CHILD_T,
},
"child.connected": { argTypes: [CHILD_T], result: BOOL },
"child.send": { argTypes: [CHILD_T, STRING], result: BOOL },
"child.sendCb": { argTypes: [CHILD_T, STRING, null], result: BOOL },
"child.disconnect": { argTypes: [CHILD_T], result: VOID },
"child.onMessage": { argTypes: [CHILD_T, null, BOOL], result: VOID },
"child.onDisconnect": { argTypes: [CHILD_T, null, BOOL], result: VOID },
"process.connected": { argTypes: [], result: BOOL },
"process.send": { argTypes: [STRING], result: BOOL },
"process.sendCb": { argTypes: [STRING, null], result: BOOL },
"process.disconnect": { argTypes: [], result: VOID },
"process.onMessage": { argTypes: [null, BOOL], result: VOID },
"process.onDisconnect": { argTypes: [null, BOOL], result: VOID },
"cp.execFile": { argTypes: [STRING, arrayOf(STRING), null], result: CHILD_T },
// spawn's options form: per-slot stdio modes (0 ignore / 1 inherit /
// 2 fd) with the out/err fds for mode 2, detached, env replacement
// pairs, cwd ("" = inherit).
"cp.spawnOpts": {
argTypes: [STRING, arrayOf(STRING), F64, F64, F64, F64, F64, BOOL, BOOL, BOOL, arrayOf(STRING), STRING],
result: CHILD_T,
},
// The callback's func type is program-dependent (zero params, or the
// `number | null` union / the %Error class) — the libCall case checks
// the shape; the slot here only pins arity and the child receiver.
"child.onExit": { argTypes: [CHILD_T, null], result: VOID },
"child.onClose": { argTypes: [CHILD_T, null], result: VOID },
"child.onError": { argTypes: [CHILD_T, null], result: VOID },
// Like process.envGet, spawnRes.status's result type is program-dependent
// (the interned `number | null` union) — the libCall case checks the arms.
"spawnRes.status": { argTypes: [SPAWNRES_T], result: VOID },
// spawnRes.error's result is the interned `Error | undefined` union —
// the libCall case checks the arms, the spawnRes.status pattern.
"spawnRes.error": { argTypes: [SPAWNRES_T], result: VOID },
// spawnRes.signal's result is the interned `string | null` union —
// same pattern.
"spawnRes.signal": { argTypes: [SPAWNRES_T], result: VOID },
// The ChildProcess lifecycle members: pid/exitCode are program-dependent
// unions (`number | undefined` / `number | null` — the libCall case
// checks the arms, the spawnRes.status pattern).
"child.pid": { argTypes: [CHILD_T], result: VOID },
"child.exitCode": { argTypes: [CHILD_T], result: VOID },
// The close-override pair: the bound close's result func type and the
// override's func argument are program-dependent (the callback union's
// tags) — the slots pin the server receiver; the libCall case checks
// the bound value's shape.
"net.serverCloseBind": { argTypes: [NETSERVER_T], result: VOID },
"net.serverSetCloseOverride": { argTypes: [NETSERVER_T, null], result: VOID },
// The piped-output stream reads: `Readable | null` unions (the libCall
// case checks the arms — the child.pid pattern with a ref arm).
"child.stdout": { argTypes: [CHILD_T], result: VOID },
"child.stderr": { argTypes: [CHILD_T], result: VOID },
"child.stdin": { argTypes: [CHILD_T], result: VOID },
// Listener registrations: callback func shapes are program-dependent
// (zero-param / Buffer / Buffer-armed union) — the slots pin arity,
// the stream receiver, and the once-flag.
"stream.onData": { argTypes: [CHILDSTREAM_T, null, BOOL], result: VOID },
"stream.onDataStr": { argTypes: [CHILDSTREAM_T, null, BOOL], result: VOID },
"stream.onEnd": { argTypes: [CHILDSTREAM_T, null, BOOL], result: VOID },
"stream.childSetEncoding": { argTypes: [CHILDSTREAM_T, STRING], result: CHILDSTREAM_T },
"writer.writeString": { argTypes: [CHILDWRITER_T, STRING], result: BOOL },
"writer.writeBytes": { argTypes: [CHILDWRITER_T, BYTES_U8], result: BOOL },
"writer.end": { argTypes: [CHILDWRITER_T], result: VOID },
"writer.destroy": { argTypes: [CHILDWRITER_T], result: VOID },
"writer.writable": { argTypes: [CHILDWRITER_T], result: BOOL },
"writer.onDrain": { argTypes: [CHILDWRITER_T, null, BOOL], result: VOID },
"writer.onFinish": { argTypes: [CHILDWRITER_T, null, BOOL], result: VOID },
"writer.onError": { argTypes: [CHILDWRITER_T, null, BOOL], result: VOID },
"procStream.write": { argTypes: [PROCSTREAM_T, STRING], result: BOOL },
"child.killed": { argTypes: [CHILD_T], result: BOOL },
"child.kill": { argTypes: [CHILD_T, STRING], result: BOOL },
"child.killNum": { argTypes: [CHILD_T, F64], result: BOOL },
"child.unref": { argTypes: [CHILD_T], result: VOID },
"spawnRes.stdout": { argTypes: [SPAWNRES_T], result: STRING },
"spawnRes.stderr": { argTypes: [SPAWNRES_T], result: STRING },
"crypto.randomUUID": { argTypes: [], result: STRING },
"crypto.randomBytesToString": { argTypes: [F64, STRING], result: STRING },
"crypto.randomBytes": { argTypes: [F64], result: BYTES_U8 },
"crypto.hashDigestStr": { argTypes: [STRING, STRING, STRING], result: STRING },
"crypto.hashDigestBytes": { argTypes: [STRING, BYTES_U8, STRING], result: STRING },
"crypto.hashNew": { argTypes: [STRING], result: CRYPTOHASH_T },
"crypto.hmacNewStr": { argTypes: [STRING, STRING], result: CRYPTOHMAC_T },
"crypto.hmacNewBytes": { argTypes: [STRING, BYTES_U8], result: CRYPTOHMAC_T },
"crypto.hashUpdateStr": { argTypes: [CRYPTOHASH_T, STRING], result: CRYPTOHASH_T },
"crypto.hashUpdateBytes": { argTypes: [CRYPTOHASH_T, BYTES_U8], result: CRYPTOHASH_T },
"crypto.hmacUpdateStr": { argTypes: [CRYPTOHMAC_T, STRING], result: CRYPTOHMAC_T },
"crypto.hmacUpdateBytes": { argTypes: [CRYPTOHMAC_T, BYTES_U8], result: CRYPTOHMAC_T },
"crypto.hashCopy": { argTypes: [CRYPTOHASH_T], result: CRYPTOHASH_T },
"crypto.hashDigestString": { argTypes: [CRYPTOHASH_T, STRING], result: STRING },
"crypto.hashDigestBuffer": { argTypes: [CRYPTOHASH_T], result: BYTES_U8 },
"crypto.hmacDigestString": { argTypes: [CRYPTOHMAC_T, STRING], result: STRING },
"crypto.hmacDigestBuffer": { argTypes: [CRYPTOHMAC_T], result: BYTES_U8 },
"crypto.timingSafeEqual": { argTypes: [BYTES_U8, BYTES_U8], result: BOOL },
"crypto.randomFill": { argTypes: [BYTES_U8, F64, F64], result: BYTES_U8 },
"crypto.randomFillRest": { argTypes: [BYTES_U8, F64], result: BYTES_U8 },
"crypto.randomInt": { argTypes: [F64, F64], result: F64 },
"crypto.pbkdf2": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING], result: BYTES_U8 },
"crypto.randomBytesCb": { argTypes: [F64, null], result: VOID },
"crypto.pbkdf2Cb": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING, null], result: VOID },
// The Buffer statics and the fs/zlib Buffer forms: fixed always-u8
// signatures (Buffer IS a Uint8Array — one bytes kind).
"buffer.fromStr": { argTypes: [STRING, STRING], result: BYTES_U8 },
"buffer.concat": { argTypes: [arrayOf(BYTES_U8)], result: BYTES_U8 },
"buffer.byteLenStr": { argTypes: [STRING, STRING], result: F64 },
"buffer.isEncoding": { argTypes: [STRING], result: BOOL },
"buffer.concatLen": { argTypes: [arrayOf(BYTES_U8), F64], result: BYTES_U8 },
// The checked-dynamic compare/equals validators (Node's argument
// ladders over dyn-boxed invalid-input probes).
"buffer.compareChk": { argTypes: [DYN, DYN], result: F64 },
"bytes.equalsChk": { argTypes: [BYTES_U8, DYN], result: BOOL },
"bytes.compareChk": { argTypes: [BYTES_U8, DYN, DYN, DYN, DYN, DYN], result: F64 },
"buffer.newStringFail": { argTypes: [DYN], result: BYTES_U8 },
"fs.toUnixTimestamp": { argTypes: [DYN], result: F64 },
"fs.existsChk": { argTypes: [DYN, DYN], result: DYN },
"fs.mkdtempChk": { argTypes: [DYN, DYN, STRING], result: VOID },
"fs.mkdtempSyncChk": { argTypes: [DYN, DYN, STRING], result: STRING },
"fs.readFileChk": { argTypes: [DYN, DYN, DYN, STRING], result: VOID },
"fs.opendirChk": { argTypes: [DYN, DYN, STRING], result: VOID },
"fs.watchFileChk": { argTypes: [DYN, DYN, STRING], result: VOID },
"fs.lchmodChk": { argTypes: [DYN, DYN, DYN, STRING], result: VOID },
"fs.lchmodSyncChk": { argTypes: [DYN, DYN], result: DYN },
"fsp.lchmodChk": { argTypes: [DYN, DYN], result: { kind: "promise", inner: VOID } },
"fs.readChk": { argTypes: [DYN, DYN, DYN, DYN, DYN, STRING], result: VOID },
"fs.streamOptsChk": { argTypes: [DYN, DYN, STRING], result: VOID },
"error.argTypeThrow": { argTypes: [STRING, STRING, DYN], result: VOID },
"error.propTypeThrow": { argTypes: [STRING, STRING, DYN], result: VOID },
"emitter.setMaxChk": { argTypes: [null, DYN], result: VOID },
"emitter.setDefaultMaxChk": { argTypes: [DYN, STRING], result: VOID },
"fs.readFileSyncBytes": { argTypes: [STRING], result: BYTES_U8 },
"fs.writeFileSyncBytes": { argTypes: [STRING, BYTES_U8], result: VOID },
"fsp.readFileBytes": { argTypes: [STRING], result: { kind: "promise", inner: BYTES_U8 } },
"zlib.deflateSync": { argTypes: [BYTES_U8], result: BYTES_U8 },
"zlib.inflateSync": { argTypes: [BYTES_U8], result: BYTES_U8 },
"zlib.deflateRawSync": { argTypes: [BYTES_U8], result: BYTES_U8 },
"zlib.inflateRawSync": { argTypes: [BYTES_U8], result: BYTES_U8 },
"zlib.gzipSync": { argTypes: [BYTES_U8], result: BYTES_U8 },
"zlib.gunzipSync": { argTypes: [BYTES_U8], result: BYTES_U8 },
"zlib.unzipSync": { argTypes: [BYTES_U8], result: BYTES_U8 },
"zlib.deflateCb": { argTypes: [BYTES_U8, null], result: VOID },
"zlib.inflateCb": { argTypes: [BYTES_U8, null], result: VOID },
"zlib.deflateRawCb": { argTypes: [BYTES_U8, null], result: VOID },
"zlib.inflateRawCb": { argTypes: [BYTES_U8, null], result: VOID },
"zlib.gzipCb": { argTypes: [BYTES_U8, null], result: VOID },
"zlib.gunzipCb": { argTypes: [BYTES_U8, null], result: VOID },
"zlib.unzipCb": { argTypes: [BYTES_U8, null], result: VOID },
"zlib.crc32": { argTypes: [BYTES_U8, F64], result: F64 },
"process.stdoutWriteBytes": { argTypes: [BYTES_U8, STRING], result: BOOL },
"process.stderrWriteBytes": { argTypes: [BYTES_U8, STRING], result: BOOL },
// Completion callback is program-dependent: zero params, checked-dynamic,
// or an optional Error | null slot (same success shape as fs.rename).
"process.stdoutWriteBytesCb": { argTypes: [BYTES_U8, STRING, null], result: BOOL },
"process.stderrWriteBytesCb": { argTypes: [BYTES_U8, STRING, null], result: BOOL },
"fsp.readFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: STRING } },
"fsp.writeFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } },
"fsp.writeFileMode": { argTypes: [STRING, STRING, F64], result: { kind: "promise", inner: VOID } },
"fsp.mkdir": { argTypes: [STRING], result: { kind: "promise", inner: VOID } },
"fsp.mkdirMode": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } },
"fsp.mkdirRecursive": { argTypes: [STRING], result: { kind: "promise", inner: VOID } },
"fsp.mkdirRecursiveMode": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } },
"fsp.unlink": { argTypes: [STRING], result: { kind: "promise", inner: VOID } },
"fsp.chmod": { argTypes: [STRING, F64], result: { kind: "promise", inner: VOID } },
"fsp.rename": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } },
"fsp.readdir": { argTypes: [STRING], result: { kind: "promise", inner: arrayOf(STRING) } },
// Result is promise<call-site Dirent record array>; the libCall case
// validates the program-dependent record shape below.
"fsp.readdirTypes": { argTypes: [STRING], result: { kind: "promise", inner: VOID } },
"fsp.rm": { argTypes: [STRING], result: { kind: "promise", inner: VOID } },
"fsp.stat": { argTypes: [STRING], result: { kind: "promise", inner: STATS_T } },
"fsp.realpath": { argTypes: [STRING], result: { kind: "promise", inner: STRING } },
"fsp.lstat": { argTypes: [STRING], result: { kind: "promise", inner: STATS_T } },
"fsp.open": { argTypes: [STRING, STRING, F64], result: { kind: "promise", inner: FILEHANDLE_T } },
"fileHandle.fd": { argTypes: [FILEHANDLE_T], result: F64 },
"fileHandle.close": { argTypes: [FILEHANDLE_T], result: { kind: "promise", inner: VOID } },
// read/write carry call-site result record shapes; the validator checks
// those below, so promise<void> is only a table sentinel.
"fileHandle.read": { argTypes: [FILEHANDLE_T, BYTES_U8, F64, F64, F64, BOOL], result: { kind: "promise", inner: VOID } },
"fileHandle.writeBytes": { argTypes: [FILEHANDLE_T, BYTES_U8, F64, F64, F64, BOOL], result: { kind: "promise", inner: VOID } },
"fileHandle.writeStr": { argTypes: [FILEHANDLE_T, STRING, F64, STRING], result: { kind: "promise", inner: VOID } },
"fileHandle.readFile": { argTypes: [FILEHANDLE_T, STRING], result: { kind: "promise", inner: STRING } },
"fileHandle.readFileBytes": { argTypes: [FILEHANDLE_T, STRING], result: { kind: "promise", inner: BYTES_U8 } },
"fileHandle.writeFile": { argTypes: [FILEHANDLE_T, STRING, STRING], result: { kind: "promise", inner: VOID } },
"fileHandle.writeFileBytes": { argTypes: [FILEHANDLE_T, BYTES_U8, STRING], result: { kind: "promise", inner: VOID } },
"fileHandle.stat": { argTypes: [FILEHANDLE_T], result: { kind: "promise", inner: STATS_T } },
"process.argv": { argTypes: [], result: arrayOf(STRING) },
"process.platform": { argTypes: [], result: STRING },
// The one libCall whose result type is program-dependent (union ids are
// per-module): the `result` here is a placeholder — the libCall case
// checks the union's ARMS ([string, undefinedT] in canonical order)
// against the module's registry instead.
"process.envGet": { argTypes: [STRING], result: VOID },
"process.envSet": { argTypes: [STRING, STRING], result: VOID },
"process.envUnset": { argTypes: [STRING], result: VOID },
"process.envPairs": { argTypes: [], result: arrayOf(STRING) },
"process.exit": { argTypes: [F64], result: VOID },
"process.setExitCode": { argTypes: [F64], result: VOID },
"process.currentExitCode": { argTypes: [], result: F64 },
"process.cwd": { argTypes: [], result: STRING },
"process.pid": { argTypes: [], result: F64 },
"dyn.this": { argTypes: [], result: DYN },
"process.getuid": { argTypes: [], result: F64 },
"process.getgid": { argTypes: [], result: F64 },
"process.execPath": { argTypes: [], result: STRING },
"process.arch": { argTypes: [], result: STRING },
"process.versionsNode": { argTypes: [], result: STRING },
"process.versionsOpenssl": { argTypes: [], result: STRING },
"process.kill": { argTypes: [F64, STRING], result: BOOL },
"process.killNum": { argTypes: [F64, F64], result: BOOL },
"process.stdoutWrite": { argTypes: [STRING], result: BOOL },
"process.stderrWrite": { argTypes: [STRING], result: BOOL },
// error.new's result and the receiver slots are builtin-error classes —
// program-dependent object types, checked in the libCall case.
"error.new": { argTypes: [STRING], result: VOID },
"error.nodeThrow": { argTypes: [F64, STRING, STRING], result: VOID },
"dyn.toStringCoerce": { argTypes: [DYN], result: STRING },
"dyn.toNumberCoerce": { argTypes: [DYN], result: F64 },
// Always throws; the result is the READ's declared type (a typed dummy
// the unwind abandons) — the libCall case skips the result check.
"global.undefRead": { argTypes: [STRING], result: VOID },
// `X.name` through a class value: the arg is a program-dependent
// classval (a null slot; the libCall case checks the kind).
"class.name": { argTypes: [null], result: STRING },
"error.ctor": { argTypes: [null, STRING], result: VOID },
"error.toString": { argTypes: [null], result: STRING },
// Receiver (any error-hierarchy object) and the program-dependent
// `string | undefined` result are checked in the libCall case.
"error.code": { argTypes: [null], result: VOID },
// DOMException: construction takes the two dyn args (WebIDL's
// resolution runs in the runtime); the %DOMException result is a
// program-dependent object type, checked in the libCall case. The
// read surface takes the %DOMException receiver (a null slot — the
// libCall case checks the class name).
"error.newDom": { argTypes: [DYN, DYN], result: VOID },
"error.domCode": { argTypes: [null], result: F64 },
"error.domHasCause": { argTypes: [null], result: BOOL },
"error.domCause": { argTypes: [null], result: DYN },
"error.domClone": { argTypes: [null, DYN], result: VOID },
"dyn.errInstanceof": { argTypes: [DYN, F64], result: BOOL },
"dyn.objKeys": { argTypes: [DYN], result: DYN },
"dyn.hasOwn": { argTypes: [DYN, STRING], result: BOOL },
"dyn.assign": { argTypes: [DYN, DYN], result: DYN },
"dyn.packPush": { argTypes: [DYN, DYN], result: VOID },
"dyn.packPushSpread": { argTypes: [DYN, DYN, STRING], result: VOID },
"dyn.packPushSpreadIter": { argTypes: [DYN, DYN], result: VOID },
"dyn.assignAll": { argTypes: [DYN, DYN], result: DYN },
"dyn.objCreateNullProto": { argTypes: [], result: DYN },
"dyn.objValues": { argTypes: [DYN], result: DYN },
"dyn.objEntries": { argTypes: [DYN], result: DYN },
"dyn.structuredClone": { argTypes: [DYN, DYN], result: DYN },
"dyn.cloneMissing": { argTypes: [], result: DYN },
"dyn.cloneTransferFail": { argTypes: [], result: DYN },
"regex.new": { argTypes: [STRING, STRING], result: REGEX },
// node:events EventEmitter: receivers are emitter-hierarchy objects and
// the chaining forms (on/off/removeAll/setMax) return the receiver's
// own class — program-dependent object types, checked in the libCall
// case. emitter.emit is the one VARIADIC libCall: args beyond
// (recv, name) are the event's frontend-unified tuple, any borrowable
// kind — the count check admits a longer list for it alone.
"emitter.new": { argTypes: [], result: VOID },
"emitter.ctor": { argTypes: [null], result: VOID },
"emitter.on": { argTypes: [null, STRING, null, BOOL, BOOL], result: VOID },
"emitter.off": { argTypes: [null, STRING, null], result: VOID },
// The dyn-adapted registration family (JS-lane checked-dynamic
// listeners): checkListener has NO receiver (it validates the listener
// argument alone); onDyn carries (recv, name, cb dyn, adapter func,
// once, prepend), offDyn (recv, name, cb dyn).
"emitter.checkListener": { argTypes: [DYN], result: VOID },
"emitter.onDyn": { argTypes: [null, STRING, DYN, null, BOOL, BOOL], result: VOID },
"emitter.onFlex": { argTypes: [null, STRING, DYN, BOOL, BOOL], result: VOID },
"emitter.offDyn": { argTypes: [null, STRING, DYN], result: VOID },
"emitter.removeAll": { argTypes: [null, STRING, BOOL], result: VOID },
"emitter.emit": { argTypes: [null, STRING], result: BOOL },
"emitter.emitFlex": { argTypes: [null, STRING], result: BOOL },
"emitter.emitError": { argTypes: [null, STRING, null], result: BOOL },
"emitter.count": { argTypes: [null, STRING], result: F64 },
"emitter.countFn": { argTypes: [null, STRING, null], result: F64 },
"emitter.names": { argTypes: [null], result: VOID },
"emitter.listeners": { argTypes: [null, STRING], result: VOID },
"emitter.onData": { argTypes: [null, STRING, null, BOOL, BOOL], result: VOID },
"emitter.onDataDyn": { argTypes: [null, STRING, DYN, null, BOOL, BOOL], result: VOID },
"emitter.emitData": { argTypes: [null, STRING, null], result: BOOL },
"emitter.setMax": { argTypes: [null, F64], result: VOID },
"emitter.getMax": { argTypes: [null], result: F64 },
"emitter.setDefaultMax": { argTypes: [F64], result: VOID },
"emitter.getDefaultMax": { argTypes: [], result: F64 },
// node:stream: receivers are stream-hierarchy objects and several
// results are program-dependent (the receiver's class, unions) — the
// libCall case owns those checks. The constructors and a few methods
// are VARIADIC (trailing option callbacks / the optional chunk+cb tail):
// argTypes here is the fixed prefix, VARIADIC_LIB_FNS admits the rest.
"readable.new": { argTypes: [F64, BOOL, BOOL, F64], result: VOID },
"writable.new": { argTypes: [F64, BOOL, BOOL, F64], result: VOID },
"duplex.new": { argTypes: [F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID },
"transform.new": { argTypes: [F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID },
"passthrough.new": { argTypes: [F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID },
// The subclass-initialization twins: the borrowed receiver leads, then
// the .new tail (variadic callbacks likewise).
// The dyn-options twins: newDyn takes the record, initDyn the borrowed
// receiver + record + fallback flags (trailing wrapper closures ride
// the variadic tail like the static forms).
"stream.setRead": { argTypes: [null, null], result: VOID },
"stream.setWrite": { argTypes: [null, null], result: VOID },
"stream.setFinal": { argTypes: [null, null], result: VOID },
"stream.setDestroy": { argTypes: [null, null], result: VOID },
"stream.setTransform": { argTypes: [null, null], result: VOID },
"stream.setFlush": { argTypes: [null, null], result: VOID },
"stream.finished": { argTypes: [null, null], result: VOID },
"stream.finishedDyn": { argTypes: [null, DYN], result: VOID },
"stream.pipeline": { argTypes: [F64], result: VOID },
"stream.pipelineDyn": { argTypes: [F64], result: VOID },
"sp.finished": { argTypes: [null], result: { kind: "promise", inner: VOID } },
"sp.pipeline": { argTypes: [F64], result: { kind: "promise", inner: VOID } },
"sc.text": { argTypes: [null], result: { kind: "promise", inner: STRING } },
"sc.json": { argTypes: [null], result: { kind: "promise", inner: DYN } },
"sc.buffer": { argTypes: [null], result: { kind: "promise", inner: BYTES_U8 } },
"readable.newDyn": { argTypes: [DYN], result: VOID },
"writable.newDyn": { argTypes: [DYN], result: VOID },
"duplex.newDyn": { argTypes: [DYN], result: VOID },
"transform.newDyn": { argTypes: [DYN], result: VOID },
"passthrough.newDyn": { argTypes: [DYN], result: VOID },
"readable.initDyn": { argTypes: [null, DYN, F64], result: VOID },
"writable.initDyn": { argTypes: [null, DYN, F64], result: VOID },
"duplex.initDyn": { argTypes: [null, DYN, F64], result: VOID },
"transform.initDyn": { argTypes: [null, DYN, F64], result: VOID },
"passthrough.initDyn": { argTypes: [null, DYN, F64], result: VOID },
"readable.init": { argTypes: [null, F64, BOOL, BOOL, F64], result: VOID },
"writable.init": { argTypes: [null, F64, BOOL, BOOL, F64], result: VOID },
"duplex.init": { argTypes: [null, F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID },
"transform.init": { argTypes: [null, F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID },
"passthrough.init": { argTypes: [null, F64, F64, BOOL, BOOL, BOOL, BOOL, BOOL, F64], result: VOID },
"readable.push": { argTypes: [null, BYTES_U8], result: BOOL },
"readable.pushStr": { argTypes: [null, STRING], result: BOOL },
"readable.pushNull": { argTypes: [null], result: BOOL },
"readable.pushU": { argTypes: [null, null], result: BOOL },
"readable.pushDyn": { argTypes: [null, DYN], result: BOOL },
"readable.unshift": { argTypes: [null, BYTES_U8], result: VOID },
"readable.unshiftStr": { argTypes: [null, STRING], result: VOID },
"readable.read": { argTypes: [null, F64], result: VOID },
"readable.pause": { argTypes: [null], result: VOID },
"readable.setEncoding": { argTypes: [null, STRING], result: VOID },
"readable.pushStrEnc": { argTypes: [null, STRING, STRING], result: BOOL },
"readable.pushEncoding": { argTypes: [null, STRING], result: VOID },
"readable.nextChunk": { argTypes: [null], result: VOID },
"readable.nextChunkDyn": { argTypes: [null], result: VOID },
"readable.fromArr": { argTypes: [null, BOOL], result: VOID },
"readable.resume": { argTypes: [null], result: VOID },
"readable.isPaused": { argTypes: [null], result: BOOL },
"readable.pipe": { argTypes: [null, null, BOOL], result: VOID },
"readable.unpipe": { argTypes: [null], result: VOID },
"readable.flowing": { argTypes: [null], result: VOID },
"writable.write": { argTypes: [null, BYTES_U8], result: BOOL },
"writable.writeStr": { argTypes: [null, STRING], result: BOOL },
"writable.writeU": { argTypes: [null, null], result: BOOL },
"writable.writeDyn": { argTypes: [null, DYN], result: BOOL },
"writable.end": { argTypes: [null, F64], result: VOID },
"writable.cork": { argTypes: [null], result: VOID },
"writable.uncork": { argTypes: [null], result: VOID },
"stream.destroy": { argTypes: [null], result: VOID },
"stream.destroyErr": { argTypes: [null, null], result: VOID },
"stream.iteratorClose": { argTypes: [null], result: VOID },
"stream.prop": { argTypes: [null, STRING], result: VOID },
"stream.errored": { argTypes: [null], result: VOID },
// node:assert: pass/negated/deep/hasMsg are frontend-computed bools; the
// message slot always carries a string ("" when hasMsg is false).
"assert.ok": { argTypes: [BOOL, STRING], result: VOID },
"assert.eqF64": { argTypes: [F64, F64, BOOL, BOOL, STRING, BOOL], result: VOID },
"assert.eqBigInt": { argTypes: [BIGINT_T, BIGINT_T, BOOL, BOOL, STRING, BOOL], result: VOID },
"assert.eqStr": { argTypes: [STRING, STRING, BOOL, BOOL, STRING, BOOL], result: VOID },
"assert.eqBool": { argTypes: [BOOL, BOOL, BOOL, BOOL, STRING, BOOL], result: VOID },
"assert.deepResult": { argTypes: [BOOL, BOOL, STRING, BOOL], result: VOID },
"assert.sameValue": { argTypes: [F64, F64], result: BOOL },
// The deep-equality pair memo: both slots are program-dependent
// (any cycle-capable record/array/map type).
"assert.deqEnter": { argTypes: [null, null], result: BOOL },
"assert.deqLeave": { argTypes: [], result: VOID },
"assert.match": { argTypes: [STRING, REGEX, BOOL, STRING, BOOL], result: VOID },
"assert.throwsNone": { argTypes: [BOOL, STRING, BOOL, STRING, BOOL], result: VOID },
"assert.throwsMismatch": { argTypes: [STRING, { kind: "object", className: "%Error" }, STRING, BOOL], result: VOID },
"assert.throwsRegex": { argTypes: [REGEX, { kind: "object", className: "%Error" }, STRING, BOOL], result: VOID },
// Symbol strict equality (pointer identity; scr_symbol.c).
"assert.eqSym": { argTypes: [{ kind: "symbol" }, { kind: "symbol" }, BOOL, BOOL, STRING, BOOL], result: VOID },
// The equality quartet over checked-dynamic operands (the frontend
// boxes a static side into the checked-dynamic tree first).
"assert.eqDyn": { argTypes: [DYN, DYN, BOOL, BOOL, STRING, BOOL], result: VOID },
"assert.looseResult": { argTypes: [BOOL, BOOL, STRING, STRING, STRING, BOOL], result: VOID },
// The throws(fn, {shape}) accumulator: begin/slot calls never throw;
// shapeEnd throws the Comparison diff. The error slot is the
// %Error-narrowed caught value.
"assert.shapeBegin": { argTypes: [{ kind: "object", className: "%Error" }], result: VOID },
"assert.shapeStr": { argTypes: [F64, STRING], result: VOID },
"assert.shapeRe": { argTypes: [F64, REGEX], result: VOID },
"assert.shapeEnd": { argTypes: [STRING, BOOL], result: VOID },
"assert.regexErrTest": { argTypes: [REGEX, { kind: "object", className: "%Error" }], result: BOOL },
"assert.unwantedRejection": { argTypes: [{ kind: "object", className: "%Error" }, STRING, BOOL], result: VOID },
"assert.expectsErrDyn": { argTypes: [DYN, DYN, STRING, BOOL], result: VOID },
// assert.ifError's typed entries (always throw; unit args never
// lower). The error slot is program-dependent — any %Error-hierarchy
// class, root or subclass (the insp.error precedent, null slot).
"assert.ifErrorErr": { argTypes: [null], result: VOID },
"assert.ifErrorF64": { argTypes: [F64], result: VOID },
"assert.ifErrorStr": { argTypes: [STRING], result: VOID },
"assert.ifErrorBool": { argTypes: [BOOL], result: VOID },
"assert.ifErrorDyn": { argTypes: [DYN], result: VOID },
// Bytes equality: the two value slots are any bytes kind (the frontend
// gates both sides to ONE static bytes type; the libCall case checks
// the kind and the pair below).
"assert.refEqBytes": { argTypes: [null, null, BOOL, BOOL, STRING, BOOL], result: VOID },
"assert.refEqFn": { argTypes: [null, null, BOOL, STRING, BOOL], result: VOID },
"assert.bytesDeepEq": { argTypes: [null, null, BOOL], result: BOOL },
// util.inspect: the error receiver is program-dependent (a builtin or
// user error class — the error.toString precedent, null slot).
"insp.f64": { argTypes: [F64], result: STRING },
"insp.jsonDyn": { argTypes: [DYN], result: STRING },
"insp.str": { argTypes: [STRING], result: STRING },
"insp.regex": { argTypes: [REGEX], result: STRING },
"insp.buffer": { argTypes: [BYTES_U8], result: STRING },
"insp.error": { argTypes: [null, F64, F64], result: STRING },
"insp.dyn": { argTypes: [DYN, F64, F64], result: STRING },
"insp.dynS": { argTypes: [DYN, F64], result: STRING },
"insp.jsval": { argTypes: [JSVAL, F64, F64], result: STRING },
"insp.begin": { argTypes: [F64], result: VOID },
"insp.entry": { argTypes: [STRING, BOOL], result: VOID },
// Circular references: the receiver slot is program-dependent (any
// cycle-capable record/array/map/class type — the insp.error precedent).
"insp.circCheck": { argTypes: [null], result: F64 },
"insp.seenPush": { argTypes: [null], result: VOID },
"insp.refWrap": { argTypes: [null, STRING], result: STRING },
"insp.circular": { argTypes: [F64], result: STRING },
"insp.key": { argTypes: [STRING], result: STRING },
"insp.moreItems": { argTypes: [F64], result: STRING },
"insp.end": { argTypes: [STRING, STRING, STRING, F64, BOOL, BOOL], result: STRING },
"strdec.write": { argTypes: [STRING, F64, BYTES_U8], result: STRING },
"strdec.next": { argTypes: [STRING, F64, BYTES_U8], result: F64 },
"strdec.end": { argTypes: [STRING, F64], result: STRING },
// node:readline: the interface handle is f64; the callbacks' func types
// are program-dependent (zero-param or (answer: string) — the emitter
// picks the adapter), so those slots are null like child.onExit's.
"rl.create": { argTypes: [], result: F64 },
"rl.question": { argTypes: [F64, STRING, null], result: VOID },
"rl.close": { argTypes: [F64], result: VOID },
"rl.onClose": { argTypes: [F64, null], result: VOID },
"tp.setTimeout": { argTypes: [F64], result: { kind: "promise", inner: VOID } },
"tp.setImmediate": { argTypes: [], result: { kind: "promise", inner: VOID } },
"dc.channel": { argTypes: [STRING], result: F64 },
"dc.subscribe": { argTypes: [STRING, DYN], result: VOID },
"dc.unsubscribe": { argTypes: [STRING, DYN], result: BOOL },
"dc.hasSubscribers": { argTypes: [STRING], result: BOOL },
"dc.publish": { argTypes: [F64, DYN], result: VOID },
"dc.chanSubscribe": { argTypes: [F64, DYN], result: VOID },
"dc.chanUnsubscribe": { argTypes: [F64, DYN], result: BOOL },
"dc.chanHasSubscribers": { argTypes: [F64], result: BOOL },
"dc.chanName": { argTypes: [F64], result: STRING },
"timers.setImmediateFnValue": { argTypes: [], result: DYN },
"timers.immediatePromise": { argTypes: [], result: { kind: "promise", inner: DYN } },
"dc.tracingChannel": { argTypes: [STRING], result: F64 },
"dc.tracingChannelOf": { argTypes: [F64, F64, F64, F64, F64], result: F64 },
"dc.tcChannel": { argTypes: [F64, F64], result: F64 },
"dc.tcHasSubscribers": { argTypes: [F64], result: BOOL },
"dc.tcSubscribe": { argTypes: [F64, DYN], result: VOID },
"dc.tcUnsubscribe": { argTypes: [F64, DYN], result: BOOL },
"dc.tcTraceSync": { argTypes: [F64, DYN, DYN, DYN, DYN], result: DYN },
"dc.tcTraceCallback": { argTypes: [F64, DYN, F64, DYN, DYN, DYN], result: DYN },
"dc.tcTracePromise": { argTypes: [F64, DYN, DYN, DYN, DYN], result: { kind: "promise", inner: DYN } },
"process.onUnhandledRejection": { argTypes: [DYN, BOOL], result: VOID },
"process.offUnhandledRejection": { argTypes: [DYN], result: VOID },
"process.onRejectionHandled": { argTypes: [DYN, BOOL], result: VOID },
"process.offRejectionHandled": { argTypes: [DYN], result: VOID },
"process.onWarning": { argTypes: [DYN], result: VOID },
"process.offWarning": { argTypes: [DYN], result: VOID },
"process.emitWarning": { argTypes: [DYN], result: VOID },
"async.awaitDyn": { argTypes: [DYN], result: DYN },
"async.hop": { argTypes: [], result: VOID },
"als.new": { argTypes: [], result: F64 },
"als.get": { argTypes: [F64], result: DYN },
"als.run": { argTypes: [F64, DYN, DYN, DYN], result: DYN },
"als.exitRun": { argTypes: [F64, DYN, DYN], result: DYN },
"als.enterWith": { argTypes: [F64, DYN], result: VOID },
"als.disable": { argTypes: [F64], result: VOID },
"dc.chanBindStore": { argTypes: [F64, F64, DYN], result: VOID },
"dc.chanUnbindStore": { argTypes: [F64, F64], result: BOOL },
"dc.chanRunStores": { argTypes: [F64, DYN, DYN, DYN, DYN], result: DYN },
"number.isFinite": { argTypes: [F64], result: BOOL },
"number.isNaN": { argTypes: [F64], result: BOOL },
"number.isInteger": { argTypes: [F64], result: BOOL },
"number.isSafeInteger": { argTypes: [F64], result: BOOL },
"date.now": { argTypes: [], result: F64 },
"date.newNow": { argTypes: [], result: DATE_T },
"date.newMs": { argTypes: [F64], result: DATE_T },
"date.newString": { argTypes: [STRING], result: DATE_T },
"date.parse": { argTypes: [STRING], result: F64 },
"date.getTime": { argTypes: [DATE_T], result: F64 },
"date.valueOf": { argTypes: [DATE_T], result: F64 },
"date.toISOString": { argTypes: [F64], result: STRING },
"date.toISOStringValue": { argTypes: [DATE_T], result: STRING },
"date.getFullYear": { argTypes: [DATE_T], result: F64 },
"date.getUTCFullYear": { argTypes: [DATE_T], result: F64 },
"date.getMonth": { argTypes: [DATE_T], result: F64 },
"date.getUTCMonth": { argTypes: [DATE_T], result: F64 },
"date.getDate": { argTypes: [DATE_T], result: F64 },
"date.getUTCDate": { argTypes: [DATE_T], result: F64 },
"date.getDay": { argTypes: [DATE_T], result: F64 },
"date.getUTCDay": { argTypes: [DATE_T], result: F64 },
"date.getHours": { argTypes: [DATE_T], result: F64 },
"date.getUTCHours": { argTypes: [DATE_T], result: F64 },
"date.getMinutes": { argTypes: [DATE_T], result: F64 },
"date.getUTCMinutes": { argTypes: [DATE_T], result: F64 },
"date.getSeconds": { argTypes: [DATE_T], result: F64 },
"date.getUTCSeconds": { argTypes: [DATE_T], result: F64 },
"date.getMilliseconds": { argTypes: [DATE_T], result: F64 },
"date.getUTCMilliseconds": { argTypes: [DATE_T], result: F64 },
"date.getTimezoneOffset": { argTypes: [DATE_T], result: F64 },
"date.parseGetTime": { argTypes: [STRING], result: F64 },
"date.utc": { argTypes: [F64, F64, F64, F64, F64, F64, F64], result: F64 },
"text.decode": { argTypes: [BYTES_U8], result: STRING },
"text.decodeLegacy": { argTypes: [BYTES_U8, F64], result: STRING },
"fs.mkdirRecursiveSync": { argTypes: [STRING], result: VOID },
"fs.rmOptsSync": { argTypes: [STRING, BOOL, BOOL], result: VOID },
"fs.rmRetrySync": { argTypes: [STRING, BOOL, BOOL, F64, F64], result: VOID },
"fs.mkdtempSync": { argTypes: [STRING], result: STRING },
"fs.accessSync": { argTypes: [STRING, F64], result: VOID },
"fs.readFdSync": { argTypes: [F64, STRING], result: STRING },
"fs.readFdSyncBytes": { argTypes: [F64], result: BYTES_U8 },
"process.isTTY": { argTypes: [F64], result: BOOL },
// Like process.envGet: the result is the module's interned
// `number | undefined` union — checked by arms in the libCall case.
"process.columns": { argTypes: [F64], result: VOID },
"process.stdinDestroy": { argTypes: [], result: VOID },
"process.stdinSetRawMode": { argTypes: [BOOL], result: VOID },
// Arg 0 is a packed f64[] OR a bytes value (the spread-typed-array
// form) — checked in the libCall case.
"string.fromCharCode": { argTypes: [null], result: STRING },
"string.lastIndexOf": { argTypes: [STRING, STRING], result: F64 },
"string.lastIndexOfFrom": { argTypes: [STRING, STRING, F64], result: F64 },
"string.raw": { argTypes: [arrayOf(STRING), arrayOf(STRING)], result: STRING },
};
/** True for an object type naming a runtime-provided error class. */
function isBuiltinErrorObject(t: IrType): boolean {
return t.kind === "object" && RUNTIME_ERROR_CLASSES.has(t.className);
}
export interface IrValidationError {
message: string;
loc: SrcLoc;
}
/** The type a CALL SITE (or function value) receives from a module
* function: an async body's returnType is the promise's INNER type, so
* calls and closures over it see Promise<T>; a generator body's is the
* TReturn channel, so call sites see the generator type. The one place
* the body/call-site split is spelled out in the validator. */
function callSiteReturnType(fn: IrFunction): IrType {
if (fn.async && fn.generator !== undefined) {
return { kind: "generator", async: true, yieldT: fn.generator.yieldT, retT: fn.returnType, nextT: fn.generator.nextT };
}
if (fn.async) return { kind: "promise", inner: fn.returnType };
if (fn.generator !== undefined) {
return { kind: "generator", yieldT: fn.generator.yieldT, retT: fn.returnType, nextT: fn.generator.nextT };
}
return fn.returnType;
}
export function validateModule(mod: IrModule): IrValidationError[] {
const errors: IrValidationError[] = [];
const functionsByName = new Map<string, IrFunction>();
for (const fn of mod.functions) {
if (functionsByName.has(fn.name)) {
errors.push({ message: `duplicate function "${fn.name}"`, loc: fn.loc });
}
functionsByName.set(fn.name, fn);
}
const ffiByName = new Map<string, NonNullable<IrModule["ffiImports"]>[number]>();
const ffiSymbols = new Set<string>();
const moduleLoc: SrcLoc = { file: mod.sourceFile, start: 0, end: 0 };
for (const entry of mod.ffiImports ?? []) {
if (ffiByName.has(entry.name)) {
errors.push({ message: `duplicate FFI binding "${entry.name}"`, loc: moduleLoc });
}
if (ffiSymbols.has(entry.symbol)) {
errors.push({ message: `duplicate FFI symbol "${entry.symbol}"`, loc: moduleLoc });
}
ffiByName.set(entry.name, entry);
ffiSymbols.add(entry.symbol);
}
const retainedFfiCallbacks = new Map<string, Extract<NonNullable<IrModule["ffiImports"]>[number]["params"][number], { callback: { id: string } }>["callback"]>();
for (const entry of mod.ffiImports ?? []) {
const ids = new Set<string>();
for (const param of entry.params) {
if (!isFfiCallbackParam(param)) continue;
if (ids.has(param.callback.id)) {
errors.push({ message: `FFI binding "${entry.name}" has duplicate callback id "${param.callback.id}"`, loc: moduleLoc });
}
ids.add(param.callback.id);
if (param.callback.invoke !== "script-thread" && param.callback.invoke !== "foreign") {
errors.push({ message: `FFI callback "${entry.name}:${param.callback.id}" has invalid invoke mode`, loc: moduleLoc });
}
if (param.callback.invoke === "foreign") {
if (param.callback.lifetime !== "retained") {
errors.push({ message: `FFI foreign callback "${entry.name}:${param.callback.id}" is not retained`, loc: moduleLoc });
}
if (param.callback.returns !== "void") {
errors.push({ message: `FFI foreign callback "${entry.name}:${param.callback.id}" does not return void`, loc: moduleLoc });
}
if (!param.callback.params.some(isFfiContextParam)) {
errors.push({ message: `FFI foreign callback "${entry.name}:${param.callback.id}" has no context`, loc: moduleLoc });
}
}
if (param.callback.lifetime === "retained") {
retainedFfiCallbacks.set(`${entry.name}:${param.callback.id}`, param.callback);
}
const hasInnerContext = param.callback.params.some(isFfiContextParam);
const outerContexts = entry.params.filter(
(candidate) => isFfiContextParam(candidate) && candidate.context === param.callback.id,
).length;
if (hasInnerContext !== (outerContexts === 1)) {
errors.push({ message: `FFI callback "${entry.name}:${param.callback.id}" has inconsistent context slots`, loc: moduleLoc });
}
}
}
for (const entry of mod.ffiImports ?? []) {
for (const param of entry.params) {
if (!isFfiReleaseParam(param)) continue;
const target = retainedFfiCallbacks.get(param.callback.release);
if (target === undefined) {
errors.push({ message: `FFI release "${entry.name}:${param.callback.release}" has no retained target`, loc: moduleLoc });
continue;
}
// A call registering its own release target defeats the emitted
// pin -> require -> call -> commit -> release ordering (the loader
// rejects this shape; mirrored here for deserialized IR).
const registeredBySameCall = entry.params.some(
(candidate) =>
isFfiCallbackParam(candidate) &&
`${entry.name}:${candidate.callback.id}` === param.callback.release,
);
if (registeredBySameCall) {
errors.push({ message: `FFI release "${entry.name}:${param.callback.release}" targets a retained callback registered by the same call`, loc: moduleLoc });
}
// Structural ABI comparison: a params entry is a value-class string
// or a {context} object. Key order and incidental object shape must
// not matter — a producer that rebuilds these arrays (deserialized
// IR, a second frontend) still validates.
const inherited = param.callback.params.length === target.params.length &&
param.callback.params.every((entry, i) => {
const other = target.params[i]!;
return isFfiContextParam(entry)
? isFfiContextParam(other) && entry.context === other.context
: entry === other;
}) &&
param.callback.returns === target.returns;
if (!inherited) {
errors.push({ message: `FFI release "${entry.name}:${param.callback.release}" does not inherit its target ABI`, loc: moduleLoc });
}
const hasInnerContext = target.params.some(isFfiContextParam);
const outerContexts = entry.params.filter(
(candidate) => isFfiContextParam(candidate) && candidate.context === param.callback.release,
).length;
if (hasInnerContext !== (outerContexts === 1)) {
errors.push({ message: `FFI release "${entry.name}:${param.callback.release}" has inconsistent context slots`, loc: moduleLoc });
}
}
}
// The lib section (library mode): every mapped function exists, is
// synchronous, and its IR signature fits the declared marshalling
// classes — the SC4xxx refusals ran before this landed on the module, so
// a violation here is a compiler bug like any other validation error.
if (mod.lib !== undefined) {
const entryLoc: SrcLoc = { file: mod.sourceFile, start: 0, end: 0 };
for (const e of mod.lib.exports) {
const fn = functionsByName.get(e.fnName);
if (!fn) {
errors.push({ message: `library export "${e.symbol}": missing function "${e.fnName}"`, loc: entryLoc });
continue;
}
if (fn.async === true || fn.generator !== undefined) {
errors.push({ message: `library export "${e.symbol}": "${e.fnName}" is async/generator`, loc: fn.loc });
}
if (fn.captures !== undefined) {
errors.push({ message: `library export "${e.symbol}": "${e.fnName}" captures an environment`, loc: fn.loc });
}
if (fn.params.length !== e.params.length) {
errors.push({
message: `library export "${e.symbol}": ${e.params.length} marshalling classes over ${fn.params.length} params`,
loc: fn.loc,
});
continue;
}
const fits = (cls: string, t: IrType): boolean =>
cls === "bool"
? t.kind === "bool"
: cls === "string"
? t.kind === "string"
: cls === "bytes"
? t.kind === "bytes" && t.elem === "u8"
: t.kind === "f64"; // f64 + the integer plumbing classes
e.params.forEach((cls, i) => {
if (!fits(cls, fn.params[i]!.type)) {
errors.push({
message: `library export "${e.symbol}": param ${i} class "${cls}" over IR type "${fn.params[i]!.type.kind}"`,
loc: fn.loc,
});
}
});
if (e.returns === "void" ? fn.returnType.kind !== "void" : !fits(e.returns, fn.returnType)) {
errors.push({
message: `library export "${e.symbol}": return class "${e.returns}" over IR type "${fn.returnType.kind}"`,
loc: fn.loc,
});
}
}
if (!functionsByName.has(mod.entry)) {
errors.push({ message: `library module missing its entry function "${mod.entry}"`, loc: entryLoc });
}
// Host-callback channels: the register symbol and the channel list are
// paired (the profile loader refuses otherwise, so a miss here is a
// compiler bug), slots are the declaration order, and every channel
// has its matching ffiImport (the lowering recognizes calls by it).
const cbs = mod.lib.callbacks ?? [];
if ((cbs.length > 0) !== (mod.lib.callbackRegisterSymbol !== undefined)) {
errors.push({ message: "library callbacks and callbackRegisterSymbol must be present together", loc: entryLoc });
}
const cbNames = new Set<string>();
cbs.forEach((cb, i) => {
if (cb.slot !== i) {
errors.push({ message: `library callback "${cb.name}": slot ${cb.slot} out of declaration order (expected ${i})`, loc: entryLoc });
}
if (cbNames.has(cb.name)) {
errors.push({ message: `duplicate library callback channel "${cb.name}"`, loc: entryLoc });
}
cbNames.add(cb.name);
if (!(mod.ffiImports ?? []).some((f) => f.name === cb.name)) {
errors.push({ message: `library callback "${cb.name}" has no matching ffiImport`, loc: entryLoc });
}
});
}
const classesByName = new Map<string, IrClassDef>();
for (const cls of mod.classes ?? []) {
if (classesByName.has(cls.name)) {
errors.push({ message: `duplicate class "${cls.name}"`, loc: cls.loc });
}
classesByName.set(cls.name, cls);
const seen = new Set<string>();
for (const f of cls.fields) {
if (seen.has(f.name)) {
errors.push({ message: `class ${cls.name}: duplicate field "${f.name}"`, loc: cls.loc });
}
seen.add(f.name);
if (isUnitType(f.type)) {
errors.push({ message: `class ${cls.name}: field "${f.name}" is ${f.type.kind}`, loc: cls.loc });
}
}
// Constructor presence is checked at `new` sites (below): a class kept
// only for its layout/type (reachability never constructs it) carries
// no constructor function, and that is fine — nothing calls it.
// ABSTRACT entries are declarations without bodies by definition — no
// module function exists for them (and must not).
for (const m of cls.abstractMethods ?? []) {
if (!cls.methods?.includes(m)) {
errors.push({ message: `class ${cls.name}: abstract method "${m}" not in methods`, loc: cls.loc });
}
if (functionsByName.has(`%${cls.name}.${m}`)) {
errors.push({ message: `class ${cls.name}: abstract method "${m}" has a function`, loc: cls.loc });
}
}
for (const m of cls.methods ?? []) {
if (cls.abstractMethods?.includes(m)) continue;
if (!functionsByName.has(`%${cls.name}.${m}`)) {
errors.push({ message: `class ${cls.name}: missing method function "${m}"`, loc: cls.loc });
}
}
}
// Hierarchy invariants: bases exist, the graph is acyclic, and every
// derived class's field list starts with its base's EXACTLY (the prefix
// layout that makes upcasts pointer reinterprets).
for (const cls of mod.classes ?? []) {
if (cls.base === undefined) continue;
const base = classesByName.get(cls.base);
if (!base) {
errors.push({ message: `class ${cls.name}: undeclared base "${cls.base}"`, loc: cls.loc });
continue;
}
const seen = new Set<string>([cls.name]);
for (let c: IrClassDef | undefined = base; c; c = c.base !== undefined ? classesByName.get(c.base) : undefined) {
if (seen.has(c.name)) {
errors.push({ message: `class ${cls.name}: cyclic extends chain`, loc: cls.loc });
break;
}
seen.add(c.name);
}
if (
cls.fields.length < base.fields.length ||
base.fields.some(
(f, i) => cls.fields[i]!.name !== f.name || !typeEquals(cls.fields[i]!.type, f.type),
)
) {
errors.push({
message: `class ${cls.name}: fields are not a layout extension of base "${cls.base}"`,
loc: cls.loc,
});
}
}
// Generic-class instantiations: the named FAMILY exists and is a proper
// ancestor (the class object's interval is read off it — see IrClassDef).
for (const cls of mod.classes ?? []) {
if (cls.genericOf === undefined) continue;
const family = classesByName.get(cls.genericOf);
if (!family) {
errors.push({ message: `class ${cls.name}: undeclared generic family "${cls.genericOf}"`, loc: cls.loc });
continue;
}
let ancestor = false;
const seen = new Set<string>();
for (let c = cls.base !== undefined ? classesByName.get(cls.base) : undefined; c && !seen.has(c.name); c = c.base !== undefined ? classesByName.get(c.base) : undefined) {
seen.add(c.name);
if (c.name === cls.genericOf) {
ancestor = true;
break;
}
}
if (!ancestor) {
errors.push({ message: `class ${cls.name}: generic family "${cls.genericOf}" is not an ancestor`, loc: cls.loc });
}
}
const noLoc: SrcLoc = { file: mod.sourceFile, start: 0, end: 0 };
const recordsById = new Map<string, IrRecordShape>();
for (const rec of mod.records ?? []) {
if (recordsById.has(rec.id)) {
errors.push({ message: `duplicate record shape "${rec.id}"`, loc: noLoc });
}
recordsById.set(rec.id, rec);
const seen = new Set<string>();
for (const f of rec.fields) {
if (seen.has(f.name)) {
errors.push({ message: `record ${rec.id}: duplicate field "${f.name}"`, loc: noLoc });
}
seen.add(f.name);
// Unit kinds (undefinedT/nullT) exist only as union arms — a BARE
// unit field is as malformed as a void one. dyn fields are VALID
// (`[string, unknown]` entries tuples, `{ v: unknown }` records):
// the slot holds a dyn value with the overflow map's plumbing.
if (
f.type.kind === "void" || f.type.kind === "jsval" ||
isUnitType(f.type)
) {
errors.push({ message: `record ${rec.id}: field "${f.name}" is ${f.type.kind}`, loc: noLoc });
}
if (f.type.kind === "record" && !new Set((mod.records ?? []).map((r) => r.id)).has(f.type.shapeId)) {
errors.push({
message: `record ${rec.id}: field "${f.name}" references undeclared shape "${f.type.shapeId}"`,
loc: noLoc,
});
}
}
// Canonical order is the shape's identity — enforce it.
const sorted = [...rec.fields].map((f) => f.name).sort();
if (rec.fields.some((f, i) => f.name !== sorted[i])) {
errors.push({ message: `record ${rec.id}: fields are not in canonical (sorted) order`, loc: noLoc });
}
// Tuple shapes carry exactly the positional fields "0".."n-1" (arity =
// field count) — anything else has no honest index/JSON story.
if (rec.tuple) {
const names = new Set(rec.fields.map((f) => f.name));
if (
rec.fields.length === 0 ||
names.size !== rec.fields.length ||
[...Array(rec.fields.length).keys()].some((i) => !names.has(String(i)))
) {
errors.push({ message: `record ${rec.id}: tuple fields are not "0".."${rec.fields.length - 1}"`, loc: noLoc });
}
if (rec.indexValue) {
errors.push({ message: `record ${rec.id}: a tuple cannot carry an index signature`, loc: noLoc });
}
}
// Index-signature shapes: the overflow value type is fenced to the map
// VALUE kinds plus dyn (`unknown` signatures) — isSupportedIndexValue.
if (rec.indexValue && !isSupportedIndexValue(rec.indexValue)) {
errors.push({
message: `record ${rec.id}: index-signature value type ${rec.indexValue.kind} is unsupported`,
loc: noLoc,
});
}
}
const unionsById = new Map<string, IrUnionDef>();
for (const u of mod.unions ?? []) {
if (unionsById.has(u.id)) {
errors.push({ message: `duplicate union "${u.id}"`, loc: noLoc });
}
unionsById.set(u.id, u);
if (u.arms.length < 2) {
errors.push({ message: `union ${u.id}: fewer than 2 arms`, loc: noLoc });
}
u.arms.forEach((arm, i) => {
// The unit kinds (undefinedT/nullT) are valid arms — union membership
// is the ONLY place they may appear; void/union/map/dyn/jsval/date
// stay out (maps and scalar Date values have no supported union
// representation/discriminant to narrow on). Func/set arm
// sibling rules live in unionFuncSetArmsOk (shared with the frontend's
// union builders): a func arm allows unit and FUNC siblings (the
// nullable-callback shape, and the primitive-constructor tables where
// closure pointer identity per tag is the narrowing); a set arm is
// valid exactly when every other arm is a unit (the defaulted-Set-
// param ABI); func/set-beside-data stays out.
if (
arm.kind === "void" ||
arm.kind === "union" ||
arm.kind === "map" ||
arm.kind === "dyn" ||
arm.kind === "jsval" ||
arm.kind === "date" ||
arm.kind === "generator"
) {
errors.push({ message: `union ${u.id}: arm ${i} is ${arm.kind}`, loc: noLoc });
}
if (
(arm.kind === "func" || arm.kind === "set") &&
!unionFuncSetArmsOk(u.arms)
) {
errors.push({ message: `union ${u.id}: ${arm.kind} arm ${i} beside non-unit arms`, loc: noLoc });
}
if (arm.kind === "record" && !recordsById.has(arm.shapeId)) {
errors.push({
message: `union ${u.id}: arm ${i} references undeclared shape "${arm.shapeId}"`,
loc: noLoc,
});
}
// classval arms may name UNDECLARED classes: the payload emits as
// the class-independent `ScrClassObj *` (see the namesUndeclared
// backstop below) — a fenced class's value slot is inert-but-valid.
for (let j = i + 1; j < u.arms.length; j++) {
if (typeEquals(arm, u.arms[j]!)) {
errors.push({ message: `union ${u.id}: arms ${i} and ${j} are identical`, loc: noLoc });
}
}
});
}
const globalsById = new Map((mod.globals ?? []).map((g) => [g.id, g]));
// Every class an emitted type slot names as an OBJECT type must be
// DECLARED: the emitter writes the class's own struct type and typed
// retain/release calls for such slots, so an unregistered class there
// is invalid C waiting to happen (the compile-C escape family — a JS
// class whose collection fenced, with the object-typed slot left
// behind; run() prunes the dead ones, and this is the backstop that
// turns any live escape into an ICE instead of a clang error). CLASSVAL
// types are exempt: every class value emits as the one class-independent
// `ScrClassObj *` (constructing/dispatching through it re-resolves the
// class and carries its own lowering-time fence), so a classval naming
// a fenced class is inert-but-valid storage, the honest leftovers of a
// runtime-fenced declaration.
const namesUndeclared = (t: IrType, seen: Set<string>): string | null => {
switch (t.kind) {
case "object":
return classesByName.has(t.className) ? null : t.className;
case "array":
case "set":
return namesUndeclared(t.elem, seen);
case "map":
return namesUndeclared(t.key, seen) ?? namesUndeclared(t.value, seen);
case "promise":
return namesUndeclared(t.inner, seen);
case "generator":
return (
namesUndeclared(t.yieldT, seen) ??
namesUndeclared(t.retT, seen) ??
namesUndeclared(t.nextT, seen)
);
case "func":
for (const p of t.params) {
const hit = namesUndeclared(p, seen);
if (hit) return hit;
}
return namesUndeclared(t.ret, seen);
case "record": {
if (seen.has(t.shapeId)) return null;
seen.add(t.shapeId);
const rec = recordsById.get(t.shapeId);
if (!rec) return null; // its own undeclared-shape check reports
if (rec.indexValue) {
const hit = namesUndeclared(rec.indexValue, seen);
if (hit) return hit;
}
for (const f of rec.fields) {
const hit = namesUndeclared(f.type, seen);
if (hit) return hit;
}
return null;
}
case "union": {
if (seen.has(t.unionId)) return null;
seen.add(t.unionId);
const def = unionsById.get(t.unionId);
if (!def) return null;
for (const a of def.arms) {
const hit = namesUndeclared(a, seen);
if (hit) return hit;
}
return null;
}
default:
return null;
}
};
for (const g of mod.globals ?? []) {
if (isUnitType(g.type)) {
errors.push({ message: `global "${g.name}" has bare unit type ${g.type.kind}`, loc: noLoc });
}
if (!g.id.startsWith("%g.")) {
errors.push({
message: `global "${g.name}" id "${g.id}" outside the %g. namespace`,
loc: { file: mod.sourceFile, start: 0, end: 0 },
});
}
const undeclared = namesUndeclared(g.type, new Set());
if (undeclared !== null) {
errors.push({
message: `global "${g.name}" names undeclared class "${undeclared}"`,
loc: noLoc,
});
}
}
// The same backstop over class LAYOUTS: a field typed by an undeclared
// class emits that class's struct type and release call into the TU —
// the exact invalid-C escape the global check catches, one level deeper
// (the anonymous-CJS-export shape reached emission through a class
// FIELD, never a global). Function locals and returns stay UNCHECKED:
// a local typed by an uncollected class is a pinned-valid module state
// (the runtime-fenced-JS-class story — captured locals emit inert
// trapping boxes; see boxNewC's uncollected-class placeholder), where a
// field slot embeds the raw struct pointer and has no degradation.
for (const cls of mod.classes ?? []) {
for (const f of cls.fields) {
const undeclared = namesUndeclared(f.type, new Set());
if (undeclared !== null) {
errors.push({
message: `class ${cls.name}: field "${f.name}" names undeclared class "${undeclared}"`,
loc: cls.loc,
});
}
}
}
for (const fn of mod.functions) {
validateFunction(
fn,
functionsByName,
ffiByName,
classesByName,
recordsById,
unionsById,
globalsById,
errors,
);
}
return errors;
}
function validateFunction(
fn: IrFunction,
functions: Map<string, IrFunction>,
ffiByName: Map<string, NonNullable<IrModule["ffiImports"]>[number]>,
classes: Map<string, IrClassDef>,
records: Map<string, IrRecordShape>,
unions: Map<string, IrUnionDef>,
globals: Map<string, IrGlobal>,
errors: IrValidationError[],
): void {
const locals = new Map(fn.locals.map((l) => [l.id, l]));
const err = (message: string, loc: SrcLoc) =>
errors.push({ message: `in ${fn.name}: ${message}`, loc });
const asyncCaches = [
["asyncCacheGlobal", fn.asyncCacheGlobal],
["asyncCycleCacheGlobal", fn.asyncCycleCacheGlobal],
] as const;
for (const [field, cacheId] of asyncCaches) {
if (cacheId === undefined) continue;
if (fn.async !== true) {
err(`an ${field} is only valid on an async function`, fn.loc);
}
if (fn.params.length !== 0 || (fn.captures?.length ?? 0) !== 0) {
err("a cached async function must have no parameters or captures", fn.loc);
}
const cache = globals.get(cacheId);
if (cache === undefined) {
err(`async cache names undeclared global "${cacheId}"`, fn.loc);
} else {
const expected: IrType = { kind: "promise", inner: fn.returnType };
if (!typeEquals(cache.type, expected)) {
err(
`async cache global "${cacheId}" has type ${typeKey(cache.type)}, expected ${typeKey(expected)}`,
fn.loc,
);
}
if (!cache.mutable) {
err(`async cache global "${cacheId}" is immutable`, fn.loc);
}
}
}
if (fn.asyncCycleCacheGlobal !== undefined && fn.asyncCacheGlobal === undefined) {
err("an asyncCycleCacheGlobal requires a module asyncCacheGlobal", fn.loc);
}
// The class graph's two questions (upcast/downcast/instanceOf/virtualCall
// legality): strict-descendant tests over the base links, and hierarchy
// membership (a class that extends or is extended).
const isStrictSubclass = (sub: string, sup: string): boolean => {
for (
let c = classes.get(sub);
c?.base !== undefined;
c = classes.get(c.base)
) {
if (c.base === sup) return true;
}
return false;
};
const hierarchy = new Set<string>();
for (const c of classes.values()) {
if (c.base !== undefined) {
hierarchy.add(c.name);
hierarchy.add(c.base);
}
// The runtime emitter class is ALWAYS a hierarchy member: ScrEmitter
// carries its vtable word even with no subclass in the program.
if (c.name === RUNTIME_EMITTER_CLASS) hierarchy.add(c.name);
}
// Unit kinds live only inside unions: a bare-unit local, param, or
// return type is frontend breakage (mapType never produces them).
for (const l of fn.locals) {
if (isUnitType(l.type)) err(`local "${l.name}" has bare unit type ${l.type.kind}`, fn.loc);
}
if (isUnitType(fn.returnType)) {
err(`return type is bare unit type ${fn.returnType.kind}`, fn.loc);
}
// Caught (catch-binding) values are local-only by construction: they can
// never be parameters, returns, or captures (the frontend fences every
// escape; a caught anywhere else is frontend breakage).
if (fn.returnType.kind === "caught") err("return type is caught", fn.loc);
for (const p of fn.params) {
if (!locals.has(p.localId)) {
err(`param "${p.name}" has no local entry "${p.localId}"`, fn.loc);
}
if (p.type.kind === "caught") err(`param "${p.name}" is caught-typed`, fn.loc);
}
for (const c of fn.captures ?? []) {
const local = locals.get(c.localId);
if (!local) err(`capture "${c.name}" has no local entry "${c.localId}"`, fn.loc);
else if (!local.boxed) err(`capture local "${c.localId}" is not boxed`, fn.loc);
if (c.type.kind === "caught") err(`capture "${c.name}" is caught-typed`, fn.loc);
}
const expectType = (expr: IrExpr, want: IrType, what: string) => {
if (!typeEquals(expr.type, want)) {
// Same-kind mismatches (func vs func, record vs record) would be
// unreadable as bare kinds — the structural keys show WHERE the two
// types diverge.
const detail =
want.kind === expr.type.kind ? ` (expected ${typeKey(want)}, got ${typeKey(expr.type)})` : "";
err(`${what}: expected ${want.kind}, got ${expr.type.kind}${detail}`, expr.loc);
}
};
/** Union arms a truthiness helper can answer: units (false), scalars and
* strings (per-value), ref kinds (always true), jsval (the engine
* answers). dyn/caught arms have no ToBoolean — the frontend fences them
* before emitting toBool/logical over the union. */
const checkTruthyUnion = (unionId: string, loc: SrcLoc) => {
const def = unions.get(unionId);
if (!def) {
err(`truthiness of unknown union ${unionId}`, loc);
return;
}
for (const arm of def.arms) {
if (arm.kind === "dyn" || arm.kind === "caught" || arm.kind === "void") {
err(`truthiness of union with ${arm.kind} arm`, loc);
}
}
};
// Optional chains open a binding scope: chainRecv is valid only inside
// the body of the optChain whose id it names.
const activeChains = new Map<string, IrType>();
const liveDynRefEligible = (
type: IrType,
seen = new Set<string>(),
): boolean => {
if (
type.kind === "record" ||
type.kind === "array" ||
type.kind === "bytes" ||
isDynTypedRefType(type)
) {
return true;
}
if (type.kind !== "union" || seen.has(type.unionId)) return false;
seen.add(type.unionId);
return unions.get(type.unionId)?.arms.some((arm) =>
liveDynRefEligible(arm, seen)
) ?? false;
};
function checkExpr(e: IrExpr): void {
switch (e.kind) {
case "numLit":
// ±Infinity and NaN are real literals (the globals
// `Infinity`/`NaN`, Number constants) — both backends spell them
// (INFINITY/NAN macros in C, bit-encoded f64 in LLVM).
// procStream is the ONE non-f64 numLit: process.stdout/stderr as
// first-class values mint the stream's fd (1/2) as the scalar —
// the prefixStream idiom.
if (e.type.kind !== "f64" && !(e.type.kind === "procStream" && (e.value === 1 || e.value === 2))) {
err("numLit must be f64", e.loc);
}
break;
case "strLit":
if (e.type.kind !== "string") err("strLit must be string", e.loc);
break;
case "moduleNsRef":
if (e.type.kind !== "moduleNs" || e.type.moduleId !== e.moduleId) {
err("moduleNsRef must match its moduleNs type", e.loc);
}
break;
case "boolLit":
if (e.type.kind !== "bool") err("boolLit must be bool", e.loc);
break;
case "unitLit":
// Reachable only for a unitLit OUTSIDE a unionWrap (the wrap case
// validates its unit value inline) — unit types have no standalone
// runtime value, so a bare one is frontend breakage.
err(`bare unitLit '${e.unit}' outside a unionWrap`, e.loc);
break;
case "varRef": {
const binding = locals.get(e.localId) ?? globals.get(e.localId);
if (!binding) err(`varRef to undeclared local/global "${e.localId}"`, e.loc);
else if (!typeEquals(binding.type, e.type)) {
err(`varRef "${e.localId}" type ${e.type.kind} != binding ${binding.type.kind}`, e.loc);
}
break;
}
case "bin": {
checkExpr(e.left);
checkExpr(e.right);
const isEq = e.op === "===" || e.op === "!==";
if (isEq && e.left.type.kind === "func") {
// Function identity tolerates DIFFERING static signatures (the
// compare is pointer equality; tsc gates the overlap).
if (e.right.type.kind !== "func") {
err(`bin ${e.op} on functions: right operand is ${e.right.type.kind}`, e.loc);
}
} else if (isEq && e.left.type.kind === "classval") {
// Class identity tolerates DIFFERING static classes like
// function identity does signatures: one immortal object per
// class, one pointer compare (tsc gates the overlap).
if (e.right.type.kind !== "classval") {
err(`bin ${e.op} on class values: right operand is ${e.right.type.kind}`, e.loc);
}
} else if (isEq && e.left.type.kind === "moduleNs") {
if (e.right.type.kind !== "moduleNs") {
err(`bin ${e.op} on module namespaces: right operand is ${e.right.type.kind}`, e.loc);
}
} else if (
isEq &&
(e.left.type.kind === "array" ||
e.left.type.kind === "map" ||
e.left.type.kind === "set" ||
e.left.type.kind === "object" ||
e.left.type.kind === "record" ||
// Symbol identity IS pointer identity (the frontend's rule).
e.left.type.kind === "symbol" ||
e.left.type.kind === "bytes" ||
e.left.type.kind === "promise" ||
// Runtime handles are objects to === (one handle per socket/
// request — pointer identity is JS's object equality).
DYN_HANDLE_KINDS.has(e.left.type.kind))
) {
// Reference identity: both operands must be the same ref type.
if (!typeEquals(e.left.type, e.right.type)) {
err(`bin ${e.op} on references: operand types differ`, e.loc);
}
} else if (isEq && e.left.type.kind === "bool") {
// bool === bool: a plain value compare.
expectType(e.right, { kind: "bool" }, `bin ${e.op} right`);
} else {
expectType(e.left, { kind: "f64" }, `bin ${e.op} left`);
expectType(e.right, { kind: "f64" }, `bin ${e.op} right`);
}
const isCompare = ["<", "<=", ">", ">=", "===", "!=="].includes(e.op);
if (!typeEquals(e.type, isCompare ? BOOL : { kind: "f64" })) {
err(`bin ${e.op} result must be ${isCompare ? "bool" : "f64"}`, e.loc);
}
break;
}
case "unary":
checkExpr(e.operand);
if (e.op === "-" || e.op === "~") {
expectType(e.operand, { kind: "f64" }, `unary ${e.op}`);
if (e.type.kind !== "f64") err(`unary ${e.op} must be f64`, e.loc);
} else {
expectType(e.operand, BOOL, "unary !");
if (e.type.kind !== "bool") err("unary ! must be bool", e.loc);
}
break;
case "incDec": {
// Expression-position ++/--: an f64 local or module global,
// mutable, read-and-written in place.
const binding = locals.get(e.localId) ?? globals.get(e.localId);
if (!binding) err(`incDec of undeclared local/global "${e.localId}"`, e.loc);
else {
if (!binding.mutable && locals.has(e.localId)) {
err(`incDec of immutable local "${binding.name}"`, e.loc);
}
if (binding.type.kind !== "f64") {
err(`incDec of non-f64 binding "${binding.name}" (${binding.type.kind})`, e.loc);
}
}
if (e.type.kind !== "f64") err("incDec must be f64", e.loc);
break;
}
case "fieldIncDec": {
// Expression-position ++/-- over a class field: an f64 field, or a
// dyn field with fieldDyn set (validated numeric read-modify-write).
checkExpr(e.obj);
const cls = classes.get(e.className);
const field = cls?.fields.find((f) => f.name === e.field);
if (!cls) err(`fieldIncDec on undeclared class "${e.className}"`, e.loc);
else if (!field) err(`class ${e.className} has no field "${e.field}"`, e.loc);
else {
expectType(e.obj, { kind: "object", className: e.className }, "fieldIncDec receiver");
if (e.fieldDyn ? field.type.kind !== "dyn" : field.type.kind !== "f64") {
err(`fieldIncDec ${e.className}.${e.field} field/flag mismatch (${field.type.kind})`, e.loc);
}
}
if (e.type.kind !== "f64") err("fieldIncDec must be f64", e.loc);
break;
}
case "assignExpr": {
// Expression-position `x = e`: a mutable local or module global,
// written with a value of its own type; the expression yields it.
checkExpr(e.value);
const binding = locals.get(e.localId) ?? globals.get(e.localId);
if (!binding) err(`assignExpr to undeclared local/global "${e.localId}"`, e.loc);
else {
if (!binding.mutable && locals.has(e.localId)) {
err(`assignExpr to immutable local "${binding.name}"`, e.loc);
}
if (!typeEquals(binding.type, e.type)) {
err(`assignExpr type must match binding "${binding.name}" (${binding.type.kind} vs ${e.type.kind})`, e.loc);
}
}
if (!typeEquals(e.value.type, e.type)) {
err(`assignExpr value type must match its own type`, e.loc);
}
break;
}
case "seqExpr": {
// Statements in an expression cannot jump out of the expression,
// but a local state branch is valid: optional array stores must
// choose ARRAY_VALUE versus ARRAY_UNDEFINED before the final result
// is evaluated. Nested blocks/ifs are checked recursively below.
const allowed = new Set(["varDecl", "assign", "exprStmt", "fieldSet", "recordSet", "recordKeySet", "arraySet", "arraySetLength", "arraySetUndefined", "arrayDelete", "bytesSet", "block", "if"]);
const flat = (ss: IrStmt[]): void => {
for (const s of ss) {
if (!allowed.has(s.kind)) {
err(`seqExpr statement kind "${s.kind}" is not straight-line`, s.loc);
continue;
}
if (s.kind === "block") {
flat(s.body);
continue;
}
if (s.kind === "if") {
flat(s.then);
if (s.else_) flat(s.else_);
checkExpr(s.cond);
continue;
}
checkStmt(s);
}
};
flat(e.stmts);
checkExpr(e.result);
if (!typeEquals(e.result.type, e.type)) err(`seqExpr type must match its result`, e.loc);
break;
}
case "dynDestrCheck": {
checkExpr(e.value);
if (e.value.type.kind !== "dyn" && e.value.type.kind !== "jsval") err("dynDestrCheck value must be dyn or jsval", e.loc);
if (e.type.kind !== e.value.type.kind) err("dynDestrCheck must have its value's type", e.loc);
break;
}
case "dynIterN": {
checkExpr(e.value);
if (e.value.type.kind !== "dyn" && e.value.type.kind !== "jsval") err("dynIterN value must be dyn or jsval", e.loc);
if (e.type.kind !== e.value.type.kind) err("dynIterN must have its value's type", e.loc);
if (!Number.isInteger(e.count) || e.count < 0) err("dynIterN count must be a non-negative integer", e.loc);
break;
}
case "toBool":
checkExpr(e.operand);
if (
e.operand.type.kind !== "f64" &&
e.operand.type.kind !== "string" &&
e.operand.type.kind !== "dyn" &&
e.operand.type.kind !== "union" &&
!REF_TRUTHY_KINDS.has(e.operand.type.kind)
) {
err(`toBool operand must be f64|string|dyn|union|ref, got ${e.operand.type.kind}`, e.loc);
}
if (e.operand.type.kind === "union") checkTruthyUnion(e.operand.type.unionId, e.loc);
if (e.type.kind !== "bool") err("toBool must be bool", e.loc);
break;
case "logical":
checkExpr(e.left);
checkExpr(e.right);
if (
e.type.kind !== "f64" && e.type.kind !== "string" && e.type.kind !== "bool" &&
e.type.kind !== "jsval" && e.type.kind !== "union" && e.type.kind !== "dyn"
) {
err(`logical ${e.op} must be f64|string|bool|jsval|union|dyn, got ${e.type.kind}`, e.loc);
}
if (e.type.kind === "union") checkTruthyUnion(e.type.unionId, e.loc);
expectType(e.left, e.type, `logical ${e.op} left`);
expectType(e.right, e.type, `logical ${e.op} right`);
break;
case "unionEq": {
checkExpr(e.left);
checkExpr(e.right);
if (!unions.has(e.unionId)) err(`unionEq of unknown union ${e.unionId}`, e.loc);
const ut: IrType = { kind: "union", unionId: e.unionId };
expectType(e.left, ut, "unionEq left");
expectType(e.right, ut, "unionEq right");
if (e.type.kind !== "bool") err("unionEq must be bool", e.loc);
break;
}
case "unionFuncEq": {
checkExpr(e.union);
checkExpr(e.func);
const def = unions.get(e.unionId);
if (!def) err(`unionFuncEq of unknown union ${e.unionId}`, e.loc);
expectType(e.union, { kind: "union", unionId: e.unionId }, "unionFuncEq union");
const arm = def?.arms[e.tag];
if (!Number.isInteger(e.tag) || arm?.kind !== "func") {
err(`unionFuncEq tag ${e.tag} must select a function arm of ${e.unionId}`, e.loc);
}
if ((def?.arms.filter((candidate) => candidate.kind === "func").length ?? 0) !== 1) {
err(`unionFuncEq requires exactly one function arm in ${e.unionId}`, e.loc);
}
if (e.func.type.kind !== "func") err("unionFuncEq function operand must be func", e.loc);
if (e.type.kind !== "bool") err("unionFuncEq must be bool", e.loc);
break;
}
case "strConcat":
checkExpr(e.left);
checkExpr(e.right);
expectType(e.left, STRING, "strConcat left");
expectType(e.right, STRING, "strConcat right");
if (e.type.kind !== "string") err("strConcat must be string", e.loc);
break;
case "strEq":
case "strCmp":
checkExpr(e.left);
checkExpr(e.right);
expectType(e.left, STRING, `${e.kind} left`);
expectType(e.right, STRING, `${e.kind} right`);
if (e.type.kind !== "bool") err(`${e.kind} must be bool`, e.loc);
break;
case "ternary":
checkExpr(e.cond);
checkExpr(e.then);
checkExpr(e.else_);
expectType(e.cond, BOOL, "ternary condition");
expectType(e.then, e.type, "ternary then-branch");
expectType(e.else_, e.type, "ternary else-branch");
if (e.type.kind === "void") err("ternary must not be void", e.loc);
break;
case "optChain": {
checkExpr(e.receiver);
// An island-handle chain (`x?.y` on 'any'): the nullish test is a
// runtime ask of the engine value; the body is the plain island
// operation over the bound handle, and both body and result stay
// jsval (the unit path is the engine's undefined).
if (e.receiver.type.kind === "jsval") {
if (activeChains.has(e.id)) err(`optChain id "${e.id}" shadows an active chain`, e.loc);
activeChains.set(e.id, JSVAL);
checkExpr(e.body);
activeChains.delete(e.id);
if (e.body.type.kind !== "jsval") {
err(`jsval optChain with ${e.body.type.kind} body`, e.loc);
}
if (e.type.kind !== "jsval") {
err(`jsval optChain must be jsval, got ${e.type.kind}`, e.loc);
}
break;
}
// A dyn (dyn) chain (`rawName?.match(re)` on a JSON.parse result):
// the nullish test reads the node's kind tag; the body is the
// validated dynamic dispatch, its result converted back into the
// dyn (dynFrom) — so body and result are dyn, or void for
// statement-position chains.
if (e.receiver.type.kind === "dyn") {
if (activeChains.has(e.id)) err(`optChain id "${e.id}" shadows an active chain`, e.loc);
activeChains.set(e.id, DYN);
checkExpr(e.body);
activeChains.delete(e.id);
if (e.body.type.kind !== "dyn" && e.body.type.kind !== "void") {
err(`dyn optChain with ${e.body.type.kind} body`, e.loc);
}
if (e.type.kind !== "dyn" && e.type.kind !== "void") {
err(`dyn optChain must be dyn or void, got ${e.type.kind}`, e.loc);
}
if (e.type.kind === "void" && e.body.type.kind !== "void") {
err(`void dyn optChain with ${e.body.type.kind} body`, e.loc);
}
break;
}
if (e.receiver.type.kind !== "union") {
err(`optChain receiver must be a union, got ${e.receiver.type.kind}`, e.loc);
break;
}
const def = unions.get(e.receiver.type.unionId);
if (!def) {
err(`optChain receiver references unknown union ${e.receiver.type.unionId}`, e.loc);
break;
}
const rest = def.arms.filter((a) => !isUnitType(a));
if (rest.length !== 1 || rest.length === def.arms.length) {
err("optChain receiver must have unit arms and exactly one non-unit arm", e.loc);
break;
}
if (activeChains.has(e.id)) err(`optChain id "${e.id}" shadows an active chain`, e.loc);
activeChains.set(e.id, rest[0]!);
checkExpr(e.body);
activeChains.delete(e.id);
if (e.type.kind === "void") {
if (e.body.type.kind !== "void") {
err(`void optChain with ${e.body.type.kind} body`, e.loc);
}
break;
}
expectType(e.body, e.type, "optChain body");
// dyn results carry the unit path as the undefined dyn value; every
// other value result needs an undefined arm to land on.
if (e.type.kind !== "dyn") {
const rdef = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
if (!rdef || !rdef.arms.some((a) => a.kind === "undefinedT")) {
err("optChain type must be void, dyn, or an undefined-armed union", e.loc);
}
}
break;
}
case "chainRecv": {
const bound = activeChains.get(e.id);
if (!bound) {
err(`chainRecv "${e.id}" outside its optChain body`, e.loc);
break;
}
expectType(e, bound, "chainRecv");
break;
}
case "nullish": {
checkExpr(e.left);
checkExpr(e.right);
expectType(e.right, e.type, "nullish right operand");
// The ISLAND form: `a ?? b` over an engine value — left, right,
// and result are all handles (the emitters' jsval nullish arm).
if (e.left.type.kind === "jsval") {
if (e.type.kind !== "jsval") err("jsval nullish must answer jsval", e.loc);
break;
}
// The CHECKED-DYNAMIC form: the runtime kind decides (the
// emitters' scr_dyn_is_nullish arm) — left, right, and result
// all live in the checked-dynamic tree.
if (e.left.type.kind === "dyn") {
if (e.type.kind !== "dyn") err("dyn nullish must answer dyn", e.loc);
break;
}
if (e.left.type.kind !== "union") {
err(`nullish left must be a union, got ${e.left.type.kind}`, e.loc);
break;
}
const def = unions.get(e.left.type.unionId);
if (!def) {
err(`nullish left references unknown union ${e.left.type.unionId}`, e.loc);
break;
}
if (!def.arms.some(isUnitType)) {
err("nullish left union has no unit arm (frontend must fence)", e.loc);
}
// Two shapes: pass-through (type === left's union) or narrowed
// (type === the union's SINGLE non-unit arm).
if (!typeEquals(e.type, e.left.type)) {
const rest = def.arms.filter((a) => !isUnitType(a));
if (rest.length !== 1 || !typeEquals(e.type, rest[0]!)) {
err("nullish type must be the left union or its single non-unit arm", e.loc);
}
}
break;
}
case "orDefault": {
checkExpr(e.left);
checkExpr(e.right);
expectType(e.right, e.type, "orDefault right operand");
if (e.left.type.kind !== "union") {
err(`orDefault left must be a union, got ${e.left.type.kind}`, e.loc);
break;
}
const def = unions.get(e.left.type.unionId);
if (!def) {
err(`orDefault left references unknown union ${e.left.type.unionId}`, e.loc);
break;
}
// Retagged shape: the truthy side is a call to the named helper, so
// the arm count is free and the type rule is the helper's signature.
if (e.retag !== undefined) {
const helper = functions.get(e.retag);
if (!helper) {
err(`orDefault retag calls undeclared function "${e.retag}"`, e.loc);
} else if (helper.params.length !== 1 || !typeEquals(helper.params[0]!.type, e.left.type)) {
err(`orDefault retag ${e.retag} must take exactly the left union`, e.loc);
} else if (!typeEquals(callSiteReturnType(helper), e.type)) {
err(`orDefault retag ${e.retag} must return the node's type`, e.loc);
}
break;
}
const rest = def.arms.filter((a) => !isUnitType(a));
if (rest.length !== 1 || !typeEquals(e.type, rest[0]!)) {
err("orDefault type must be the left union's single non-unit arm", e.loc);
}
break;
}
case "toString":
checkExpr(e.operand);
if (e.operand.type.kind === "union") {
// Union operands dispatch through the per-union ToString helper;
// every arm needs a text (unit/string/f64/bool, plus the Buffer
// arm whose toString IS the utf8 decode — the frontend fences
// unions with other ref arms, where JS prints "[object Object]").
const def = unions.get(e.operand.type.unionId);
if (!def) {
err(`toString of unknown union ${e.operand.type.unionId}`, e.loc);
} else if (
!def.arms.every(
(a) =>
a.kind === "undefinedT" || a.kind === "nullT" ||
a.kind === "string" || a.kind === "f64" || a.kind === "bool" ||
(a.kind === "bytes" && a.elem === "u8"),
)
) {
err("toString union operand has a non-stringable arm (frontend must fence)", e.loc);
}
} else if (
e.operand.type.kind !== "f64" &&
e.operand.type.kind !== "bool" &&
e.operand.type.kind !== "caught" &&
e.operand.type.kind !== "dyn" &&
// Plain data records print Object.prototype.toString's constant
// (tuples and toString-field shapes are fenced in the frontend).
e.operand.type.kind !== "record"
) {
err(`toString operand must be f64|bool|caught|dyn|record, got ${e.operand.type.kind}`, e.loc);
}
if (e.type.kind !== "string") err("toString must be string", e.loc);
break;
case "strIntrinsic": {
checkExpr(e.receiver);
expectType(e.receiver, STRING, `strIntrinsic ${e.method} receiver`);
const sig = STR_INTRINSIC_SIGS[e.method];
if (e.args.length < sig.minArgs || e.args.length > sig.argTypes.length) {
const want =
sig.minArgs === sig.argTypes.length
? `${sig.argTypes.length}`
: `${sig.minArgs}-${sig.argTypes.length}`;
err(`strIntrinsic ${e.method}: ${e.args.length} args, expected ${want}`, e.loc);
}
e.args.forEach((a, i) => {
checkExpr(a);
const want = sig.argTypes[i];
if (want) expectType(a, want, `strIntrinsic ${e.method} arg ${i}`);
});
if (!typeEquals(e.type, sig.result)) {
err(`strIntrinsic ${e.method} must be ${sig.result.kind}, got ${e.type.kind}`, e.loc);
}
break;
}
case "regexLit": {
if (e.type.kind !== "regex") err("regexLit must be regex-typed", e.loc);
if (!/^[gimsuy]*$/.test(e.flags)) {
err(`regexLit flags "${e.flags}" outside the supported alphabet (gimsuy)`, e.loc);
}
if (new Set(e.flags).size !== e.flags.length) {
err(`regexLit flags "${e.flags}" contain a duplicate`, e.loc);
}
break;
}
case "templateStrings": {
if (e.type.kind !== "array" || e.type.elem.kind !== "string") {
err("templateStrings must be string[]-typed", e.loc);
}
if (e.key === "") err("templateStrings key must be non-empty", e.loc);
break;
}
case "regexIntrinsic": {
checkExpr(e.receiver);
const sig = REGEX_INTRINSIC_SIGS[e.method];
expectType(e.receiver, sig.receiver, `regexIntrinsic ${e.method} receiver`);
if (e.args.length !== sig.argTypes.length) {
err(`regexIntrinsic ${e.method}: ${e.args.length} args, expected ${sig.argTypes.length}`, e.loc);
}
e.args.forEach((a, i) => {
checkExpr(a);
const want = sig.argTypes[i];
if (want) expectType(a, want, `regexIntrinsic ${e.method} arg ${i}`);
});
if (e.method === "match") {
// The `string[] | null` union (program-dependent id) — checked
// by arms, like the libCall case checks process.envGet.
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok =
def &&
def.arms.length === 2 &&
def.arms.some((a) => a.kind === "array" && a.elem.kind === "string") &&
def.arms.some((a) => a.kind === "nullT");
if (!ok) err("regexIntrinsic match must be the string[] | null union", e.loc);
break;
}
if (!typeEquals(e.type, sig.result)) {
err(`regexIntrinsic ${e.method} must be ${sig.result.kind}, got ${e.type.kind}`, e.loc);
}
break;
}
case "arrayLit": {
if (e.type.kind !== "array") {
err(`arrayLit must be array-typed, got ${e.type.kind}`, e.loc);
break;
}
const elem = e.type.elem;
if (!isSupportedArrayElem(elem)) {
err(`arrayLit with unsupported ${elem.kind} elements (frontend must fence)`, e.loc);
}
const spreadSet = new Set(e.spreads ?? []);
for (const i of spreadSet) {
if (!Number.isInteger(i) || i < 0 || i >= e.elems.length) {
err(`arrayLit spread index ${i} out of range`, e.loc);
}
}
e.elems.forEach((el, i) => {
checkExpr(el);
// Spread positions hold a same-typed ARRAY (copied element-wise);
// plain positions hold one element.
expectType(el, spreadSet.has(i) ? e.type : elem, `arrayLit element ${i}`);
});
break;
}
case "arrayNewLen": {
// Mapper-less Array.from({length: n}): absent-slot fill exists
// only for refcounted element kinds (frontend fences scalars).
if (e.type.kind !== "array") {
err(`arrayNewLen must be array-typed, got ${e.type.kind}`, e.loc);
break;
}
if (!isRefCounted(e.type.elem)) {
err(`arrayNewLen with non-refcounted ${e.type.elem.kind} elements (no absent value)`, e.loc);
}
if (!isSupportedArrayElem(e.type.elem)) {
err(`arrayNewLen with unsupported ${e.type.elem.kind} elements (frontend must fence)`, e.loc);
}
checkExpr(e.length);
expectType(e.length, F64, "arrayNewLen length");
break;
}
case "arrayGet": {
checkExpr(e.arr);
checkExpr(e.index);
expectType(e.index, F64, "arrayGet index");
if (e.arr.type.kind !== "array") {
err(`arrayGet on non-array ${e.arr.type.kind}`, e.loc);
} else if (!typeEquals(e.type, e.arr.type.elem)) {
err(`arrayGet result ${e.type.kind} != element ${e.arr.type.elem.kind}`, e.loc);
}
break;
}
case "arrayHas": {
checkExpr(e.arr);
checkExpr(e.index);
expectType(e.index, F64, "arrayHas index");
if (!typeEquals(e.type, BOOL)) err(`arrayHas result ${e.type.kind} != bool`, e.loc);
if (e.arr.type.kind !== "array") {
err(`arrayHas on non-array ${e.arr.type.kind}`, e.loc);
}
break;
}
case "arrayState": {
checkExpr(e.arr);
checkExpr(e.index);
expectType(e.index, F64, "arrayState index");
if (!typeEquals(e.type, F64)) err(`arrayState result ${e.type.kind} != f64`, e.loc);
if (e.arr.type.kind !== "array") err(`arrayState on non-array ${e.arr.type.kind}`, e.loc);
break;
}
case "bytesNew": {
if (e.type.kind !== "bytes") {
err(`bytesNew of non-bytes type ${e.type.kind}`, e.loc);
break;
}
if (e.source) {
checkExpr(e.source);
const sk = e.source.type;
if (sk.kind === "bytes") {
// Same-elem copies only (cross-kind construction is fenced).
if (!typeEquals(sk, e.type)) {
err(`bytesNew copy source elem mismatch`, e.loc);
}
} else if (sk.kind === "array") {
if (sk.elem.kind !== "f64") {
err(`bytesNew array source must hold f64, got ${sk.elem.kind}`, e.loc);
}
} else if (sk.kind !== "f64") {
err(`bytesNew source of kind ${sk.kind}`, e.loc);
}
}
break;
}
case "bytesIntrinsic": {
checkExpr(e.receiver);
if (e.receiver.type.kind !== "bytes") {
err(`bytesIntrinsic ${e.method} on non-bytes ${e.receiver.type.kind}`, e.loc);
break;
}
const recv = e.receiver.type;
// The dvGet* getters read through a DataView — always a bytes<u8>
// view; dataViewNew and byteOffset take ANY elem kind (views form
// over any typed array's storage, owners answer byteOffset 0).
const isDvGet = e.method.startsWith("dvGet");
const isNum =
e.method === "readNum" || e.method === "writeNum" || e.method === "readNumVar" || e.method === "writeNumVar";
const U8_ONLY_EXTRA: ReadonlySet<string> = new Set([
"toString", "toStringVar", "equals", "compareBuf", "indexOf", "lastIndexOf", "includes",
"indexOfNum", "lastIndexOfNum", "includesNum", "fill", "fillNum", "fillStr",
"copy", "swap16", "swap32", "swap64", "writeStr",
]);
const u8Only = U8_ONLY_EXTRA.has(e.method) || isNum || isDvGet;
if (u8Only && recv.elem !== "u8") {
err(`bytesIntrinsic ${e.method} on a ${recv.elem} receiver (u8 only)`, e.loc);
}
// The numeric families' kind rides as args[0] — ALWAYS a string
// literal (the backend maps it to the runtime tag at compile
// time; a runtime-valued kind has no meaning).
if (isNum && e.args[0]?.kind !== "strLit") {
err(`bytesIntrinsic ${e.method} args[0] must be a strLit kind token`, e.loc);
}
// fillStr/writeStr carry their NORMALIZED encoding as args[1],
// always a strLit (the frontend folds the aliases).
if ((e.method === "fillStr" || e.method === "writeStr") && e.args[1]?.kind !== "strLit") {
err(`bytesIntrinsic ${e.method} args[1] must be a strLit encoding`, e.loc);
}
const EXTRA_SIGS: Record<string, { argTypes: IrType[]; minArgs: number; result: IrType } | undefined> = {
equals: { argTypes: [BYTES_U8], minArgs: 1, result: BOOL },
compareBuf: { argTypes: [BYTES_U8, F64, F64, F64, F64], minArgs: 1, result: F64 },
// [needle, align, byteOffset?] — an OMITTED byteOffset is Node's
// search-everything default (the backend passes NaN).
indexOf: { argTypes: [BYTES_U8, F64, F64], minArgs: 2, result: F64 },
lastIndexOf: { argTypes: [BYTES_U8, F64, F64], minArgs: 2, result: F64 },
includes: { argTypes: [BYTES_U8, F64, F64], minArgs: 2, result: BOOL },
indexOfNum: { argTypes: [F64, F64], minArgs: 1, result: F64 },
lastIndexOfNum: { argTypes: [F64, F64], minArgs: 1, result: F64 },
includesNum: { argTypes: [F64, F64], minArgs: 1, result: BOOL },
fill: { argTypes: [BYTES_U8, F64, F64], minArgs: 1, result: BYTES_U8 },
fillNum: { argTypes: [F64, F64, F64], minArgs: 1, result: BYTES_U8 },
// Per-element TypedArray fill (any elem — the non-u8 fill path).
fillElem: { argTypes: [F64, F64, F64], minArgs: 1, result: bytesOf(recv.elem) },
fillStr: { argTypes: [STRING, STRING, F64, F64], minArgs: 2, result: BYTES_U8 },
copy: { argTypes: [BYTES_U8, F64, F64, F64], minArgs: 1, result: F64 },
swap16: { argTypes: [], minArgs: 0, result: BYTES_U8 },
swap32: { argTypes: [], minArgs: 0, result: BYTES_U8 },
swap64: { argTypes: [], minArgs: 0, result: BYTES_U8 },
writeStr: { argTypes: [STRING, STRING, F64, F64], minArgs: 3, result: F64 },
};
// Object.hasOwn: "toString" is a real method name here and must
// not answer from Object.prototype.
const extraSig = Object.hasOwn(EXTRA_SIGS, e.method) ? EXTRA_SIGS[e.method] : undefined;
const sig: { argTypes: IrType[]; minArgs: number; result: IrType } = extraSig ?? (
e.method === "length" || e.method === "byteLength" || e.method === "byteOffset"
? { argTypes: [], minArgs: 0, result: F64 }
: e.method === "get"
? { argTypes: [F64], minArgs: 1, result: F64 }
: e.method === "slice" || e.method === "subarray"
? { argTypes: [F64, F64], minArgs: 0, result: bytesOf(recv.elem) }
: e.method === "toReversed"
? { argTypes: [], minArgs: 0, result: bytesOf(recv.elem) }
: e.method === "with"
? { argTypes: [F64, F64], minArgs: 2, result: bytesOf(recv.elem) }
: e.method === "join"
? { argTypes: [STRING], minArgs: 1, result: STRING }
: e.method === "toArray"
? { argTypes: [], minArgs: 0, result: arrayOf(F64) }
: e.method === "setFrom"
? { argTypes: [bytesOf(recv.elem), F64], minArgs: 1, result: VOID }
: e.method === "toString" || e.method === "toStringVar"
? { argTypes: [STRING, F64, F64], minArgs: 1, result: STRING }
: e.method === "readNum"
? { argTypes: [STRING, F64], minArgs: 2, result: F64 }
: e.method === "writeNum"
? { argTypes: [STRING, F64, F64], minArgs: 3, result: F64 }
: e.method === "readNumVar"
? { argTypes: [STRING, F64, F64], minArgs: 3, result: F64 }
: e.method === "writeNumVar"
? { argTypes: [STRING, F64, F64, F64], minArgs: 4, result: F64 }
: e.method === "dataViewNew"
? { argTypes: [F64, F64], minArgs: 0, result: BYTES_U8 }
: e.method.startsWith("dvSet")
? {
// dvSet*: [offset, value], the 8-bit setters take no littleEndian.
argTypes:
e.method === "dvSetUint8" || e.method === "dvSetInt8"
? [F64, F64]
: [F64, F64, BOOL],
minArgs: 2,
result: VOID,
}
: {
// dvGet*: the 8-bit getters take no littleEndian.
argTypes:
e.method === "dvGetUint8" || e.method === "dvGetInt8" ? [F64] : [F64, BOOL],
minArgs: 1,
result: F64,
});
if (e.args.length < sig.minArgs || e.args.length > sig.argTypes.length) {
err(`bytesIntrinsic ${e.method}: ${e.args.length} args`, e.loc);
}
e.args.forEach((a, i) => {
checkExpr(a);
const want = sig.argTypes[i];
if (want) expectType(a, want, `bytesIntrinsic ${e.method} arg ${i}`);
});
if (!typeEquals(e.type, sig.result)) {
err(`bytesIntrinsic ${e.method} result must be ${sig.result.kind}, got ${e.type.kind}`, e.loc);
}
break;
}
case "arrIntrinsic": {
checkExpr(e.receiver);
if (e.receiver.type.kind !== "array") {
err(`arrIntrinsic ${e.method} on non-array ${e.receiver.type.kind}`, e.loc);
break;
}
const elem = e.receiver.type.elem;
const sig =
e.method === "push" || e.method === "unshift"
? { argTypes: e.args.map(() => elem), result: F64 }
: e.method === "pushSpread" || e.method === "concatSpread" || e.method === "unshiftSpread"
? { argTypes: [e.receiver.type], result: F64 }
: e.method === "nextPresent" || e.method === "getNumber"
? { argTypes: [F64], result: F64 }
: e.method === "pop"
? { argTypes: [], result: e.type } // union-checked below
: e.method === "indexOf"
? { argTypes: [elem], result: F64 }
: e.method === "includes"
? { argTypes: [elem], result: BOOL }
: e.method === "join"
? { argTypes: [STRING], result: STRING }
: e.method === "slice"
? { argTypes: [F64, F64], result: e.receiver.type }
: e.method === "toReversed"
? { argTypes: [], result: e.receiver.type }
: e.method === "reverse"
? { argTypes: [], result: e.receiver.type }
: e.method === "toSpliced"
? { argTypes: [F64, F64, e.receiver.type], result: e.receiver.type }
: e.method === "with"
? { argTypes: [F64, elem], result: e.receiver.type }
: e.method === "withUndefined"
? { argTypes: [F64], result: e.receiver.type }
: e.method === "splice"
? { argTypes: [F64, F64], result: e.receiver.type }
: e.method === "shift"
? { argTypes: [], result: e.type } // union-checked below
: { argTypes: [], result: F64 }; // length
if (e.method === "getNumber" && elem.kind !== "f64") {
err(`arrIntrinsic getNumber requires f64 elements, got ${elem.kind}`, e.loc);
}
if (
e.method === "join" &&
elem.kind !== "f64" && elem.kind !== "string" && elem.kind !== "bool" &&
!(
elem.kind === "union" &&
(unions
.get(elem.unionId)
?.arms.every(
(a) => a.kind === "f64" || a.kind === "string" || a.kind === "bool" || isUnitType(a),
) ??
false)
)
) {
err(`arrIntrinsic join on ${elem.kind} elements (frontend must reject)`, e.loc);
}
if ((e.method === "indexOf" || e.method === "includes") && elem.kind === "union") {
// Union boxes are compiler artifacts — pointer identity would
// misjudge JS ===; the frontend fences these.
err(`arrIntrinsic ${e.method} on union elements (frontend must reject)`, e.loc);
}
if (e.method === "shift" || e.method === "pop") {
const rdef = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
if (
!rdef ||
!rdef.arms.some((arm) => arm.kind === "undefinedT") ||
!(elem.kind === "union" ? typeEquals(elem, e.type) : rdef.arms.some((arm) => typeEquals(arm, elem)))
) {
err(`arrIntrinsic ${e.method} result must be the elem|undefined union`, e.loc);
}
}
// slice's indices and splice's count are optional (omitted args
// omitted from the IR — backends fill the defaults); everything
// else is exact.
const minArgs = e.method === "slice" ? 0 : e.method === "splice" ? 1 : sig.argTypes.length;
if (e.args.length < minArgs || e.args.length > sig.argTypes.length) {
err(`arrIntrinsic ${e.method}: ${e.args.length} args, expected ${sig.argTypes.length}`, e.loc);
}
e.args.forEach((a, i) => {
checkExpr(a);
const want = sig.argTypes[i];
if (want) expectType(a, want, `arrIntrinsic ${e.method} arg ${i}`);
});
if (!typeEquals(e.type, sig.result)) {
err(`arrIntrinsic ${e.method} must be ${sig.result.kind}, got ${e.type.kind}`, e.loc);
}
break;
}
case "mapNew": {
if (e.type.kind !== "map") {
err(`mapNew must be map-typed, got ${e.type.kind}`, e.loc);
break;
}
if (!isSupportedMapKey(e.type.key)) {
err(`mapNew key kind ${e.type.key.kind} (frontend must fence)`, e.loc);
}
if (!isSupportedMapValue(e.type.value)) {
err(`mapNew value kind ${e.type.value.kind} (frontend must fence)`, e.loc);
}
// Seed entries lower pairwise, K/V-typed exactly like set() args.
for (const pair of e.seed ?? []) {
checkExpr(pair.key);
expectType(pair.key, e.type.key, "mapNew seed key");
checkExpr(pair.value);
expectType(pair.value, e.type.value, "mapNew seed value");
}
break;
}
case "mapIntrinsic": {
checkExpr(e.receiver);
if (e.receiver.type.kind !== "map") {
err(`mapIntrinsic ${e.method} on non-map ${e.receiver.type.kind}`, e.loc);
break;
}
const { key, value } = e.receiver.type;
if (e.method === "get") {
// Result is the interned `V | undefined` union: an undefined arm
// must exist, and every OTHER arm must be V (V non-union) or one
// of V's arms IN ORDER (V union — `undefined` sorts last in
// canonical arm order, so tags coincide and the backend can hand
// the stored box straight through).
if (e.args.length !== 1) {
err(`mapIntrinsic get: ${e.args.length} args, expected 1`, e.loc);
break;
}
checkExpr(e.args[0]!);
expectType(e.args[0]!, key, "mapIntrinsic get key");
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const rest = def ? def.arms.filter((a) => a.kind !== "undefinedT") : [];
// When V is itself a union its own undefined arm (if any) folds
// into the result's — compare the non-undefined arms pairwise.
const varms =
value.kind === "union"
? (unions.get(value.unionId)?.arms ?? []).filter((a) => a.kind !== "undefinedT")
: [value];
const ok =
def &&
rest.length === def.arms.length - 1 &&
def.arms[def.arms.length - 1]!.kind === "undefinedT" &&
rest.length === varms.length &&
rest.every((a, i) => typeEquals(a, varms[i]!));
if (!ok) {
err(`mapIntrinsic get must return the 'V | undefined' union`, e.loc);
}
break;
}
const sig =
e.method === "set"
? { argTypes: [key, value], result: VOID }
: e.method === "has" || e.method === "delete"
? { argTypes: [key], result: BOOL }
: e.method === "clear" || e.method === "iterEnter" || e.method === "iterExit"
? { argTypes: [], result: VOID }
: e.method === "iterLive"
? { argTypes: [F64], result: BOOL }
: e.method === "iterKey"
? { argTypes: [F64], result: key }
: e.method === "iterValue"
? { argTypes: [F64], result: value }
: { argTypes: [], result: F64 }; // size / iterCount
if (e.args.length !== sig.argTypes.length) {
err(`mapIntrinsic ${e.method}: ${e.args.length} args, expected ${sig.argTypes.length}`, e.loc);
}
e.args.forEach((a, i) => {
checkExpr(a);
const want = sig.argTypes[i];
if (want) expectType(a, want, `mapIntrinsic ${e.method} arg ${i}`);
});
if (!typeEquals(e.type, sig.result)) {
err(`mapIntrinsic ${e.method} must be ${sig.result.kind}, got ${e.type.kind}`, e.loc);
}
break;
}
case "setNew": {
if (e.type.kind !== "set") {
err(`setNew must be set-typed, got ${e.type.kind}`, e.loc);
break;
}
if (!isSupportedSetElem(e.type.elem)) {
err(`setNew element kind ${e.type.elem.kind} (frontend must fence)`, e.loc);
}
// The seed is one T[]-typed expression (T = the element type).
if (e.seed) {
checkExpr(e.seed);
expectType(e.seed, arrayOf(e.type.elem), "setNew seed");
}
break;
}
case "setIntrinsic": {
checkExpr(e.receiver);
if (e.receiver.type.kind !== "set") {
err(`setIntrinsic ${e.method} on non-set ${e.receiver.type.kind}`, e.loc);
break;
}
const { elem } = e.receiver.type;
const sig =
e.method === "add"
? { argTypes: [elem], result: VOID }
: e.method === "has" || e.method === "delete"
? { argTypes: [elem], result: BOOL }
: e.method === "clear" || e.method === "iterEnter" || e.method === "iterExit"
? { argTypes: [], result: VOID }
: e.method === "iterLive"
? { argTypes: [F64], result: BOOL }
: e.method === "iterKey"
? { argTypes: [F64], result: elem }
: e.method === "toArray"
? { argTypes: [], result: arrayOf(elem) }
: { argTypes: [], result: F64 }; // size / iterCount
if (e.args.length !== sig.argTypes.length) {
err(`setIntrinsic ${e.method}: ${e.args.length} args, expected ${sig.argTypes.length}`, e.loc);
}
e.args.forEach((a, i) => {
checkExpr(a);
const want = sig.argTypes[i];
if (want) expectType(a, want, `setIntrinsic ${e.method} arg ${i}`);
});
if (!typeEquals(e.type, sig.result)) {
err(`setIntrinsic ${e.method} must be ${sig.result.kind}, got ${e.type.kind}`, e.loc);
}
break;
}
case "call": {
for (const a of e.args) checkExpr(a);
const callee = functions.get(e.callee);
if (!callee) {
err(`call to undeclared function "${e.callee}"`, e.loc);
break;
}
if (callee.params.length !== e.args.length) {
err(`call ${e.callee}: ${e.args.length} args, expected ${callee.params.length}`, e.loc);
}
e.args.forEach((a, i) => {
const p = callee.params[i];
if (p) expectType(a, p.type, `call ${e.callee} arg ${i}`);
});
const expected = callSiteReturnType(callee);
if (!typeEquals(e.type, expected)) {
err(`call ${e.callee} type ${e.type.kind} != return ${expected.kind}`, e.loc);
}
break;
}
case "ffiCall": {
for (const arg of e.args) checkExpr(arg);
const entry = ffiByName.get(e.import);
if (!entry) {
err(`FFI call to undeclared import "${e.import}"`, e.loc);
break;
}
const sourceParamTypes = ffiSourceParamTypes(entry.params);
if (sourceParamTypes.length !== e.args.length) {
err(
`FFI call ${e.import}: ${e.args.length} args, expected ${sourceParamTypes.length}`,
e.loc,
);
}
e.args.forEach((arg, i) => {
const expectedParam = sourceParamTypes[i];
if (expectedParam !== undefined) {
expectType(arg, expectedParam, `FFI call ${e.import} arg ${i}`);
}
});
const expected = ffiClassType(entry.returns);
if (!typeEquals(e.type, expected)) {
err(
`FFI call ${e.import} type ${e.type.kind} != return class ${entry.returns}`,
e.loc,
);
}
break;
}
case "closure": {
const target = functions.get(e.fnName);
if (!target) {
err(`closure over undeclared function "${e.fnName}"`, e.loc);
break;
}
if (e.type.kind !== "func") {
err("closure must have func type", e.loc);
break;
}
const wantCaps = target.captures ?? [];
if (target.captures === undefined && e.captures.length > 0) {
err(`closure over plain function "${e.fnName}" cannot capture`, e.loc);
}
if (e.captures.length !== wantCaps.length) {
err(`closure ${e.fnName}: ${e.captures.length} captures, expected ${wantCaps.length}`, e.loc);
}
e.captures.forEach((id, i) => {
const local = locals.get(id);
const want = wantCaps[i];
if (!local) err(`closure capture of undeclared local "${id}"`, e.loc);
else if (!local.boxed) err(`closure capture "${id}" is not boxed`, e.loc);
else if (want && !typeEquals(local.type, want.type)) {
err(`closure capture "${id}" type ${local.type.kind} != ${want.type.kind}`, e.loc);
}
});
// Result func type must match the target's signature. A VARIADIC
// (rest-marked) type hides one synthetic trailing dyn-array param
// in the lifted function (the thunk fills it) — the type's
// declared params plus that one must match.
if (e.type.kind === "func") {
const wantRet = callSiteReturnType(target);
// ISLAND-REST types (restAbi jsval) SPELL their trailing engine
// array param — the lifted signature matches directly, no
// hidden slot.
const hiddenRest = e.type.rest === true && e.type.restAbi === undefined;
const declared = hiddenRest ? target.params.slice(0, -1) : target.params;
const restOk =
!hiddenRest ||
(target.params.length === e.type.params.length + 1 &&
target.params[target.params.length - 1]!.type.kind === "dyn");
if (
!restOk ||
e.type.params.length !== declared.length ||
!e.type.params.every((p, i) => typeEquals(p, declared[i]!.type)) ||
!typeEquals(e.type.ret, wantRet)
) {
err(`closure ${e.fnName}: func type does not match target signature`, e.loc);
}
}
break;
}
case "callValue": {
checkExpr(e.callee);
for (const a of e.args) checkExpr(a);
if (e.callee.type.kind !== "func") {
err(`callValue callee is ${e.callee.type.kind}, not func`, e.loc);
break;
}
const ft = e.callee.type;
if (ft.params.length !== e.args.length) {
err(`callValue: ${e.args.length} args, expected ${ft.params.length}`, e.loc);
}
e.args.forEach((a, i) => {
const p = ft.params[i];
if (p) expectType(a, p, `callValue arg ${i}`);
});
if (!typeEquals(e.type, ft.ret)) {
err(`callValue type ${e.type.kind} != return ${ft.ret.kind}`, e.loc);
}
break;
}
case "selfRef":
if (fn.captures === undefined) {
err("selfRef outside a lifted function", e.loc);
}
if (e.type.kind !== "func") err("selfRef must have func type", e.loc);
break;
case "new": {
for (const a of e.args) checkExpr(a);
const cls = classes.get(e.className);
if (!cls) {
err(`new of undeclared class "${e.className}"`, e.loc);
break;
}
const ctor = functions.get(`%${e.className}.constructor`);
if (!ctor) {
err(`new ${e.className}: missing constructor function`, e.loc);
break;
}
// arg 0 is `this` (supplied by the new expr itself)
if (ctor.params.length !== e.args.length + 1) {
err(`new ${e.className}: ${e.args.length} args, ctor expects ${ctor.params.length - 1}`, e.loc);
}
e.args.forEach((a, i) => {
const p = ctor.params[i + 1];
if (p) expectType(a, p.type, `new ${e.className} arg ${i}`);
});
if (!typeEquals(e.type, { kind: "object", className: e.className })) {
err(`new ${e.className} must have that object type`, e.loc);
}
break;
}
case "fieldGet": {
checkExpr(e.obj);
const cls = classes.get(e.className);
const field = cls?.fields.find((f) => f.name === e.field);
if (!cls) err(`fieldGet on undeclared class "${e.className}"`, e.loc);
else if (!field) err(`class ${e.className} has no field "${e.field}"`, e.loc);
else {
expectType(e.obj, { kind: "object", className: e.className }, "fieldGet receiver");
if (!typeEquals(e.type, field.type)) {
err(`fieldGet ${e.className}.${e.field} type mismatch`, e.loc);
}
}
break;
}
case "promiseVoidWiden": {
// A promise value flowing into a VOID-promise slot: one C
// representation, type-only — sound for every inner (awaiting
// through the slot ignores the fulfillment payload).
checkExpr(e.value);
if (e.value.type.kind !== "promise") {
err("promiseVoidWiden over a non-promise operand", e.loc);
}
if (e.type.kind !== "promise" || e.type.inner.kind !== "void") {
err("promiseVoidWiden must be promise<void>-typed", e.loc);
}
break;
}
case "upcast":
case "downcast": {
// A pointer reinterpret is sound exactly between hierarchy
// relatives: upcast widens a STRICT descendant to an ancestor,
// downcast (checker-trusted) narrows an ancestor to a STRICT
// descendant. Upcast additionally widens CLASS VALUES
// (classval:D → classval:C): the same pointer, type-only — legal
// exactly when D strictly descends from C AND the two completed
// constructor ABIs agree (what newValue completion rests on).
checkExpr(e.value);
if (e.kind === "upcast" && e.type.kind === "classval" && e.value.type.kind === "classval") {
const [sub, sup] = [e.value.type.className, e.type.className];
if (!isStrictSubclass(sub, sup)) {
err(`upcast: "${sub}" does not extend "${sup}"`, e.loc);
break;
}
const subCtor = functions.get(`%${sub}.constructor`);
const supCtor = functions.get(`%${sup}.constructor`);
if (!subCtor || !supCtor) {
err(`classval upcast: "${sub}"/"${sup}" lack constructor functions`, e.loc);
break;
}
const abiEqual =
subCtor.params.length === supCtor.params.length &&
subCtor.params.every((p, i) => i === 0 || typeEquals(p.type, supCtor.params[i]!.type));
if (!abiEqual) {
err(`classval upcast: "${sub}" and "${sup}" constructor ABIs differ`, e.loc);
}
break;
}
if (e.type.kind !== "object" || e.value.type.kind !== "object") {
err(`${e.kind} between non-class types`, e.loc);
break;
}
const [sub, sup] =
e.kind === "upcast"
? [e.value.type.className, e.type.className]
: [e.type.className, e.value.type.className];
if (!isStrictSubclass(sub, sup)) {
err(`${e.kind}: "${sub}" does not extend "${sup}"`, e.loc);
}
break;
}
case "classRef": {
const cls = classes.get(e.className);
if (!cls) {
err(`classRef to undeclared class "${e.className}"`, e.loc);
break;
}
if (cls.runtime) {
err(`classRef to runtime-provided class "${e.className}"`, e.loc);
}
if (cls.jsName === undefined) {
err(`classRef to "${e.className}" without a jsName (the class object's .name)`, e.loc);
}
// A class value can always be constructed through — the frontend
// notes the constructor edge at every classRef, so the emitted
// thunk has a body to call.
if (!functions.has(`%${e.className}.constructor`)) {
err(`classRef to "${e.className}" without its constructor function`, e.loc);
}
if (!typeEquals(e.type, { kind: "classval", className: e.className })) {
err(`classRef to "${e.className}" must have that classval type`, e.loc);
}
break;
}
case "newValue": {
checkExpr(e.callee);
for (const a of e.args) checkExpr(a);
if (e.callee.type.kind !== "classval") {
err(`newValue callee must be a class value, got ${e.callee.type.kind}`, e.loc);
break;
}
const cls = e.callee.type.className;
const ctor = functions.get(`%${cls}.constructor`);
if (!ctor) {
err(`newValue on "${cls}": missing constructor function`, e.loc);
break;
}
// Count-exact against the static class's completed ABI (every
// value legally in the slot shares it — the upcast rule).
if (ctor.params.length !== e.args.length + 1) {
err(`newValue on "${cls}": ${e.args.length} args, ctor expects ${ctor.params.length - 1}`, e.loc);
}
e.args.forEach((a, i) => {
const p = ctor.params[i + 1];
if (p) expectType(a, p.type, `newValue on "${cls}" arg ${i}`);
});
if (!typeEquals(e.type, { kind: "object", className: cls })) {
err(`newValue on "${cls}" must have that object type`, e.loc);
}
break;
}
case "instanceOfValue": {
checkExpr(e.value);
checkExpr(e.classValue);
if (e.type.kind !== "bool") err("instanceOfValue must be bool", e.loc);
if (e.classValue.type.kind !== "classval") {
err(`instanceOfValue target must be a class value, got ${e.classValue.type.kind}`, e.loc);
break;
}
// Both sides must be hierarchy members: the operand needs a vt
// word to read; a standalone target class has one possible value
// and the frontend folds it statically.
if (!hierarchy.has(e.classValue.type.className)) {
err(`instanceOfValue against standalone class "${e.classValue.type.className}"`, e.loc);
}
if (e.value.type.kind !== "object" || !hierarchy.has(e.value.type.className)) {
err("instanceOfValue operand is not a hierarchy class instance", e.loc);
}
break;
}
case "instanceOf": {
checkExpr(e.value);
if (e.type.kind !== "bool") err("instanceOf must be bool", e.loc);
if (!hierarchy.has(e.className)) {
err(`instanceOf against non-hierarchy class "${e.className}"`, e.loc);
}
if (e.value.type.kind !== "object" || !hierarchy.has(e.value.type.className)) {
err("instanceOf operand is not a hierarchy class instance", e.loc);
}
break;
}
case "virtualCall": {
// args[0] is the receiver, typed exactly as the static class; the
// call is well-formed against the NEAREST declaration at/above it,
// and dynamic dispatch must be reachable: some strict descendant
// overrides the method (otherwise the frontend devirtualizes).
for (const a of e.args) checkExpr(a);
const recv = e.args[0];
if (!recv || !typeEquals(recv.type, { kind: "object", className: e.className })) {
err(`virtualCall receiver must be object:${e.className}`, e.loc);
break;
}
// The nearest declaration may be ABSTRACT (no function): the call
// is then well-formed against any concrete override below — the
// frontend's override-exactness rule makes every implementation
// ABI-identical, so any one of them carries the slot's signature.
let declared = false;
let impl: IrFunction | undefined;
for (let c = classes.get(e.className); c; c = c.base !== undefined ? classes.get(c.base) : undefined) {
if (c.methods?.includes(e.method)) {
declared = true;
if (!c.abstractMethods?.includes(e.method)) {
impl = functions.get(`%${c.name}.${e.method}`);
break;
}
}
}
if (!declared) {
err(`virtualCall ${e.className}.${e.method}: no declaration on the base chain`, e.loc);
break;
}
const concreteBelow = [...classes.values()].filter(
(c) =>
c.methods?.includes(e.method) &&
!c.abstractMethods?.includes(e.method) &&
isStrictSubclass(c.name, e.className),
);
if (concreteBelow.length === 0) {
err(`virtualCall ${e.className}.${e.method}: no concrete override below the static class`, e.loc);
}
impl ??= concreteBelow
.map((c) => functions.get(`%${c.name}.${e.method}`))
.find((f) => f !== undefined);
if (!impl) {
err(`virtualCall ${e.className}.${e.method}: no implementation function exists`, e.loc);
break;
}
if (impl.params.length !== e.args.length) {
err(`virtualCall ${e.className}.${e.method}: ${e.args.length} args, method expects ${impl.params.length}`, e.loc);
}
e.args.slice(1).forEach((a, i) => {
const p = impl.params[i + 1];
if (p) expectType(a, p.type, `virtualCall ${e.className}.${e.method} arg ${i}`);
});
if (!typeEquals(e.type, callSiteReturnType(impl))) {
err(`virtualCall ${e.className}.${e.method} result type mismatch`, e.loc);
}
break;
}
case "recordLit": {
if (e.type.kind !== "record") {
err(`recordLit must be record-typed, got ${e.type.kind}`, e.loc);
break;
}
const shape = records.get(e.type.shapeId);
if (!shape) {
err(`recordLit of undeclared shape "${e.type.shapeId}"`, e.loc);
break;
}
const want = new Map(shape.fields.map((f) => [f.name, f.type]));
const seen = new Set<string>();
for (const f of e.fields) {
checkExpr(f.value);
if (seen.has(f.name)) err(`recordLit initializes field "${f.name}" twice`, e.loc);
seen.add(f.name);
if (f.drop) {
// A mapping-dropped field (the PromiseSettledResult honest
// subset): evaluated, never stored — it must NOT name a
// declared field (that would silently drop a real store),
// and its value may be any type, void included.
if (f.overflow) err(`recordLit drop entry "${f.name}" flagged overflow too`, e.loc);
if (want.has(f.name)) {
err(`recordLit drop entry "${f.name}" shadows a declared field of shape ${shape.id}`, e.loc);
}
continue;
}
if (f.overflow) {
// Overflow entries exist only on index-signature shapes, name
// no declared field, and carry the index-value type exactly
// (the frontend converts/coerces before constructing).
if (!shape.indexValue) {
err(`recordLit overflow entry "${f.name}" on non-index-signature shape ${shape.id}`, e.loc);
} else if (want.has(f.name)) {
err(`recordLit overflow entry "${f.name}" shadows a declared field`, e.loc);
} else {
expectType(f.value, shape.indexValue, `recordLit overflow entry "${f.name}"`);
}
continue;
}
const ft = want.get(f.name);
if (!ft) err(`shape ${shape.id} has no field "${f.name}"`, e.loc);
else expectType(f.value, ft, `recordLit field "${f.name}"`);
}
if (e.fields.filter((f) => !f.overflow && !f.drop).length !== shape.fields.length) {
err(`recordLit does not initialize every field of shape ${shape.id}`, e.loc);
}
break;
}
case "recordClone": {
checkExpr(e.source);
if (e.type.kind !== "record") {
err(`recordClone must be record-typed, got ${e.type.kind}`, e.loc);
break;
}
const shape = records.get(e.type.shapeId);
if (!shape) {
err(`recordClone of undeclared shape "${e.type.shapeId}"`, e.loc);
break;
}
if (shape.tuple || shape.indexValue || shapeHasAccessorSlots(shape)) {
err(`recordClone requires a plain declared-field shape, got ${shape.id}`, e.loc);
}
expectType(e.source, e.type, "recordClone source");
const want = new Map(shape.fields.map((f) => [f.name, f.type]));
const seen = new Set<string>();
for (const f of e.overrides) {
checkExpr(f.value);
if (seen.has(f.name)) err(`recordClone overrides field "${f.name}" twice`, e.loc);
seen.add(f.name);
const ft = want.get(f.name);
if (!ft) err(`shape ${shape.id} has no field "${f.name}"`, e.loc);
else expectType(f.value, ft, `recordClone field "${f.name}"`);
}
break;
}
case "recordGet": {
checkExpr(e.obj);
const shape = records.get(e.shapeId);
const field = shape?.fields.find((f) => f.name === e.field);
if (!shape) err(`recordGet on undeclared shape "${e.shapeId}"`, e.loc);
else if (!field) err(`shape ${e.shapeId} has no field "${e.field}"`, e.loc);
else {
expectType(e.obj, { kind: "record", shapeId: e.shapeId }, "recordGet receiver");
if (!typeEquals(e.type, field.type)) {
err(`recordGet ${e.shapeId}.${e.field} type mismatch`, e.loc);
}
}
break;
}
case "recordKeyGet": {
checkExpr(e.obj);
checkExpr(e.key);
const shape = records.get(e.shapeId);
if (!shape) {
err(`recordKeyGet on undeclared shape "${e.shapeId}"`, e.loc);
break;
}
if (shape.tuple) err(`recordKeyGet on tuple shape ${e.shapeId}`, e.loc);
expectType(e.obj, { kind: "record", shapeId: e.shapeId }, "recordKeyGet receiver");
expectType(e.key, STRING, "recordKeyGet key");
// Every reachable value must SURFACE as the result type: identity,
// an arm of a union result, or (dyn results) a dyn conversion —
// the frontend's recordKeyResultOk mirror. overflowOnly reads (a
// literal key naming no declared field) skip the declared check
// and require the overflow to exist.
const surfaces = (t: IrType): boolean =>
typeEquals(t, e.type) ||
(e.type.kind === "union" &&
(unions.get(e.type.unionId)?.arms.some((a) => typeEquals(a, t)) ?? false)) ||
e.type.kind === "dyn";
if (e.overflowOnly && !shape.indexValue) {
err(`recordKeyGet on ${e.shapeId}: overflowOnly read of a shape without an index signature`, e.loc);
}
if (!e.overflowOnly && !shape.fields.every((f) => surfaces(f.type))) {
err(`recordKeyGet on ${e.shapeId}: a declared field cannot surface as the result type`, e.loc);
}
if (e.type.kind === "dyn" && shape.indexValue && shape.indexValue.kind !== "dyn") {
err(`recordKeyGet on ${e.shapeId}: dyn result over a non-dyn index value`, e.loc);
}
if (shape.indexValue && e.type.kind !== "dyn" && !surfaces(shape.indexValue)) {
err(`recordKeyGet on ${e.shapeId}: the overflow value cannot surface as the result type`, e.loc);
}
break;
}
case "recordOvfKeys": {
checkExpr(e.obj);
const shape = records.get(e.shapeId);
if (!shape) {
err(`recordOvfKeys on undeclared shape "${e.shapeId}"`, e.loc);
break;
}
if (!shape.indexValue) err(`recordOvfKeys on ${e.shapeId}: no index signature`, e.loc);
expectType(e.obj, { kind: "record", shapeId: e.shapeId }, "recordOvfKeys receiver");
if (e.type.kind !== "array" || e.type.elem.kind !== "string") {
err("recordOvfKeys must be string[]", e.loc);
}
break;
}
case "dynFrom": {
if (e.type.kind !== "dyn") err(`dynFrom must be dyn-typed, got ${e.type.kind}`, e.loc);
// A bare unit literal is legal exactly here (like unionWrap): the
// dyn has first-class undefined/null values.
if (e.value.kind === "unitLit") {
const want = e.value.unit === "undefined" ? "undefinedT" : "nullT";
if (e.value.type.kind !== want) {
err(`unitLit '${e.value.unit}' typed ${e.value.type.kind}`, e.loc);
}
break;
}
checkExpr(e.value);
// Domain: JSON-safe, bytes<u8> (the checked-dynamic tree's bytes kind — payload
// copied), an undefined-armed union of JSON-safe arms (the
// undefined arm becomes the undefined dyn value), or a BOXABLE
// function type (the checked-dynamic tree's function kind — canConvertToDyn folds
// all four in).
const vt = e.value.type;
if (!canConvertToDyn(vt, (id) => records.get(id), (id) => unions.get(id))) {
err(`dynFrom of non-dyn-convertible type ${vt.kind}`, e.loc);
}
if (e.liveRef && !liveDynRefEligible(vt)) {
err(`live dynFrom of unsupported type ${vt.kind}`, e.loc);
}
break;
}
case "dynFromJsval": {
// The jsval→dyn crossing: exactly a jsval operand into a dyn
// result (the by-reference island wrap; scalars normalize at
// runtime).
checkExpr(e.value);
if (e.type.kind !== "dyn") err(`dynFromJsval must be dyn-typed, got ${e.type.kind}`, e.loc);
if (e.value.type.kind !== "jsval") {
err(`dynFromJsval operand must be jsval, got ${e.value.type.kind}`, e.loc);
}
break;
}
case "dynCall": {
checkExpr(e.callee);
expectType(e.callee, DYN, "dynCall callee");
if (e.type.kind !== "dyn") err(`dynCall must be dyn-typed, got ${e.type.kind}`, e.loc);
for (const a of e.args) {
checkExpr(a);
if (a.type.kind !== "dyn") err(`dynCall argument of kind ${a.type.kind} (must be dyn)`, e.loc);
}
// The runtime-arity form: spread entries point into args (their
// dyn values flatten at the call), strictly increasing.
if (e.spreads !== undefined) {
if (e.spreads.length === 0) err("dynCall spreads must be non-empty when present", e.loc);
let prev = -1;
for (const s of e.spreads) {
if (!Number.isInteger(s.arg) || s.arg < 0 || s.arg >= e.args.length) {
err(`dynCall spread index ${s.arg} out of range`, e.loc);
}
if (s.arg <= prev) err("dynCall spread indices must be strictly increasing", e.loc);
prev = s.arg;
}
}
break;
}
case "dynInvoke": {
checkExpr(e.recv);
expectType(e.recv, DYN, "dynInvoke receiver");
if (e.type.kind !== "dyn") err(`dynInvoke must be dyn-typed, got ${e.type.kind}`, e.loc);
for (const a of e.args) {
checkExpr(a);
if (a.type.kind !== "dyn") err(`dynInvoke argument of kind ${a.type.kind} (must be dyn)`, e.loc);
}
break;
}
case "dynObjLit":
if (e.type.kind !== "dyn") err(`dynObjLit must be dyn-typed, got ${e.type.kind}`, e.loc);
for (const f of e.fields ?? []) {
checkExpr(f.key);
if (f.key.type.kind !== "string") err(`dynObjLit key of kind ${f.key.type.kind} (must be string)`, e.loc);
checkExpr(f.value);
if (f.value.type.kind !== "dyn") err(`dynObjLit field value of kind ${f.value.type.kind} (must be dyn)`, e.loc);
}
break;
case "dynArrLit": {
if (e.type.kind !== "dyn") err(`dynArrLit must be dyn-typed, got ${e.type.kind}`, e.loc);
for (const el of e.elems) {
checkExpr(el);
if (el.type.kind !== "dyn") err(`dynArrLit element of kind ${el.type.kind} (must be dyn)`, e.loc);
}
break;
}
case "unionWrap": {
// A unitLit is legal exactly HERE: validate it inline (checkExpr
// rejects bare ones) — the unit spelling must agree with its type,
// and the generic arm/type agreement below covers the rest.
if (e.value.kind === "unitLit") {
const want = e.value.unit === "undefined" ? "undefinedT" : "nullT";
if (e.value.type.kind !== want) {
err(`unitLit '${e.value.unit}' typed ${e.value.type.kind}`, e.loc);
}
} else {
checkExpr(e.value);
if (isUnitType(e.value.type)) {
err(`unionWrap of a non-literal unit value (${e.value.kind})`, e.loc);
}
}
const def = unions.get(e.unionId);
if (!def) {
err(`unionWrap of undeclared union "${e.unionId}"`, e.loc);
break;
}
if (!typeEquals(e.type, { kind: "union", unionId: e.unionId })) {
err(`unionWrap type ${e.type.kind} != union ${e.unionId}`, e.loc);
}
const arm = def.arms[e.tag];
if (!Number.isInteger(e.tag) || !arm) {
err(`unionWrap tag ${e.tag} out of range for union ${e.unionId}`, e.loc);
} else if (e.value.type.kind === "void") {
// A VOID payload: the backends evaluate the operand for its
// effects and produce the interned unit instance — legal only
// against the undefined arm (JS's void value IS undefined).
if (arm.kind !== "undefinedT") {
err(`unionWrap of a void value against non-undefined arm ${e.tag} of ${e.unionId}`, e.loc);
}
} else if (!typeEquals(e.value.type, arm)) {
err(`unionWrap value ${e.value.type.kind} != arm ${e.tag} of ${e.unionId}`, e.loc);
}
break;
}
case "dynTest": {
checkExpr(e.value);
expectType(e.value, { kind: "dyn" }, "dynTest operand");
if (e.type.kind !== "bool") err("dynTest must be bool", e.loc);
break;
}
case "dynKeyGet": {
checkExpr(e.value);
checkExpr(e.key);
expectType(e.value, { kind: "dyn" }, "dynKeyGet operand");
if (e.key.type.kind !== "string") err(`dynKeyGet key is ${e.key.type.kind}, not string`, e.loc);
if (e.type.kind !== "dyn") err("dynKeyGet must be dyn", e.loc);
break;
}
case "dynHasKey": {
checkExpr(e.value);
expectType(e.value, { kind: "dyn" }, "dynHasKey operand");
if (e.type.kind !== "bool") err("dynHasKey must be bool", e.loc);
break;
}
case "dynScalarEq": {
checkExpr(e.left);
checkExpr(e.right);
const dynSide = e.left.type.kind === "dyn" ? e.left : e.right;
const scalarSide = dynSide === e.left ? e.right : e.left;
if (dynSide.type.kind !== "dyn") err("dynScalarEq needs a dyn side", e.loc);
if (!["f64", "string", "bool", "dyn"].includes(scalarSide.type.kind)) {
err(`dynScalarEq scalar side is ${scalarSide.type.kind}`, e.loc);
}
if (e.type.kind !== "bool") err("dynScalarEq must be bool", e.loc);
break;
}
case "caughtTest": {
checkExpr(e.value);
expectType(e.value, { kind: "caught" }, "caughtTest operand");
if (e.type.kind !== "bool") err("caughtTest must be bool", e.loc);
if (e.test === "instanceof") {
if (!e.className) err("caughtTest instanceof without a class", e.loc);
else if (!hierarchy.has(e.className)) {
err(`caughtTest instanceof against non-hierarchy class "${e.className}"`, e.loc);
}
} else if (e.className !== undefined) {
err(`caughtTest ${e.test} with a class name`, e.loc);
}
break;
}
case "caughtCheck": {
checkExpr(e.value);
expectType(e.value, { kind: "caught" }, "caughtCheck operand");
if (!hierarchy.has(e.className)) {
err(`caughtCheck against non-hierarchy class "${e.className}"`, e.loc);
}
if (e.type.kind !== "object" || e.type.className !== e.className) {
err("caughtCheck type must be the checked class's object type", e.loc);
}
break;
}
case "caughtToDyn": {
// The caught snapshot converting to a dyn value (an unknown
// slot): operand caught, result dyn — the runtime dispatch handles
// every payload kind, so nothing else constrains it.
checkExpr(e.value);
expectType(e.value, { kind: "caught" }, "caughtToDyn operand");
if (e.type.kind !== "dyn") err(`caughtToDyn must be dyn-typed, got ${e.type.kind}`, e.loc);
break;
}
case "caughtNarrow": {
checkExpr(e.value);
expectType(e.value, { kind: "caught" }, "caughtNarrow operand");
const t = e.type;
const ok =
t.kind === "f64" || t.kind === "bool" || t.kind === "string" ||
(t.kind === "object" && hierarchy.has(t.className));
if (!ok) {
err(`caughtNarrow to ${t.kind === "object" ? `non-hierarchy class "${t.className}"` : t.kind}`, e.loc);
}
break;
}
case "unionNarrow": {
checkExpr(e.value);
const def = unions.get(e.unionId);
if (!def) {
err(`unionNarrow of undeclared union "${e.unionId}"`, e.loc);
break;
}
expectType(e.value, { kind: "union", unionId: e.unionId }, "unionNarrow operand");
const arm = def.arms[e.tag];
if (!Number.isInteger(e.tag) || !arm) {
err(`unionNarrow tag ${e.tag} out of range for union ${e.unionId}`, e.loc);
} else if (!typeEquals(e.type, arm)) {
err(`unionNarrow type ${e.type.kind} != arm ${e.tag} of ${e.unionId}`, e.loc);
} else if (isUnitType(arm)) {
// A unit arm has no payload: narrowing to it produces no value,
// so the frontend never emits this (it leaves the union-typed
// expression alone in unit-narrowed branches).
err(`unionNarrow to unit arm ${e.tag} of ${e.unionId}`, e.loc);
}
break;
}
case "unionDisc": {
checkExpr(e.value);
const def = unions.get(e.unionId);
if (!def) {
err(`unionDisc of undeclared union "${e.unionId}"`, e.loc);
break;
}
expectType(e.value, { kind: "union", unionId: e.unionId }, "unionDisc receiver");
// Any representable field type reads through the tag switch (the
// emitter retains ref results uniformly); units/void can never be
// record/class field types and have no C value form.
if (e.type.kind === "void" || e.type.kind === "undefinedT" || e.type.kind === "nullT") {
err(`unionDisc of valueless type ${e.type.kind}`, e.loc);
}
def.arms.forEach((arm, i) => {
const fieldType =
arm.kind === "record"
? records.get(arm.shapeId)?.fields.find((f) => f.name === e.field)?.type
: arm.kind === "object"
? classes.get(arm.className)?.fields.find((f) => f.name === e.field)?.type
: undefined;
if (!fieldType) {
err(`unionDisc: arm ${i} of ${e.unionId} has no field "${e.field}"`, e.loc);
} else if (!typeEquals(fieldType, e.type)) {
err(`unionDisc: arm ${i} field "${e.field}" is ${fieldType.kind}, not ${e.type.kind}`, e.loc);
}
});
break;
}
case "unionKeyGet": {
checkExpr(e.value);
checkExpr(e.key);
const def = unions.get(e.unionId);
if (!def) {
err(`unionKeyGet of undeclared union "${e.unionId}"`, e.loc);
break;
}
expectType(e.value, { kind: "union", unionId: e.unionId }, "unionKeyGet receiver");
if (e.key.type.kind !== "string" && e.key.type.kind !== "f64") {
err(`unionKeyGet key is ${e.key.type.kind}, not string/number`, e.loc);
}
if (e.type.kind === "void" || e.type.kind === "undefinedT" || e.type.kind === "nullT") {
err(`unionKeyGet of valueless type ${e.type.kind}`, e.loc);
}
// Per-arm answerability: the frontend joined the answers into
// e.type — every arm must surface as it (equal or one of its arms),
// unit arms need the undefined arm, and non-record/non-array/
// non-unit arms have no keyed read at all. Number keys read ARRAY
// arms; string keys read RECORD arms.
const resultUnion = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const surfaces = (t: IrType): boolean =>
typeEquals(t, e.type) || !!resultUnion?.arms.some((a) => typeEquals(a, t));
def.arms.forEach((arm, i) => {
if (arm.kind === "undefinedT" || arm.kind === "nullT") {
if (!resultUnion?.arms.some((a) => a.kind === "undefinedT")) {
err(`unionKeyGet: unit arm ${i} of ${e.unionId} needs an undefined arm in the result`, e.loc);
}
return;
}
if (e.key.type.kind === "f64") {
if (arm.kind !== "array") {
err(`unionKeyGet: number-keyed arm ${i} of ${e.unionId} is ${arm.kind}, not an array`, e.loc);
} else if (!surfaces(arm.elem)) {
err(`unionKeyGet: arm ${i} element ${arm.elem.kind} cannot surface as the result`, e.loc);
}
return;
}
if (arm.kind !== "record") {
err(`unionKeyGet: arm ${i} of ${e.unionId} is ${arm.kind}, not a record`, e.loc);
return;
}
const shape = records.get(arm.shapeId);
if (!shape) {
err(`unionKeyGet: arm ${i} of unknown shape ${arm.shapeId}`, e.loc);
return;
}
const literal = e.key.kind === "strLit" ? e.key.value : null;
const declared = literal !== null ? shape.fields.find((f) => f.name === literal)?.type : undefined;
if (declared) {
if (!surfaces(declared)) {
err(`unionKeyGet: arm ${i} field "${literal}" of type ${declared.kind} cannot surface as the result`, e.loc);
}
return;
}
if (!shape.indexValue && !(literal === null && shape.fields.length > 0)) {
err(`unionKeyGet: arm ${i} of ${e.unionId} has no answer for the key`, e.loc);
}
});
break;
}
case "unionIsTag": {
checkExpr(e.value);
const def = unions.get(e.unionId);
if (!def) {
err(`unionIsTag of undeclared union "${e.unionId}"`, e.loc);
break;
}
expectType(e.value, { kind: "union", unionId: e.unionId }, "unionIsTag operand");
if (!Number.isInteger(e.tag) || !def.arms[e.tag]) {
err(`unionIsTag tag ${e.tag} out of range for union ${e.unionId}`, e.loc);
}
if (e.type.kind !== "bool") err("unionIsTag must be bool", e.loc);
break;
}
case "intrinsic":
if (e.name === "module.await") {
if (e.args.length !== 1) err("module.await takes exactly one argument", e.loc);
for (const a of e.args) {
checkExpr(a);
if (a.type.kind !== "promise" || a.type.inner.kind !== "void") {
err(`${typeKey(a.type)} argument to module.await (needs promise<void>)`, a.loc);
}
}
if (e.type.kind !== "void") err("module.await must be void", e.loc);
break;
}
if (e.name === "promise.all") {
// ONE argument: an array of promises whose inner type is the
// result's array element (or void, collapsing to promise<void>).
// The exact-inner-type rule is the frontend's fence; the coarse
// shape keeps hand-written IR honest.
if (e.args.length !== 1) err("promise.all takes exactly one argument", e.loc);
for (const a of e.args) {
checkExpr(a);
if (a.type.kind !== "array" || a.type.elem.kind !== "promise") {
err(`${a.type.kind} argument to promise.all (needs an array of promises)`, a.loc);
}
}
if (e.type.kind !== "promise") err("promise.all must be promise-typed", e.loc);
else if (e.type.inner.kind !== "array" && e.type.inner.kind !== "void") {
err("promise.all result must be a promise of an array (or void)", e.loc);
}
break;
}
if (e.name === "promise.reject") {
// ONE argument: the %Error-rooted reason object (the rejection
// payload shares the thrown-Error representation — the
// frontend's fence pins the hierarchy; the coarse object shape
// keeps hand-written IR honest) or a checked-dynamic reason
// (the thrown-dyn representation — identity flows to catch and
// unhandledRejection observers). The result is the
// context-named promise.
if (e.args.length !== 1) err("promise.reject takes exactly one argument", e.loc);
for (const a of e.args) {
checkExpr(a);
if (a.type.kind !== "object" && a.type.kind !== "dyn") {
err(`${a.type.kind} argument to promise.reject (needs an Error object or a dyn reason)`, a.loc);
}
}
if (e.type.kind !== "promise") err("promise.reject must be promise-typed", e.loc);
break;
}
if (e.name === "promise.resolve") {
// Zero args (Promise<void>) or one plain value of the result's
// inner type — promise arguments never reach the intrinsic
// (the frontend returns them as-is).
if (e.args.length > 1) err("promise.resolve takes at most one argument", e.loc);
for (const a of e.args) {
checkExpr(a);
if (a.type.kind === "promise") {
err("promise argument to promise.resolve (identity belongs in the frontend)", a.loc);
}
}
if (e.type.kind !== "promise") err("promise.resolve must be promise-typed", e.loc);
else if (e.args.length === 0 && e.type.inner.kind !== "void") {
err("zero-argument promise.resolve must be promise<void>", e.loc);
} else if (e.args.length === 1 && !typeEquals(e.args[0]!.type, e.type.inner)) {
err("promise.resolve argument must be the result's inner type", e.loc);
}
break;
}
if (e.name === "promise.race") {
// Entries are promises; the result is the combined promise. The
// per-entry inner-type compatibility (equal to the result inner,
// one of its union arms, or a sub-union of it) is the frontend's
// fence; here the coarse shape keeps hand-written IR honest.
if (e.args.length === 0) err("promise.race with no entries", e.loc);
for (const a of e.args) {
checkExpr(a);
if (a.type.kind !== "promise") {
err(`${a.type.kind} entry in promise.race`, a.loc);
}
}
if (e.type.kind !== "promise") err("promise.race must be promise-typed", e.loc);
break;
}
for (const a of e.args) {
checkExpr(a);
// Arrays/functions/records/unions stay out of console.log (and
// its stderr twin console.error) by design (the ambient signature
// accepts number|string|boolean; a union of those satisfies it,
// so the frontend rejects union args explicitly) — inspect
// formatting is unimplemented, so the backend must never see one.
if (a.type.kind !== "f64" && a.type.kind !== "string" && a.type.kind !== "bool") {
err(`${a.type.kind} argument to ${e.name}`, a.loc);
}
}
if (e.type.kind !== "void") err(`${e.name} must be void`, e.loc);
break;
case "libCall": {
const sig = LIB_FN_SIGS[e.fn];
if (!sig) {
err(`libCall of unknown library function "${e.fn as string}"`, e.loc);
break;
}
// emitter.emit is variadic: (recv, name) plus the event's tuple.
// The stream constructors (trailing option callbacks), write/end
// (the optional chunk/cb tail), and unpipe (the optional
// destination) admit a longer list the same way.
const variadic =
e.fn === "emitter.emit" || e.fn === "emitter.emitFlex" ||
e.fn === "readable.new" || e.fn === "writable.new" ||
e.fn === "duplex.new" || e.fn === "transform.new" ||
e.fn === "passthrough.new" ||
e.fn === "readable.init" || e.fn === "writable.init" ||
e.fn === "duplex.init" || e.fn === "transform.init" ||
e.fn === "passthrough.init" ||
e.fn === "readable.initDyn" || e.fn === "writable.initDyn" ||
e.fn === "duplex.initDyn" || e.fn === "transform.initDyn" ||
e.fn === "passthrough.initDyn" ||
e.fn === "stream.pipeline" || e.fn === "stream.pipelineDyn" ||
e.fn === "sp.pipeline" ||
e.fn === "writable.write" ||
e.fn === "writable.writeStr" || e.fn === "writable.writeU" ||
e.fn === "writable.end" ||
e.fn === "readable.unpipe";
if (variadic
? e.args.length < sig.argTypes.length
: e.args.length !== sig.argTypes.length) {
err(`libCall ${e.fn}: ${e.args.length} args, expected ${sig.argTypes.length}`, e.loc);
}
e.args.forEach((a, i) => {
checkExpr(a);
const want = sig.argTypes[i];
if (want) expectType(a, want, `libCall ${e.fn} arg ${i}`);
});
if (e.fn === "fileHandle.read" || e.fn === "fileHandle.writeBytes" || e.fn === "fileHandle.writeStr") {
const inner = e.type.kind === "promise" ? e.type.inner : undefined;
const shape = inner?.kind === "record" ? records.get(inner.shapeId) : undefined;
const countName = e.fn === "fileHandle.read" ? "bytesRead" : "bytesWritten";
const payload = e.args[1]?.type;
const count = shape?.fields.find((f) => f.name === countName);
const buffer = shape?.fields.find((f) => f.name === "buffer");
const ok =
shape !== undefined && !shape.tuple && shape.indexValue === undefined && shape.fields.length === 2 &&
count?.type.kind === "f64" && payload !== undefined && buffer !== undefined &&
typeEquals(buffer.type, payload);
if (!ok) {
err(`libCall ${e.fn} must return a promise of { ${countName}: number, buffer }`, e.loc);
}
break;
}
if (e.fn === "fetch.streamFrom") {
const t = e.args[0]?.type;
const ok =
t?.kind === "string" ||
(t?.kind === "bytes" && t.elem === "u8") ||
(t?.kind === "array" &&
canConvertToDyn(
t.elem,
(id) => records.get(id),
(id) => unions.get(id),
)) ||
t?.kind === "dyn";
if (!ok) {
err(
`libCall fetch.streamFrom arg 0: expected a supported iterable, got ${t?.kind}`,
e.loc,
);
}
break;
}
if (e.fn === "fetch.readerRead") {
if (e.type.kind !== "promise" || e.type.inner.kind !== "record") {
err(
`libCall fetch.readerRead must return a promise of a read-result record`,
e.loc,
);
}
break;
}
if (e.fn === "fetch.responseText" || e.fn === "fetch.responseBytes") {
const valueType = e.fn === "fetch.responseText" ? STRING : BYTES_U8;
const inner = e.type.kind === "promise" ? e.type.inner : null;
const union = inner?.kind === "union" ? unions.get(inner.unionId) : undefined;
if (
inner === null ||
(!typeEquals(inner, valueType) &&
!union?.arms.some((arm) => typeEquals(arm, valueType)))
) {
err(
`libCall ${e.fn} must return a promise whose value includes ${valueType.kind}`,
e.loc,
);
}
break;
}
if (e.fn === "string.fromCharCode") {
// One packed f64[] or one bytes value (the spread form).
const t = e.args[0]?.type;
const ok =
t && ((t.kind === "array" && t.elem.kind === "f64") || t.kind === "bytes");
if (!ok) {
err(`libCall string.fromCharCode arg 0: expected number[] or bytes, got ${t?.kind}`, e.loc);
}
break;
}
if (e.fn === "process.envGet") {
// Result is the module's interned `string | undefined` union.
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok =
def &&
def.arms.length === 2 &&
def.arms[0]!.kind === "string" &&
def.arms[1]!.kind === "undefinedT";
if (!ok) {
err(`libCall process.envGet must return the 'string | undefined' union`, e.loc);
}
break;
}
if (e.fn === "process.columns") {
// Result is the module's interned `number | undefined` union.
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok =
def &&
def.arms.length === 2 &&
def.arms[0]!.kind === "f64" &&
def.arms[1]!.kind === "undefinedT";
if (!ok) {
err(`libCall process.columns must return the 'number | undefined' union`, e.loc);
}
break;
}
if (e.fn === "http.createServer" || e.fn === "http2.createServerReq") {
// The request handler: void, at most (req, res) in order.
const cbT = e.args[0]?.type;
let ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= 2;
if (ok && cbT?.kind === "func") {
const [p0, p1] = cbT.params;
if (p0 !== undefined && p0.kind !== "httpReq") ok = false;
if (p1 !== undefined && p1.kind !== "httpRes") ok = false;
}
if (!ok) {
err(`libCall ${e.fn} handler shape (frontend must fence)`, e.loc);
}
break;
}
if (e.fn === "http.serverOnRequest") {
// The 'request' listener: the same shape as http.createServer's
// handler — void, at most (req, res) in order.
const cbT = e.args[1]?.type;
let ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= 2;
if (ok && cbT?.kind === "func") {
const [p0, p1] = cbT.params;
if (p0 !== undefined && p0.kind !== "httpReq") ok = false;
if (p1 !== undefined && p1.kind !== "httpRes") ok = false;
}
if (!ok) {
err(`libCall http.serverOnRequest handler shape (frontend must fence)`, e.loc);
}
break;
}
if (e.fn === "http2.serverOnSessionError") {
const cbT = e.args[1]?.type;
const ok = cbT?.kind === "func" && cbT.ret.kind === "void" &&
cbT.params.length <= 2 &&
(cbT.params[0] === undefined ||
(cbT.params[0].kind === "object" && cbT.params[0].className === "%Error")) &&
(cbT.params[1] === undefined || cbT.params[1].kind === "http2Session");
if (!ok) {
err(`libCall http2.serverOnSessionError callback shape (frontend must fence)`, e.loc);
}
break;
}
if (e.fn === "http.reqOnData") {
const cbT = e.args[1]?.type;
let ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= 1;
if (ok && cbT?.kind === "func" && cbT.params.length === 1) {
const p = cbT.params[0]!;
// dyn = the checked-dynamic listener's adapter (the chunk
// boxes Buffer-flavored in the runtime data thunk).
ok = (p.kind === "bytes" && p.elem === "u8") || p.kind === "dyn";
}
if (!ok) {
err(`libCall http.reqOnData callback shape (frontend must fence)`, e.loc);
}
break;
}
if (e.fn === "http.reqStatusCode") {
// Result is the module's interned `number | undefined` union.
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok =
def &&
def.arms.length === 2 &&
def.arms[0]!.kind === "f64" &&
def.arms[1]!.kind === "undefinedT";
if (!ok) {
err(`libCall http.reqStatusCode must return the 'number | undefined' union`, e.loc);
}
break;
}
if (e.fn === "net.sockRemoteAddress") {
// Result is the interned `string | undefined` union (envGet's).
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok =
def &&
def.arms.length === 2 &&
def.arms[0]!.kind === "string" &&
def.arms[1]!.kind === "undefinedT";
if (!ok) {
err(`libCall net.sockRemoteAddress must return the 'string | undefined' union`, e.loc);
}
break;
}
if (e.fn === "net.sockEncrypted") {
// Result is the interned `boolean | undefined` union.
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok =
def &&
def.arms.length === 2 &&
def.arms[0]!.kind === "bool" &&
def.arms[1]!.kind === "undefinedT";
if (!ok) {
err(`libCall net.sockEncrypted must return the 'boolean | undefined' union`, e.loc);
}
break;
}
if (e.fn === "tls.sockAuthError") {
// Result is the interned `string | null` union (Node's
// authorizationError: the verify-failure code string, or null).
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok =
def &&
def.arms.length === 2 &&
def.arms[0]!.kind === "string" &&
def.arms[1]!.kind === "nullT";
if (!ok) {
err(`libCall tls.sockAuthError must return the 'string | null' union`, e.loc);
}
break;
}
if (e.fn === "http.requestCb" || e.fn === "https.requestCb" ||
e.fn === "http.requestUrlCb" || e.fn === "http.clientOnResponse" ||
e.fn === "http.requestAgentCb" || e.fn === "https.requestAgentCb" ||
e.fn === "https.requestUrlCb") {
// The response listener: void, no params or exactly (res: httpReq).
const cbT = e.args[
e.fn === "http.requestCb" ? 7
: e.fn === "https.requestCb" ? 9
: e.fn === "http.requestUrlCb" || e.fn === "https.requestUrlCb" ? 3
: e.fn === "http.requestAgentCb" ? 8
: e.fn === "https.requestAgentCb" ? 10
: 1]?.type;
let ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= 1;
if (ok && cbT?.kind === "func" && cbT.params.length === 1) {
ok = cbT.params[0]!.kind === "httpReq";
}
if (!ok) {
err(`libCall ${e.fn} callback shape (frontend must fence)`, e.loc);
}
break;
}
if (e.fn === "http.reqOnError" || e.fn === "http.clientOnError") {
// The error listener: void, no params or exactly (err: %Error).
const cbT = e.args[1]?.type;
let ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= 1;
if (ok && cbT?.kind === "func" && cbT.params.length === 1) {
const p = cbT.params[0]!;
ok = p.kind === "object" && p.className === "%Error";
}
if (!ok) {
err(`libCall ${e.fn} callback shape (frontend must fence)`, e.loc);
}
break;
}
if (e.fn === "net.sockRead") {
// Result is the interned `Buffer | null` union.
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok =
def &&
def.arms.length === 2 &&
def.arms.some((a) => a.kind === "bytes" && a.elem === "u8") &&
def.arms.some((a) => a.kind === "nullT");
if (!ok) {
err(`libCall net.sockRead must return the 'Buffer | null' union`, e.loc);
}
break;
}
if (e.fn === "http.serverOnUpgrade" || e.fn === "http.clientOnUpgrade" ||
e.fn === "http.serverOnConnect") {
// (req, socket, head) or any shorter prefix, void return. The
// 'connect' registration additionally admits a socket slot
// that is a UNION carrying a netSocket arm (the h2 compat
// listener — the emitted adapter wraps the socket there).
const cbT = e.args[1]?.type;
let ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= 3;
if (ok && cbT?.kind === "func") {
const [p0, p1, p2] = cbT.params;
if (p0 !== undefined && p0.kind !== "httpReq") ok = false;
if (p1 !== undefined && p1.kind !== "netSocket") {
if (e.fn === "http.serverOnConnect" && p1?.kind === "union") {
const def = unions.get(p1.unionId);
if (!def || !def.arms.some((a) => a.kind === "netSocket")) ok = false;
} else {
ok = false;
}
}
if (p2 !== undefined && !(p2.kind === "bytes" && p2.elem === "u8")) ok = false;
}
if (!ok) {
err(`libCall ${e.fn} callback shape (frontend must fence)`, e.loc);
}
break;
}
if (e.fn === "net.listenOptsCb" || e.fn === "net.listenOptsReusePortCb") {
const cbIndex = e.fn === "net.listenOptsReusePortCb" ? 5 : 4;
const t = e.args[cbIndex]?.type;
const funcOk = (x: IrType | undefined): boolean =>
x?.kind === "func" && x.params.length === 0 && x.ret.kind === "void";
let ok = funcOk(t);
if (!ok && t?.kind === "union") {
const def = unions.get(t.unionId);
ok = !!def && def.arms.length === 2 && def.arms.some((a) => funcOk(a)) &&
def.arms.some((a) => a.kind === "undefinedT");
}
if (!ok) {
err(`libCall ${e.fn} callback shape (frontend must fence)`, e.loc);
}
break;
}
if (e.fn === "net.connectLookup") {
// The caller's resolver: (hostname: string, options: unknown,
// callback: (err: <union>, addresses: <record[]>) => void) =>
// void — the emitter synthesizes the answer thunk from these
// types, so the structure must hold.
const t = e.args[2]?.type;
let ok =
t?.kind === "func" && t.ret.kind === "void" && t.params.length === 3 &&
t.params[0]!.kind === "string" && t.params[1]!.kind === "dyn";
if (ok && t?.kind === "func") {
const cbT = t.params[2]!;
ok = cbT.kind === "func" && cbT.ret.kind === "void" && cbT.params.length === 2 &&
cbT.params[0]!.kind === "union" &&
cbT.params[1]!.kind === "array" && cbT.params[1]!.elem.kind === "record";
}
if (!ok) {
err(`libCall net.connectLookup resolver shape (frontend must fence)`, e.loc);
}
break;
}
if (e.fn === "http.requestConn" || e.fn === "http.requestConnCb") {
const dialT = e.args[0]?.type;
if (!(dialT?.kind === "func" && dialT.params.length === 0 && dialT.ret.kind === "netSocket")) {
err(`libCall ${e.fn} dialer shape (frontend must fence)`, e.loc);
break;
}
if (e.fn === "http.requestConnCb") {
const cbT = e.args[6]?.type;
const ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= 1 &&
(cbT.params[0] === undefined || cbT.params[0].kind === "httpReq");
if (!ok) err(`libCall ${e.fn} callback shape (frontend must fence)`, e.loc);
}
break;
}
if (e.fn === "http.reqStatusMessage" || e.fn === "http.resStatusMsgGet") {
// Result is the interned `string | undefined` union (reqHeader's).
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok =
def &&
def.arms.length === 2 &&
def.arms.some((a) => a.kind === "string") &&
def.arms.some((a) => a.kind === "undefinedT");
if (!ok) {
err(`libCall ${e.fn} must return the 'string | undefined' union`, e.loc);
}
break;
}
if (e.fn === "http.resSocket") {
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok = def && def.arms.length === 2 &&
def.arms.some((a) => a.kind === "netSocket") &&
def.arms.some((a) => a.kind === "nullT");
if (!ok) err(`libCall http.resSocket must return the 'Socket | null' union`, e.loc);
break;
}
if (e.fn === "http.reqH2Stream") {
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok = def && def.arms.length === 2 &&
def.arms.some((a) => a.kind === "http2Stream") &&
def.arms.some((a) => a.kind === "undefinedT");
if (!ok) err(`libCall http.reqH2Stream must return the 'Http2Stream | undefined' union`, e.loc);
break;
}
if (e.fn === "http.reqHeader" || e.fn === "http.reqTrailer" || e.fn === "http.resGetHeader" || e.fn === "http.clientGetHeader") {
// Result is the interned `string | undefined` union (envGet's).
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok =
def &&
def.arms.length === 2 &&
def.arms[0]!.kind === "string" &&
def.arms[1]!.kind === "undefinedT";
if (!ok) {
err(`libCall ${e.fn} must return the 'string | undefined' union`, e.loc);
}
break;
}
if (e.fn === "http.reqHeaderValues" || e.fn === "http.reqTrailerValues") {
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok = def && def.arms.length === 2 &&
def.arms[0]!.kind === "array" && def.arms[0]!.elem.kind === "string" &&
def.arms[1]!.kind === "undefinedT";
if (!ok) err(`libCall ${e.fn} must return the 'string[] | undefined' union`, e.loc);
break;
}
if (e.fn === "net.createServerCb" || e.fn === "net.serverOnConnection" ||
e.fn === "net.serverOnSecureConnection" ||
e.fn === "http.serverSetTimeoutCb" || e.fn === "http.serverOnTimeout" ||
e.fn === "http.clientOnSocket" ||
e.fn === "net.sockOnData" || e.fn === "net.serverOnError" ||
e.fn === "net.sockOnError") {
// The program-dependent listener shapes: a void closure with no
// params, or exactly the one supported parameter per event
// (socket handle / data chunk bytes / error %Error). The
// callback slot is arg 0 for createServerCb, arg 1 otherwise.
const cbT = e.args[e.fn === "net.createServerCb" ? 0 : e.fn === "http.serverSetTimeoutCb" ? 2 : 1]?.type;
let ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= 1;
if (ok && cbT?.kind === "func" && cbT.params.length === 1) {
const p = cbT.params[0]!;
if (e.fn === "net.sockOnData") ok = (p.kind === "bytes" && p.elem === "u8") || p.kind === "dyn";
else if (e.fn === "net.serverOnError" || e.fn === "net.sockOnError") {
ok = p.kind === "object" && p.className === "%Error";
} else ok = p.kind === "netSocket";
}
if (!ok) {
err(`libCall ${e.fn} callback shape (frontend must fence)`, e.loc);
}
break;
}
if (e.fn === "http2.createSecureServerSni") {
// The SNI callback: (servername: string, cb: (err, ctx?) => void)
// => void — as a bare func, or the `| undefined` union from the
// conditional-spread spelling (exactly two arms: the func and
// undefined). The cb's own params are program-interned unions
// (Error|null, SecureContext|undefined) whose arms the emitted
// answer thunk decodes; here the structural func shape is what
// the validator can pin.
const sniOk = (t: IrType | undefined): boolean =>
t?.kind === "func" && t.ret.kind === "void" && t.params.length === 2 &&
t.params[0]!.kind === "string" && t.params[1]!.kind === "func";
const argT = e.args[2]?.type;
let ok = sniOk(argT);
if (!ok && argT?.kind === "union") {
const def = unions.get(argT.unionId);
ok = def !== undefined && def.arms.length === 2 &&
def.arms.some((a) => a.kind === "undefinedT") &&
def.arms.some((a) => sniOk(a));
}
if (!ok) {
err(`libCall ${e.fn} SNI callback shape (frontend must fence)`, e.loc);
}
break;
}
if (e.fn === "dgram.onMessage" || e.fn === "dgram.onError") {
// The dgram listener shapes: a void closure with no params, or
// the per-event parameter shapes — message takes (msg: bytes<u8>
// [, rinfo: record]), error the one %Error param.
const cbT = e.args[1]?.type;
const maxParams = e.fn === "dgram.onMessage" ? 2 : 1;
let ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= maxParams;
if (ok && cbT?.kind === "func" && cbT.params.length >= 1) {
if (e.fn === "dgram.onMessage") {
const p0 = cbT.params[0]!;
ok = p0.kind === "bytes" && p0.elem === "u8";
const p1 = cbT.params[1];
if (ok && p1 !== undefined) ok = p1.kind === "record";
} else {
const p = cbT.params[0]!;
ok = p.kind === "object" && p.className === "%Error";
}
}
if (!ok) {
err(`libCall ${e.fn} callback shape (frontend must fence)`, e.loc);
}
break;
}
if (e.fn === "dns.lookup") {
// The callback: void, at most (err: Error | null, address:
// string, family: number).
const cbT = e.args[2]?.type;
let ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= 3;
if (ok && cbT?.kind === "func" && cbT.params.length >= 1) {
const p0 = cbT.params[0]!;
const def = p0.kind === "union" ? unions.get(p0.unionId) : undefined;
ok =
def !== undefined &&
def.arms.length === 2 &&
def.arms.some((a) => a.kind === "nullT") &&
def.arms.some((a) => a.kind === "object" && a.className === "%Error");
const p1 = cbT.params[1];
if (ok && p1 !== undefined) ok = p1.kind === "string";
const p2 = cbT.params[2];
if (ok && p2 !== undefined) ok = p2.kind === "f64";
}
if (!ok) {
err(`libCall dns.lookup callback shape (frontend must fence)`, e.loc);
}
break;
}
if (
e.fn === "zlib.deflateCb" || e.fn === "zlib.inflateCb" ||
e.fn === "zlib.deflateRawCb" || e.fn === "zlib.inflateRawCb" ||
e.fn === "zlib.gzipCb" || e.fn === "zlib.gunzipCb" || e.fn === "zlib.unzipCb"
) {
const cbT = e.args[1]?.type;
let ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= 2;
if (ok && cbT?.kind === "func" && cbT.params.length >= 1) {
const error = cbT.params[0]!;
if (error.kind !== "dyn") {
const def = error.kind === "union" ? unions.get(error.unionId) : undefined;
ok = def !== undefined &&
def.arms.some((arm) => arm.kind === "nullT") &&
def.arms.some((arm) => arm.kind === "object" && arm.className === "%Error") &&
def.arms.every((arm) =>
arm.kind === "nullT" || arm.kind === "undefinedT" ||
(arm.kind === "object" && arm.className === "%Error"));
}
}
if (ok && cbT?.kind === "func" && cbT.params.length === 2) {
const value = cbT.params[1]!;
ok = value.kind === "dyn" || (value.kind === "bytes" && value.elem === "u8");
}
if (!ok) err(`libCall ${e.fn} callback shape (frontend must fence)`, e.loc);
break;
}
if (
e.fn === "fs.renameCb" ||
e.fn === "process.stdoutWriteBytesCb" ||
e.fn === "process.stderrWriteBytesCb"
) {
// The callback is void and accepts either no parameters, one
// checked-dynamic error slot (JS), or Error | null (optionally
// including undefined for an explicitly optional parameter).
const cbT = e.args[2]?.type;
let ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= 1;
if (ok && cbT?.kind === "func" && cbT.params.length === 1) {
const p = cbT.params[0]!;
if (p.kind === "dyn") {
ok = true;
} else {
const def = p.kind === "union" ? unions.get(p.unionId) : undefined;
ok =
def !== undefined &&
def.arms.some((a) => a.kind === "nullT") &&
def.arms.some((a) => a.kind === "object" && a.className === "%Error") &&
def.arms.every((a) =>
a.kind === "nullT" || a.kind === "undefinedT" ||
(a.kind === "object" && a.className === "%Error"));
}
}
if (!ok) {
err(`libCall ${e.fn} callback shape (frontend must fence)`, e.loc);
}
break;
}
if (e.fn === "net.serverAddress") {
// Result is the {address, family, port} record (dgram.address's
// check, another receiver).
const shape = e.type.kind === "record" ? records.get(e.type.shapeId) : undefined;
const ok =
shape !== undefined &&
shape.fields.length === 3 &&
shape.fields[0]!.name === "address" && shape.fields[0]!.type.kind === "string" &&
shape.fields[1]!.name === "family" && shape.fields[1]!.type.kind === "string" &&
shape.fields[2]!.name === "port" && shape.fields[2]!.type.kind === "f64";
if (!ok) {
err(`libCall net.serverAddress must return the {address, family, port} record`, e.loc);
}
break;
}
if (e.fn === "dgram.address") {
// Result is the {address, family, port} record.
const shape = e.type.kind === "record" ? records.get(e.type.shapeId) : undefined;
const ok =
shape !== undefined &&
shape.fields.length === 3 &&
shape.fields[0]!.name === "address" && shape.fields[0]!.type.kind === "string" &&
shape.fields[1]!.name === "family" && shape.fields[1]!.type.kind === "string" &&
shape.fields[2]!.name === "port" && shape.fields[2]!.type.kind === "f64";
if (!ok) {
err(`libCall dgram.address must return the {address, family, port} record`, e.loc);
}
break;
}
if (e.fn === "fs.readdirTypesSync" || e.fn === "fsp.readdirTypes") {
// Result: the interned Dirent record array — {%dtype: f64,
// name: string, parentPath: string} rows (canonical field
// order; the structure lowerFsReaddirTypesCall pinned).
const result = e.fn === "fsp.readdirTypes"
? e.type.kind === "promise" ? e.type.inner : undefined
: e.type;
const shape =
result?.kind === "array" && result.elem.kind === "record"
? records.get(result.elem.shapeId)
: undefined;
const ok =
shape !== undefined &&
!shape.tuple &&
shape.indexValue === undefined &&
shape.fields.length === 3 &&
shape.fields[0]!.name === "%dtype" && shape.fields[0]!.type.kind === "f64" &&
shape.fields[1]!.name === "name" && shape.fields[1]!.type.kind === "string" &&
shape.fields[2]!.name === "parentPath" && shape.fields[2]!.type.kind === "string";
if (!ok) {
err(`libCall ${e.fn} must return ${e.fn === "fsp.readdirTypes" ? "a promise of " : ""}the Dirent record array`, e.loc);
}
break;
}
if (e.fn === "os.networkInterfaces") {
// Result: a pure index-signature record whose value is
// `Info[] | undefined`, Info a two-record union (one arm's
// scopeid f64, the other's `number | undefined`) — the structure
// lowerOsNetworkInterfacesCall pinned.
const shape = e.type.kind === "record" ? records.get(e.type.shapeId) : undefined;
let ok = shape !== undefined && !shape.tuple && shape.fields.length === 0 && shape.indexValue !== undefined;
const ivDef = ok && shape!.indexValue!.kind === "union" ? unions.get(shape!.indexValue!.unionId) : undefined;
const arrArm = ivDef?.arms.find((a) => a.kind === "array");
ok = ok && ivDef !== undefined && ivDef.arms.length === 2 && arrArm !== undefined && ivDef.arms.some((a) => a.kind === "undefinedT");
const infoDef = ok && arrArm!.kind === "array" && arrArm!.elem.kind === "union" ? unions.get(arrArm!.elem.unionId) : undefined;
ok = ok && infoDef !== undefined && infoDef.arms.length === 2 && infoDef.arms.every((a) => a.kind === "record");
if (!ok) {
err(`libCall os.networkInterfaces must return the NetworkInterfaceInfo dictionary record`, e.loc);
}
break;
}
if (e.fn === "qs.parse") {
// Result: a pure index-signature record whose value union
// carries a string arm and a string[] arm (undefined tolerated
// — @types/node's Dict — and f64 too: the header-family
// canonicalization interns every such dictionary with the
// number arm, type-level only) — the structure
// lowerQuerystringParseCall pinned.
const shape = e.type.kind === "record" ? records.get(e.type.shapeId) : undefined;
let ok = shape !== undefined && !shape.tuple && shape.fields.length === 0 && shape.indexValue !== undefined;
const ivDef = ok && shape!.indexValue!.kind === "union" ? unions.get(shape!.indexValue!.unionId) : undefined;
ok = ok && ivDef !== undefined &&
ivDef.arms.some((a) => a.kind === "string") &&
ivDef.arms.some((a) => a.kind === "array" && a.elem.kind === "string") &&
ivDef.arms.every((a) => a.kind === "string" || a.kind === "array" || a.kind === "undefinedT" || a.kind === "f64");
if (!ok) {
err(`libCall qs.parse must return the ParsedUrlQuery dictionary record`, e.loc);
}
break;
}
if (e.fn === "cp.execFile") {
const cb = e.args[2]?.type;
if (cb?.kind !== "func" || cb.rest === true || cb.params.length > 3 || cb.ret.kind !== "void") {
err(`libCall cp.execFile callback must be a non-rest void function with at most three parameters`, e.loc);
} else {
const error = cb.params[0];
if (error !== undefined) {
const def = error.kind === "union" ? unions.get(error.unionId) : undefined;
if (!def || def.arms.length !== 2 ||
!def.arms.some((arm) => arm.kind === "nullT") ||
!def.arms.some((arm) => arm.kind === "object" && arm.className === "%Error")) {
err(`libCall cp.execFile callback error parameter must be Error | null`, e.loc);
}
}
const stdout = cb.params[1];
if (stdout !== undefined && stdout.kind !== "string") {
err(`libCall cp.execFile callback stdout parameter must be string`, e.loc);
}
const stderr = cb.params[2];
if (stderr !== undefined && stderr.kind !== "string") {
err(`libCall cp.execFile callback stderr parameter must be string`, e.loc);
}
}
}
if (e.fn === "child.onExit" || e.fn === "child.onClose" || e.fn === "child.onError") {
// The listener: a closure with no params, or exactly the
// supported parameter shapes per event — exit takes (code:
// number | null) with an optional (signal: string | null)
// second parameter, error exactly (err: %Error).
const cb = e.args[1];
const cbT = cb?.type;
const maxParams = e.fn === "child.onError" ? 1 : 2;
let ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= maxParams;
if (ok && cbT?.kind === "func" && cbT.params.length >= 1) {
const p = cbT.params[0]!;
if (e.fn === "child.onExit" || e.fn === "child.onClose") {
const def = p.kind === "union" ? unions.get(p.unionId) : undefined;
ok =
def !== undefined &&
def.arms.length === 2 &&
def.arms[0]!.kind === "f64" &&
def.arms[1]!.kind === "nullT";
if (ok && cbT.params.length === 2) {
const s = cbT.params[1]!;
const sdef = s.kind === "union" ? unions.get(s.unionId) : undefined;
ok =
sdef !== undefined &&
sdef.arms.length === 2 &&
sdef.arms.some((a) => a.kind === "string") &&
sdef.arms.some((a) => a.kind === "nullT");
}
} else {
ok = p.kind === "object" && p.className === "%Error";
}
}
if (!ok) {
err(`libCall ${e.fn} callback shape (frontend must fence)`, e.loc);
}
break;
}
if (e.fn === "writer.onDrain" || e.fn === "writer.onFinish" || e.fn === "writer.onError") {
const cbT = e.args[1]?.type;
const maxParams = e.fn === "writer.onError" ? 1 : 0;
let ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= maxParams;
if (ok && cbT?.kind === "func" && cbT.params.length === 1) {
const p = cbT.params[0]!;
ok = p.kind === "object" && p.className === "%Error";
}
if (!ok) err(`libCall ${e.fn} callback shape (frontend must fence)`, e.loc);
break;
}
if (e.fn === "process.onExit" || e.fn === "process.offExit" ||
e.fn === "stdin.onData" || e.fn === "stdin.onError") {
// The listener: a void closure with no params, or exactly the
// one supported parameter shape per event (code number / data
// chunk bytes / error %Error).
const cbT = e.args[0]?.type;
let ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= 1;
if (ok && cbT?.kind === "func" && cbT.params.length === 1) {
const p = cbT.params[0]!;
if (e.fn === "stdin.onData") ok = p.kind === "bytes" && p.elem === "u8";
else if (e.fn === "stdin.onError") ok = p.kind === "object" && p.className === "%Error";
else ok = p.kind === "f64";
}
if (!ok) {
err(`libCall ${e.fn} callback shape (frontend must fence)`, e.loc);
}
break;
}
if (e.fn === "spawnRes.status") {
// Result is the module's interned `number | null` union.
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok =
def &&
def.arms.length === 2 &&
def.arms[0]!.kind === "f64" &&
def.arms[1]!.kind === "nullT";
if (!ok) {
err(`libCall spawnRes.status must return the 'number | null' union`, e.loc);
}
break;
}
if (e.fn === "spawnRes.signal") {
// Result is the module's interned `string | null` union.
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok =
def &&
def.arms.length === 2 &&
def.arms.some((a) => a.kind === "string") &&
def.arms.some((a) => a.kind === "nullT");
if (!ok) {
err(`libCall spawnRes.signal must return the 'string | null' union`, e.loc);
}
break;
}
if (e.fn === "sp.get") {
// Result is the interned `string | null` union (the runtime
// answers +1-or-NULL; the backend builds the arms).
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok =
def &&
def.arms.length === 2 &&
def.arms.some((a) => a.kind === "string") &&
def.arms.some((a) => a.kind === "nullT");
if (!ok) {
err(`libCall sp.get must return the 'string | null' union`, e.loc);
}
break;
}
if (e.fn === "sp.fromPairs") {
const t = e.args[0]?.type;
const ok = t && t.kind === "array" && t.elem.kind === "array" && t.elem.elem.kind === "string";
if (!ok) {
err(`libCall sp.fromPairs arg 0: expected string[][], got ${t?.kind}`, e.loc);
}
break;
}
if (e.fn === "sym.desc" || e.fn === "sym.keyFor") {
// Result is the interned `string | undefined` union (the
// runtime answers +1-or-NULL; the backend builds the arms).
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok =
def &&
def.arms.length === 2 &&
def.arms.some((a) => a.kind === "string") &&
def.arms.some((a) => a.kind === "undefinedT");
if (!ok) {
err(`libCall ${e.fn} must return the 'string | undefined' union`, e.loc);
}
break;
}
if (e.fn === "spawnRes.error") {
// Result is the interned `%Error | undefined` union.
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok =
def &&
def.arms.length === 2 &&
def.arms.some((a) => a.kind === "object" && a.className === "%Error") &&
def.arms.some((a) => a.kind === "undefinedT");
if (!ok) {
err(`libCall spawnRes.error must return the 'Error | undefined' union`, e.loc);
}
break;
}
if (e.fn === "child.pid" || e.fn === "child.exitCode") {
// pid: the interned `number | undefined`; exitCode: `number | null`.
const wantUnit = e.fn === "child.pid" ? "undefinedT" : "nullT";
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok =
def &&
def.arms.length === 2 &&
def.arms[0]!.kind === "f64" &&
def.arms[1]!.kind === wantUnit;
if (!ok) {
err(
`libCall ${e.fn} must return the 'number | ${wantUnit === "nullT" ? "null" : "undefined"}' union`,
e.loc,
);
}
break;
}
if (e.fn === "net.serverCloseBind") {
// The bound close: (cbUnion) => netServer, cbUnion carrying a
// void-returning func arm (≤1 param) and the undefined arm.
const t = e.type;
const cbU = t.kind === "func" && t.params.length === 1 ? t.params[0]! : null;
const def = cbU?.kind === "union" ? unions.get(cbU.unionId) : undefined;
const ok =
t.kind === "func" && (t.ret.kind === "netServer" || t.ret.kind === "void") &&
def &&
def.arms.some((a) => a.kind === "func" && a.params.length <= 1 && a.ret.kind === "void") &&
def.arms.some((a) => a.kind === "undefinedT");
if (!ok) {
err(`libCall net.serverCloseBind must produce the bound-close func type`, e.loc);
}
break;
}
if (e.fn === "child.stdin" || e.fn === "child.stdout" || e.fn === "child.stderr") {
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const handleKind = e.fn === "child.stdin" ? "childWriter" : "childStream";
const ok =
def &&
def.arms.length === 2 &&
def.arms.some((a) => a.kind === handleKind) &&
def.arms.some((a) => a.kind === "nullT");
if (!ok) {
err(`libCall ${e.fn} must return the '${e.fn === "child.stdin" ? "Writable" : "Readable"} | null' union`, e.loc);
}
break;
}
if (e.fn === "island.castFail") {
// The deferred boundary failure's typed dummy: a promise (the
// only cast shape the frontend defers).
if (e.type.kind !== "promise") {
err(`libCall island.castFail must return a promise type, got ${e.type.kind}`, e.loc);
}
break;
}
if (e.fn === "global.undefRead") {
// Always throws — the result type is whatever the declared type
// of the undefined global mapped to (never materialized).
break;
}
if (e.fn === "error.nodeThrow" || e.fn === "error.argTypeThrow" || e.fn === "error.propTypeThrow" ||
e.fn === "fs.mkdtempChk" || e.fn === "fs.readFileChk" ||
e.fn === "fs.opendirChk" || e.fn === "fs.watchFileChk" || e.fn === "fs.lchmodChk" ||
e.fn === "fs.readChk" || e.fn === "fs.streamOptsChk" || e.fn === "net.connectOptsChk" ||
e.fn === "tls.caCertsChk") {
// Always throws — the result type is the replaced expression's
// own (never materialized; the global.undefRead pattern). The
// fs Chk ladders qualify: every validation failure throws
// Node's typed error, and a full pass throws the trailing
// compiler-rendered fence.
break;
}
if (e.fn === "error.new") {
// Which builtin the runtime constructs is named by the result type.
if (!isBuiltinErrorObject(e.type)) {
err(`libCall error.new must return a builtin error class, got ${e.type.kind}`, e.loc);
}
break;
}
if (e.fn === "error.newDom") {
if (e.type.kind !== "object" || e.type.className !== "%DOMException") {
err(`libCall error.newDom must return %DOMException, got ${e.type.kind}`, e.loc);
}
break;
}
if (e.fn === "error.domCode" || e.fn === "error.domHasCause" || e.fn === "error.domCause" || e.fn === "error.domClone") {
// Receiver: exactly %DOMException (subclassing is fenced — the
// hidden runtime slots admit no other layout).
const recv = e.args[0];
if (!recv || recv.type.kind !== "object" || recv.type.className !== "%DOMException") {
err(`libCall ${e.fn} receiver must be %DOMException`, e.loc);
}
break;
}
if (e.fn === "class.name") {
// The arg is any class value (program-dependent classval).
if (e.args[0]?.type.kind !== "classval") {
err(`libCall class.name takes a class value`, e.loc);
}
if (e.type.kind !== "string") {
err(`libCall class.name must return string`, e.loc);
}
break;
}
if (e.fn === "assert.refEqBytes" || e.fn === "assert.bytesDeepEq") {
// Both value slots: ONE static bytes type (the frontend's
// same-static-type gate — a u8/u32 mix would memcmp garbage).
const a = e.args[0]?.type;
const b = e.args[1]?.type;
if (a?.kind !== "bytes" || b === undefined || !typeEquals(a, b)) {
err(`libCall ${e.fn} takes two same-typed bytes values`, e.loc);
}
break;
}
if (e.fn === "assert.refEqFn") {
// ANY two function signatures: the compare is pointer identity.
if (e.args[0]?.type.kind !== "func" || e.args[1]?.type.kind !== "func") {
err(`libCall assert.refEqFn takes two function values`, e.loc);
}
break;
}
if (e.fn === "error.code") {
// Receiver: any error-hierarchy object (a user subclass embeds
// the code slot in its prefix); result: the interned
// `string | undefined` union (the process.envGet pattern).
const recv = e.args[0];
if (!recv || recv.type.kind !== "object") {
err(`libCall error.code receiver must be an error object`, e.loc);
break;
}
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const ok =
def &&
def.arms.length === 2 &&
def.arms[0]!.kind === "string" &&
def.arms[1]!.kind === "undefinedT";
if (!ok) {
err(`libCall error.code must return the 'string | undefined' union`, e.loc);
}
break;
}
if (
e.fn.startsWith("readable.") || e.fn.startsWith("writable.") ||
e.fn.startsWith("duplex.") || e.fn.startsWith("transform.") ||
e.fn.startsWith("passthrough.") || e.fn === "stream.destroy" ||
e.fn === "stream.destroyErr" || e.fn === "stream.iteratorClose" || e.fn === "stream.prop" ||
e.fn === "stream.errored" || e.fn === "stream.finished" ||
e.fn === "stream.finishedDyn" || e.fn === "stream.pipeline" ||
e.fn === "stream.pipelineDyn"
) {
// Receiver/result: stream-hierarchy objects (a class whose base
// chain reaches a runtime stream class). Constructors have no
// receiver — their RESULT names the class; chaining forms
// return the receiver's static type; pipe returns its
// destination's.
const isStreamObject = (t: IrType | undefined): boolean => {
if (t?.kind !== "object") return false;
let name: string | undefined = t.className;
while (name !== undefined) {
if (RUNTIME_STREAM_CLASSES.has(name)) return true;
name = classes.get(name)?.base;
}
return false;
};
if (e.fn.endsWith(".new")) {
if (!isStreamObject(e.type)) {
err(`libCall ${e.fn} must return a stream class, got ${e.type.kind}`, e.loc);
}
for (let i = sig.argTypes.length; i < e.args.length; i++) {
if (e.args[i]!.type.kind !== "func") {
err(`libCall ${e.fn} option callback ${i} must be a func`, e.loc);
}
}
break;
}
if (e.fn.endsWith(".newDyn")) {
// The dyn-options constructor: (optsDyn) → the stream class.
if (!isStreamObject(e.type)) {
err(`libCall ${e.fn} must return a stream class, got ${e.type.kind}`, e.loc);
}
break;
}
if (e.fn.endsWith(".initDyn")) {
// (recv, optsDyn, flags, ...fallback wrapper closures).
if (!isStreamObject(e.args[0]?.type)) {
err(`libCall ${e.fn} receiver must be a stream-hierarchy object`, e.loc);
}
for (let i = sig.argTypes.length; i < e.args.length; i++) {
if (e.args[i]!.type.kind !== "func") {
err(`libCall ${e.fn} fallback callback ${i} must be a func`, e.loc);
}
}
if (e.type.kind !== "void") err(`libCall ${e.fn} must be void`, e.loc);
break;
}
if (e.fn === "stream.finished" || e.fn === "stream.finishedDyn") {
// (recv, cb) → the cleanup closure.
if (!isStreamObject(e.args[0]?.type)) {
err(`libCall ${e.fn} receiver must be a stream-hierarchy object`, e.loc);
}
const cbK = e.args[1]?.type.kind;
if (e.fn === "stream.finished" ? cbK !== "func" : cbK !== "dyn") {
err(`libCall ${e.fn} callback must be ${e.fn === "stream.finished" ? "a func" : "dyn"}`, e.loc);
}
if (e.type.kind !== "func") {
err(`libCall ${e.fn} must return the cleanup closure, got ${e.type.kind}`, e.loc);
}
break;
}
if (e.fn === "stream.pipeline" || e.fn === "stream.pipelineDyn") {
// (count, s1..sn, cb) → the destination's type.
const count = e.args[0];
const n = count?.kind === "numLit" ? count.value : -1;
if (n < 2 || e.args.length !== n + 2) {
err(`libCall ${e.fn} count/arity mismatch`, e.loc);
break;
}
for (let i = 1; i <= n; i++) {
if (!isStreamObject(e.args[i]?.type)) {
err(`libCall ${e.fn} stage ${i} must be a stream-hierarchy object`, e.loc);
}
}
const cbK = e.args[n + 1]?.type.kind;
if (e.fn === "stream.pipeline" ? cbK !== "func" : cbK !== "dyn") {
err(`libCall ${e.fn} callback must be ${e.fn === "stream.pipeline" ? "a func" : "dyn"}`, e.loc);
}
if (!typeEquals(e.type, e.args[n]!.type)) {
err(`libCall ${e.fn} must return its destination's type`, e.loc);
}
break;
}
if (e.fn === "readable.fromArr") {
// No receiver: the seed array leads; the result is the class.
if (e.args[0]?.type.kind !== "array" || !isStreamObject(e.type)) {
err(`libCall readable.fromArr must take an array and return a stream class`, e.loc);
}
break;
}
if (!isStreamObject(e.args[0]?.type)) {
err(`libCall ${e.fn} receiver must be a stream-hierarchy object`, e.loc);
break;
}
if (e.fn === "readable.nextChunk" || e.fn === "readable.nextChunkDyn") {
if (e.type.kind !== "promise") {
err(`libCall ${e.fn} must return a promise, got ${e.type.kind}`, e.loc);
}
break;
}
if (e.fn === "readable.pause" || e.fn === "readable.resume" ||
e.fn === "readable.unpipe" || e.fn === "writable.end" ||
e.fn === "readable.setEncoding" || e.fn === "readable.pushEncoding" ||
e.fn === "stream.destroy" || e.fn === "stream.destroyErr" ||
e.fn === "stream.iteratorClose") {
if (!typeEquals(e.type, e.args[0]!.type)) {
err(`libCall ${e.fn} must return its receiver's type (the chaining 'this')`, e.loc);
}
break;
}
if (e.fn === "readable.pipe") {
if (!isStreamObject(e.args[1]?.type) || !typeEquals(e.type, e.args[1]!.type)) {
err(`libCall readable.pipe must return its destination's type`, e.loc);
}
break;
}
if (e.fn === "readable.read" || e.fn === "readable.flowing" || e.fn === "stream.errored") {
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const wantArm = (t: IrType): boolean =>
e.fn === "readable.read" ? t.kind === "bytes" :
e.fn === "readable.flowing" ? t.kind === "bool" :
t.kind === "object";
const ok = def && def.arms.length === 2 &&
def.arms.some(wantArm) && def.arms.some((a) => a.kind === "nullT");
if (!ok) err(`libCall ${e.fn} must return its nullable union`, e.loc);
break;
}
if (e.fn === "stream.prop") {
if (e.type.kind !== "bool" && e.type.kind !== "f64") {
err(`libCall stream.prop must be bool or f64, got ${e.type.kind}`, e.loc);
}
break;
}
if (!typeEquals(e.type, sig.result)) {
err(`libCall ${e.fn} must be ${sig.result.kind}, got ${e.type.kind}`, e.loc);
}
break;
}
if (e.fn.startsWith("emitter.") && e.fn !== "emitter.setDefaultMax" &&
e.fn !== "emitter.setDefaultMaxChk" &&
e.fn !== "emitter.getDefaultMax" && e.fn !== "emitter.checkListener") {
// Receiver: an emitter-hierarchy object (the %EventEmitter class
// itself, or a class whose base chain reaches it). emitter.new
// has no receiver — its RESULT is the bare emitter class.
const isEmitterObject = (t: IrType | undefined): boolean => {
if (t?.kind !== "object") return false;
let name: string | undefined = t.className;
while (name !== undefined) {
if (name === RUNTIME_EMITTER_CLASS) return true;
name = classes.get(name)?.base;
}
return false;
};
if (e.fn === "emitter.new") {
if (e.type.kind !== "object" || e.type.className !== RUNTIME_EMITTER_CLASS) {
err(`libCall emitter.new must return '%EventEmitter', got ${e.type.kind}`, e.loc);
}
break;
}
if (!isEmitterObject(e.args[0]?.type)) {
err(`libCall ${e.fn} receiver must be an emitter-hierarchy object`, e.loc);
break;
}
// The chaining forms return the receiver's own static class.
if (e.fn === "emitter.on" || e.fn === "emitter.off" ||
e.fn === "emitter.onDyn" || e.fn === "emitter.onFlex" || e.fn === "emitter.offDyn" ||
e.fn === "emitter.onData" || e.fn === "emitter.onDataDyn" ||
e.fn === "emitter.removeAll" || e.fn === "emitter.setMax" ||
e.fn === "emitter.setMaxChk") {
if (!typeEquals(e.type, e.args[0]!.type)) {
err(`libCall ${e.fn} must return its receiver's type (the chaining 'this')`, e.loc);
}
// The listener slots carry closures (the dyn family's checked-
// dynamic listener is table-checked; its adapter is a func).
if ((e.fn === "emitter.on" || e.fn === "emitter.off" || e.fn === "emitter.onData") && e.args[2]?.type.kind !== "func") {
err(`libCall ${e.fn} listener must be a func`, e.loc);
}
if ((e.fn === "emitter.onDyn" || e.fn === "emitter.onDataDyn") && e.args[3]?.type.kind !== "func") {
err(`libCall ${e.fn} adapter must be a func`, e.loc);
}
break;
}
if (e.fn === "emitter.emitData") {
const chunkT = e.args[2]?.type;
const ok = chunkT !== undefined &&
((chunkT.kind === "bytes" && chunkT.elem === "u8") || chunkT.kind === "string");
if (!ok) err(`libCall emitter.emitData chunk must be bytes<u8> or string`, e.loc);
if (e.type.kind !== "bool") err(`libCall emitter.emitData must be bool`, e.loc);
break;
}
if (e.fn === "emitter.countFn" && e.args[2]?.type.kind !== "func") {
err(`libCall emitter.countFn listener must be a func`, e.loc);
break;
}
if (e.fn === "emitter.emitError" && e.args[2]?.type.kind !== "object") {
err(`libCall emitter.emitError payload must be an error-hierarchy object`, e.loc);
break;
}
if (e.fn === "emitter.names") {
const ok = e.type.kind === "array" && e.type.elem.kind === "string";
if (!ok) err(`libCall emitter.names must return string[]`, e.loc);
break;
}
if (e.fn === "emitter.listeners") {
const ok = e.type.kind === "array" && e.type.elem.kind === "func";
if (!ok) err(`libCall emitter.listeners must return a func array`, e.loc);
break;
}
if (e.fn === "emitter.emitFlex") {
if (e.args.slice(2).some((arg) => arg.type.kind !== "dyn")) {
err("libCall emitter.emitFlex payloads must be checked-dynamic", e.loc);
}
}
if (e.fn === "emitter.emit" || e.fn === "emitter.emitFlex" || e.fn === "emitter.count" ||
e.fn === "emitter.getMax" || e.fn === "emitter.ctor" ||
e.fn === "emitter.countFn" || e.fn === "emitter.emitError") {
if (!typeEquals(e.type, sig.result)) {
err(`libCall ${e.fn} must be ${sig.result.kind}, got ${e.type.kind}`, e.loc);
}
break;
}
}
if (e.fn === "error.ctor" || e.fn === "error.toString") {
const recv = e.args[0];
const wantErrorRoot = e.fn === "error.toString";
const ok =
recv &&
isBuiltinErrorObject(recv.type) &&
(!wantErrorRoot || (recv.type.kind === "object" && recv.type.className === "%Error"));
if (!ok) {
err(
`libCall ${e.fn} receiver must be ${wantErrorRoot ? "'%Error'" : "a builtin error class"}`,
e.loc,
);
}
}
if (!typeEquals(e.type, sig.result)) {
err(`libCall ${e.fn} must be ${sig.result.kind}, got ${e.type.kind}`, e.loc);
}
break;
}
case "jsonStringify": {
checkExpr(e.value);
if (e.type.kind !== "string") {
err(`jsonStringify must be string, got ${e.type.kind}`, e.loc);
}
// The value's STATIC type drives the emitted serializer — it must be
// JSON-safe (the frontend rejects the rest with a specific message).
// A dyn ROOT is the one non-static shape allowed: the runtime's dyn
// walker serializes it (scr_dyn_format_j), no emitted serializer.
if (
e.value.type.kind !== "dyn" &&
!isJsonStringifySafeType(e.value.type, (id) => records.get(id), (id) => unions.get(id))
) {
err(`jsonStringify of non-JSON-safe type ${e.value.type.kind}`, e.loc);
}
break;
}
case "dynCheck": {
checkExpr(e.value);
expectType(e.value, DYN, "dynCheck operand");
// The target drives the emitted validator/builder: non-dyn,
// non-void, JSON-representable (closures/class instances can never
// be found inside a JSON dyn — the frontend rejects those casts).
// Bare undefined-armed unions of JSON-safe arms are additionally
// valid: the checked-dynamic tree holds a first-class undefined value (overflow
// reads), which matches exactly the undefined arm.
const jsonOk = (t: IrType): boolean =>
isJsonSafeType(t, (id) => records.get(id), (id) => unions.get(id));
const undefArmedOk =
e.type.kind === "union" &&
(unions.get(e.type.unionId)?.arms.every(
(a) =>
a.kind === "undefinedT" ||
jsonOk(a) ||
isDynTypedRefType(a) ||
DYN_HANDLE_KINDS.has(a.kind),
) ??
false);
// bytes<u8> targets extract the checked-dynamic tree's bytes kind (a copy).
const bytesOk = e.type.kind === "bytes" && e.type.elem === "u8";
// The %Error root extracts the checked-dynamic tree's error encoding (the "%error"
// marker object caughtToDyn builds) as a fresh runtime error.
const errorOk = e.type.kind === "object" && e.type.className === "%Error";
const classOk = isDynTypedRefType(e.type);
// ADAPTABLE function targets unwrap or wrap the checked-dynamic tree's function
// kind (the checked-dynamic function boundary, ir.ts).
const funcOk =
e.type.kind === "func" &&
canAdaptDynFuncTo(e.type, (id) => records.get(id), (id) => unions.get(id));
// Runtime HANDLE targets unwrap the checked-dynamic tree's handle kind by tag (a
// retained reference, no copy — DYN_HANDLE_KINDS).
const handleOk = DYN_HANDLE_KINDS.has(e.type.kind);
if (!jsonOk(e.type) && !undefArmedOk && !bytesOk && !errorOk && !classOk && !funcOk && !handleOk) {
err(`dynCheck against non-JSON-representable type ${e.type.kind}`, e.loc);
}
break;
}
case "awaitExpr": {
checkExpr(e.value);
if (e.value.type.kind !== "promise") {
err(`await of non-promise ${e.value.type.kind}`, e.loc);
} else if (!typeEquals(e.type, e.value.type.inner)) {
err(`await type ${e.type.kind} != promise inner ${e.value.type.inner.kind}`, e.loc);
}
if (!fn.async) err("await outside an async function", e.loc);
break;
}
case "yieldExpr": {
if (fn.generator === undefined) {
err("yield outside a generator function", e.loc);
break;
}
if (e.value === null) {
err("yieldExpr with no operand (the frontend fills undefined)", e.loc);
break;
}
checkExpr(e.value);
if (!typeEquals(e.value.type, fn.generator.yieldT)) {
err(`yield operand ${typeKey(e.value.type)} != yield channel ${typeKey(fn.generator.yieldT)}`, e.loc);
}
// The undefined next-channel has no C value form: such yields are
// void-typed (statement position only — the frontend fences reads).
if (fn.generator.nextT.kind === "undefinedT") {
if (e.type.kind !== "void") err("yield result must be void on an undefined next-channel", e.loc);
} else if (!typeEquals(e.type, fn.generator.nextT)) {
err(`yield result ${typeKey(e.type)} != next channel ${typeKey(fn.generator.nextT)}`, e.loc);
}
break;
}
case "genResume": {
checkExpr(e.gen);
if (e.gen.type.kind !== "generator") {
err(`genResume on ${e.gen.type.kind}`, e.loc);
break;
}
const genT = e.gen.type;
if (e.arg !== null) checkExpr(e.arg);
if (e.mode === "next") {
if (e.arg === null) {
if (genT.nextT.kind !== "undefinedT" && genT.nextT.kind !== "dyn") {
err(`valueless next() on a ${typeKey(genT.nextT)} next-channel`, e.loc);
}
} else if (!typeEquals(e.arg.type, genT.nextT)) {
err(`next argument ${typeKey(e.arg.type)} != next channel ${typeKey(genT.nextT)}`, e.loc);
}
} else if (e.mode === "return") {
if (e.arg !== null && !typeEquals(e.arg.type, genT.retT)) {
err(`return argument ${typeKey(e.arg.type)} != return channel ${typeKey(genT.retT)}`, e.loc);
}
} else {
// throw: any exception-representable payload (the throw
// statement's rule). Date cannot retain its object kind in the
// untyped exception cell.
if (e.arg === null) {
err("genResume throw with no payload", e.loc);
} else if (
e.arg.type.kind === "void" ||
e.arg.type.kind === "dyn" ||
e.arg.type.kind === "caught" ||
e.arg.type.kind === "date"
) {
err(`genResume throw of a ${e.arg.type.kind} value`, e.loc);
}
}
// Sync resumes return IteratorResult directly; async resumes return
// Promise<IteratorResult>. The record is { done: bool, value: V }
// with V dyn (the any/unknown channel) or an undefined-armed union.
const resultT = genT.async ? (e.type.kind === "promise" ? e.type.inner : null) : e.type;
if (resultT?.kind !== "record") {
err(`genResume result is ${typeKey(e.type)}, not ${genT.async ? "a promise of " : ""}a record`, e.loc);
break;
}
const rec = records.get(resultT.shapeId);
const doneF = rec?.fields.find((f) => f.name === "done");
const valueF = rec?.fields.find((f) => f.name === "value");
if (!rec || rec.fields.length !== 2 || doneF?.type.kind !== "bool" || valueF === undefined) {
err(`genResume result record ${resultT.shapeId} is not { done: bool, value: V }`, e.loc);
break;
}
if (valueF.type.kind === "dyn") break;
const vdef = valueF.type.kind === "union" ? unions.get(valueF.type.unionId) : undefined;
if (!vdef || !vdef.arms.some((a) => a.kind === "undefinedT")) {
err(`genResume value slot ${typeKey(valueF.type)} is neither dyn nor an undefined-armed union`, e.loc);
}
break;
}
case "awaitUnionExpr": {
checkExpr(e.value);
const def = e.value.type.kind === "union" ? unions.get(e.value.type.unionId) : undefined;
const promiseArm = def?.arms[e.promiseTag];
if (!def) {
err(`awaitUnion of non-union ${e.value.type.kind}`, e.loc);
} else if (promiseArm?.kind !== "promise") {
err(`awaitUnion arm ${e.promiseTag} is not a promise`, e.loc);
} else if (!def.arms.every((a, i) => i === e.promiseTag || isUnitType(a))) {
err("awaitUnion union has non-unit arms beside the promise", e.loc);
} else if (e.type.kind === "void") {
if (promiseArm.inner.kind !== "void") {
err("awaitUnion void result over a value-carrying promise", e.loc);
}
} else if (e.type.kind !== "union") {
err(`awaitUnion result is ${e.type.kind}, not void or a union`, e.loc);
} else {
const res = unions.get(e.type.unionId);
const covered =
res &&
res.arms.some((a) => typeEquals(a, promiseArm.inner)) &&
def.arms.every(
(a, i) => i === e.promiseTag || res.arms.some((b) => typeEquals(a, b)),
);
if (!covered) err("awaitUnion result union misses an arm", e.loc);
}
if (!fn.async) err("await outside an async function", e.loc);
break;
}
case "newPromise": {
checkExpr(e.executor);
if (e.type.kind !== "promise") {
err("newPromise must have promise type", e.loc);
break;
}
const inner = e.type.inner;
const exec = e.executor.type;
if (exec.kind !== "func" || exec.params.length > 2) {
err("newPromise executor must be (resolve?, reject?) => void", e.loc);
break;
}
if (exec.params.length === 2) {
// The reject closure's one shape: (reason: %Error) => void.
const rj = exec.params[1]!;
if (
rj.kind !== "func" ||
rj.ret.kind !== "void" ||
rj.params.length !== 1 ||
rj.params[0]!.kind !== "object" ||
rj.params[0]!.className !== "%Error"
) {
err("newPromise reject param must be (%Error) => void", e.loc);
}
}
if (exec.params.length >= 1) {
const rp = exec.params[0]!;
if (rp.kind !== "func" || rp.ret.kind !== "void") {
err("newPromise resolve must be a void-returning function", e.loc);
break;
}
const expectsArg = inner.kind !== "void";
if (expectsArg && (rp.params.length !== 1 || !typeEquals(rp.params[0]!, inner))) {
err(`newPromise resolve param must be (${inner.kind}) => void`, e.loc);
}
if (!expectsArg && rp.params.length !== 0) {
err("newPromise<void> resolve takes no argument", e.loc);
}
}
break;
}
case "promiseWithResolvers": {
// The record shape: promise: Promise<T>, resolve: (T) => void
// (() => void when T is void), reject: (%Error) => void.
if (e.type.kind !== "record") {
err("promiseWithResolvers must have record type", e.loc);
break;
}
const shape = records.get(e.type.shapeId);
const fields = new Map(shape?.fields.map((f) => [f.name, f.type]) ?? []);
const prom = fields.get("promise");
const resolve = fields.get("resolve");
const reject = fields.get("reject");
if (!shape || shape.fields.length !== 3 || !prom || !resolve || !reject) {
err("promiseWithResolvers record must be { promise, resolve, reject }", e.loc);
break;
}
if (prom.kind !== "promise") {
err("promiseWithResolvers promise field must be a promise", e.loc);
break;
}
const inner = prom.inner;
if (resolve.kind !== "func" || resolve.ret.kind !== "void") {
err("promiseWithResolvers resolve must be a void-returning function", e.loc);
} else if (inner.kind === "void" ? resolve.params.length !== 0
: resolve.params.length !== 1 || !typeEquals(resolve.params[0]!, inner)) {
err(`promiseWithResolvers resolve param must match the promise's inner type`, e.loc);
}
if (
reject.kind !== "func" ||
reject.ret.kind !== "void" ||
reject.params.length !== 1 ||
reject.params[0]!.kind !== "object" ||
reject.params[0]!.className !== "%Error"
) {
err("promiseWithResolvers reject must be (%Error) => void", e.loc);
}
break;
}
case "jsMarshal": {
checkExpr(e.value);
if (e.type.kind !== "jsval") err(`jsMarshal must be jsval, got ${e.type.kind}`, e.loc);
const src = e.value.type;
// The marshal direction admits qualifying closures (host-function
// wrapping) on top of the JSON-safe set; jsExit does not. Typed
// closures (per-param call-time conversion through the exit
// machinery) are the wider func shape.
// Typed arrays and URLs marshal IN (an engine typed-array copy /
// an engine URL built from the components) without ever joining
// the round-trip (JSON) set — the frontend's union lift narrows
// union arms before marshaling, so bare bytes/url operands are
// exactly what it produces.
if (
src.kind !== "bytes" &&
src.kind !== "url" &&
src.kind !== "dyn" && // dyn values deep-copy in (data kinds; the runtime throws on boxes)
// A STATIC promise crosses as a real engine thenable when its
// fulfillment is in the reverse bridge's payload domain
// (scr_jsval_from_promise — the async-callback return bridge).
!(src.kind === "promise" && islandPromisePayloadTag(src.inner) !== null) &&
!canMarshalIntoIsland(src, (id) => records.get(id), (id) => unions.get(id)) &&
!canMarshalTypedFuncIntoIsland(src, (id) => records.get(id), (id) => unions.get(id))
) {
err(`jsMarshal of unmarshalable type ${src.kind}`, e.loc);
}
break;
}
case "jsOp": {
for (const a of e.args) checkExpr(a);
if (e.type.kind !== jsOpResultKind(e.op)) {
err(`jsOp ${e.op} must be ${jsOpResultKind(e.op)}, got ${e.type.kind}`, e.loc);
}
const named =
e.op === "getProp" || e.op === "setProp" || e.op === "callMethod" || e.op === "optCallMethod" || e.op === "globalGet" ||
e.op === "callSpread"; // the spread expression's source spelling (V8's nullish text spells it)
if (named !== (e.name !== undefined)) {
err(`jsOp ${e.op} ${named ? "requires" : "forbids"} a name`, e.loc);
}
const arity: Record<string, number | null> = {
add: 2, sub: 2, mul: 2, div: 2, mod: 2, pow: 2,
lt: 2, le: 2, gt: 2, ge: 2, eq: 2, neq: 2, instanceOf: 2,
neg: 1, plus: 1, truthy: 1, not: 1, typeof: 1, toStr: 1,
getProp: 1, setProp: 2, getIdx: 2, setIdx: 3, globalGet: 0,
undefLit: 0, nullLit: 0, iterNew: 1, defineGetter: 3, objSpread: 2,
callSpread: 3, // callee + pre array + spread source
callMethod: null, optCallMethod: null, callFn: null, callFnThis: null, construct: null, // receiver/callee + any number of args
objLit: null, arrLit: null, tplStrings: null, // variable length (objLit: key/value pairs; tplStrings: n cooked + n raw)
};
const want = arity[e.op];
if (want !== null && want !== undefined && e.args.length !== want) {
err(`jsOp ${e.op} takes ${want} arg(s), got ${e.args.length}`, e.loc);
}
if (want === null && e.op !== "objLit" && e.op !== "arrLit" && e.op !== "tplStrings" && e.args.length < 1) {
err(`jsOp ${e.op} needs a receiver/callee arg`, e.loc);
}
if (e.op === "callFnThis" && e.args.length < 2) {
err("jsOp callFnThis needs a callee and receiver", e.loc);
}
if (e.op === "objLit" && e.args.length % 2 !== 0) {
err("jsOp objLit takes key/value pairs", e.loc);
}
if (e.op === "tplStrings" && e.args.length % 2 !== 0) {
err("jsOp tplStrings takes n cooked + n raw strings", e.loc);
}
for (const a of e.args) {
if (a.type.kind !== "jsval") err(`jsOp ${e.op} arg must be jsval, got ${a.type.kind}`, e.loc);
}
break;
}
case "jsExit": {
checkExpr(e.value);
expectType(e.value, JSVAL, "jsExit operand");
if (!canExitIslandToType(e.type, (id) => records.get(id), (id) => unions.get(id))) {
err(`jsExit to non-extractable type ${e.type.kind}`, e.loc);
}
break;
}
case "jsBridgePromise": {
checkExpr(e.value);
expectType(e.value, JSVAL, "jsBridgePromise operand");
// The settled engine value crosses as a HANDLE (or not at all) —
// plus the ONE converting payload: an `any[]`-declared
// fulfillment exits Array.isArray-gated by reference AT THE
// SETTLE (SCR_ISLP_JSVAL_ARR); every other typed use exits later.
if (
e.type.kind !== "promise" ||
(e.type.inner.kind !== "jsval" &&
e.type.inner.kind !== "void" &&
!(e.type.inner.kind === "array" && e.type.inner.elem.kind === "jsval"))
) {
err("jsBridgePromise must be a promise of jsval, void, or jsval-element array", e.loc);
}
break;
}
default: {
const _exhaustive: never = e;
void _exhaustive;
}
}
}
// `continue` needs an enclosing loop; `break` an enclosing loop OR switch.
let loopDepth = 0;
let breakableDepth = 0;
// Labeled jump targets, innermost last: every loop/switch/labeled-block
// enters with its labels (possibly none) so `break lbl`/`continue lbl`
// can resolve to an enclosing entry.
const labelTargets: { kind: "loop" | "switch" | "block"; labels: string[] }[] = [];
function checkStmts(stmts: IrStmt[]): void {
for (const s of stmts) checkStmt(s);
}
function checkLoopBody(stmts: IrStmt[], labels: string[] | undefined): void {
loopDepth++;
breakableDepth++;
labelTargets.push({ kind: "loop", labels: labels ?? [] });
checkStmts(stmts);
labelTargets.pop();
breakableDepth--;
loopDepth--;
}
/** The innermost enclosing target carrying `label`, or null. */
function labelTargetOf(label: string): (typeof labelTargets)[number] | null {
for (let i = labelTargets.length - 1; i >= 0; i--) {
if (labelTargets[i]!.labels.includes(label)) return labelTargets[i]!;
}
return null;
}
function checkStmt(s: IrStmt): void {
switch (s.kind) {
case "varDecl": {
const local = locals.get(s.localId);
if (!local) err(`varDecl of undeclared local "${s.localId}"`, s.loc);
// Catch bindings are introduced by tryCatch alone.
if (local?.type.kind === "caught") err(`varDecl of caught-typed local "${s.localId}"`, s.loc);
if (s.init === null) {
// Declared, uninitialized: reads before assignment are impossible
// (tsc TS2454); an uninitialized immutable local is nonsense —
// EXCEPT a TDZ const (forward-captured): its box is allocated
// empty at scope entry and the source declaration assigns once.
if (local && !local.mutable && !local.tdz) {
err(`varDecl "${local.name}" has no init but is immutable`, s.loc);
}
break;
}
checkExpr(s.init);
if (local) expectType(s.init, local.type, `varDecl "${local.name}" init`);
break;
}
case "assign": {
const binding = locals.get(s.localId) ?? globals.get(s.localId);
if (!binding) err(`assign to undeclared local/global "${s.localId}"`, s.loc);
// Global initialization happens via assign inside %init functions,
// so a const global legitimately receives exactly one assign there;
// tsc rejects user reassignment of consts, and the lowerer only
// emits the init-time one. Locals keep the strict check — except a
// TDZ const, whose source declaration IS an assign into the
// scope-entry box (tsc rejects user reassignment there too).
else if (!binding.mutable && locals.has(s.localId) && !("tdz" in binding && binding.tdz)) {
err(`assign to immutable local "${binding.name}"`, s.loc);
}
if (binding?.type.kind === "caught") {
err(`assign to catch binding "${binding.name}" (frontend must reject)`, s.loc);
}
checkExpr(s.value);
if (binding) expectType(s.value, binding.type, `assign "${binding.name}"`);
break;
}
case "exprStmt":
checkExpr(s.expr);
break;
case "if":
checkExpr(s.cond);
expectType(s.cond, BOOL, "if condition");
checkStmts(s.then);
if (s.else_) checkStmts(s.else_);
break;
case "while":
checkExpr(s.cond);
expectType(s.cond, BOOL, "while condition");
checkLoopBody(s.body, s.labels);
break;
case "doWhile":
checkExpr(s.cond);
expectType(s.cond, BOOL, "do-while condition");
checkLoopBody(s.body, s.labels);
break;
case "switch": {
checkExpr(s.disc);
const dk = s.disc.type.kind;
if (dk !== "f64" && dk !== "string" && dk !== "bool") {
err(`switch discriminant must be f64|string|bool, got ${dk}`, s.loc);
}
let defaults = 0;
for (const c of s.cases) {
if (c.test === null) {
defaults++;
} else {
checkExpr(c.test);
expectType(c.test, s.disc.type, "switch case test");
}
// Case bodies: `break` binds here, `continue` does not.
breakableDepth++;
labelTargets.push({ kind: "switch", labels: s.labels ?? [] });
checkStmts(c.body);
labelTargets.pop();
breakableDepth--;
}
if (defaults > 1) err(`switch has ${defaults} default clauses`, s.loc);
break;
}
case "for":
if (s.init) checkStmt(s.init);
if (s.cond) {
checkExpr(s.cond);
expectType(s.cond, BOOL, "for condition");
}
if (s.update) checkStmt(s.update);
checkLoopBody(s.body, s.labels);
break;
case "arraySet": {
checkExpr(s.arr);
checkExpr(s.index);
checkExpr(s.value);
expectType(s.index, F64, "arraySet index");
if (s.arr.type.kind !== "array") {
err(`arraySet on non-array ${s.arr.type.kind}`, s.loc);
} else {
expectType(s.value, s.arr.type.elem, "arraySet value");
}
break;
}
case "arraySetLength": {
checkExpr(s.arr);
checkExpr(s.length);
expectType(s.length, F64, "arraySetLength length");
if (s.arr.type.kind !== "array") {
err(`arraySetLength on non-array ${s.arr.type.kind}`, s.loc);
}
break;
}
case "arraySetUndefined":
case "arrayDelete": {
checkExpr(s.arr);
checkExpr(s.index);
expectType(s.index, F64, `${s.kind} index`);
if (s.arr.type.kind !== "array") err(`${s.kind} on non-array ${s.arr.type.kind}`, s.loc);
break;
}
case "bytesSet": {
checkExpr(s.arr);
checkExpr(s.index);
checkExpr(s.value);
expectType(s.index, F64, "bytesSet index");
expectType(s.value, F64, "bytesSet value");
if (s.arr.type.kind !== "bytes") {
err(`bytesSet on non-bytes ${s.arr.type.kind}`, s.loc);
}
break;
}
case "fieldSet": {
checkExpr(s.obj);
checkExpr(s.value);
const cls = classes.get(s.className);
const field = cls?.fields.find((f) => f.name === s.field);
if (!cls) err(`fieldSet on undeclared class "${s.className}"`, s.loc);
else if (!field) err(`class ${s.className} has no field "${s.field}"`, s.loc);
else {
expectType(s.obj, { kind: "object", className: s.className }, "fieldSet receiver");
expectType(s.value, field.type, `fieldSet ${s.className}.${s.field}`);
}
break;
}
case "recordSet": {
checkExpr(s.obj);
checkExpr(s.value);
const shape = records.get(s.shapeId);
const field = shape?.fields.find((f) => f.name === s.field);
if (!shape) err(`recordSet on undeclared shape "${s.shapeId}"`, s.loc);
else if (!field) err(`shape ${s.shapeId} has no field "${s.field}"`, s.loc);
else {
expectType(s.obj, { kind: "record", shapeId: s.shapeId }, "recordSet receiver");
expectType(s.value, field.type, `recordSet ${s.shapeId}.${s.field}`);
}
break;
}
case "recordKeyDelete": {
checkExpr(s.obj);
checkExpr(s.key);
const shape = records.get(s.shapeId);
if (!shape) {
err(`recordKeyDelete on undeclared shape "${s.shapeId}"`, s.loc);
break;
}
if (!shape.indexValue || shape.fields.length > 0) {
err(`recordKeyDelete on non-pure-index-signature shape ${s.shapeId}`, s.loc);
break;
}
expectType(s.obj, { kind: "record", shapeId: s.shapeId }, "recordKeyDelete receiver");
expectType(s.key, STRING, "recordKeyDelete key");
break;
}
case "recordKeySet": {
checkExpr(s.obj);
checkExpr(s.key);
checkExpr(s.value);
const shape = records.get(s.shapeId);
if (!shape) {
err(`recordKeySet on undeclared shape "${s.shapeId}"`, s.loc);
break;
}
if (!shape.indexValue) {
// Signature-free dispatch: every declared field shares ONE type
// (the frontend's gate), the value IS that type, and a key miss
// traps at runtime — so overflowOnly writes cannot exist here.
const common = shape.fields[0]?.type;
if (!common || !shape.fields.every((f) => typeEquals(f.type, common))) {
err(`recordKeySet on non-index-signature shape ${s.shapeId} without one shared field type`, s.loc);
break;
}
if (s.overflowOnly) {
err(`recordKeySet overflowOnly on signature-free shape ${s.shapeId}`, s.loc);
}
expectType(s.obj, { kind: "record", shapeId: s.shapeId }, "recordKeySet receiver");
expectType(s.key, STRING, "recordKeySet key");
expectType(s.value, common, "recordKeySet value");
break;
}
expectType(s.obj, { kind: "record", shapeId: s.shapeId }, "recordKeySet receiver");
expectType(s.key, STRING, "recordKeySet key");
expectType(s.value, shape.indexValue, "recordKeySet value");
// A dyn value validates against declared fields at runtime; typed
// values need every declared field to BE the index-value type (the
// write-through stores directly). overflowOnly writes (a literal
// key naming no declared field) never collide and skip the check.
if (
!s.overflowOnly &&
shape.indexValue.kind !== "dyn" &&
!shape.fields.every((f) => typeEquals(f.type, shape.indexValue!))
) {
err(`recordKeySet on ${s.shapeId}: declared fields differ from the index-value type`, s.loc);
}
break;
}
case "forOf": {
const local = locals.get(s.localId);
if (!local) err(`forOf with undeclared local "${s.localId}"`, s.loc);
checkExpr(s.iterable);
if (s.iterable.type.kind !== "array") {
err(`forOf over non-array ${s.iterable.type.kind}`, s.loc);
} else if (local && !typeEquals(local.type, s.iterable.type.elem)) {
err(
`forOf local "${local.name}" type ${local.type.kind} != element ${s.iterable.type.elem.kind}`,
s.loc,
);
}
checkLoopBody(s.body, s.labels);
break;
}
case "block":
if (s.labels !== undefined) {
// A labeled block is a break-only jump target.
labelTargets.push({ kind: "block", labels: s.labels });
checkStmts(s.body);
labelTargets.pop();
} else {
checkStmts(s.body);
}
break;
case "throw":
checkExpr(s.value);
// dyn throws are allowed: the dyn node rides the REF cell arm by
// reference (the JS-lane `throw err` of a dyn argument).
if (s.value.type.kind === "void" || s.value.type.kind === "caught" || s.value.type.kind === "date") {
err(`throw of a ${s.value.type.kind} value`, s.loc);
}
break;
case "runtimeFence":
// The deferred JS compile fence: carries only its message/code.
if (s.message.length === 0) err(`runtimeFence with an empty message`, s.loc);
break;
case "rethrow": {
// `throw e` of a catch binding: re-raises the saved snapshot.
const local = locals.get(s.localId);
if (!local) err(`rethrow of undeclared local "${s.localId}"`, s.loc);
else if (local.type.kind !== "caught") {
err(`rethrow of non-caught local "${s.localId}" (${local.type.kind})`, s.loc);
}
break;
}
case "tryCatch": {
if (s.catchBody === null && s.finallyBody === null) {
err(`tryCatch with neither catch nor finally`, s.loc);
}
if (s.catchLocalId !== null) {
const cl = locals.get(s.catchLocalId);
if (!cl) err(`tryCatch catch binding "${s.catchLocalId}" has no local entry`, s.loc);
else if (cl.type.kind !== "caught") {
err(`tryCatch catch binding "${s.catchLocalId}" is ${cl.type.kind}, not caught`, s.loc);
}
if (s.catchBody === null) {
err(`tryCatch catch binding without a catch body`, s.loc);
}
}
checkStmts(s.tryBody);
if (s.catchBody) checkStmts(s.catchBody);
if (s.finallyBody) checkStmts(s.finallyBody);
break;
}
case "break": {
if (s.label !== undefined) {
const target = labelTargetOf(s.label);
if (!target) err(`break to unknown label "${s.label}"`, s.loc);
break;
}
if (breakableDepth === 0) err(`break outside a loop or switch`, s.loc);
break;
}
case "continue": {
if (s.label !== undefined) {
const target = labelTargetOf(s.label);
if (!target) err(`continue to unknown label "${s.label}"`, s.loc);
else if (target.kind !== "loop") {
err(`continue to non-loop label "${s.label}"`, s.loc);
}
break;
}
if (loopDepth === 0) err(`continue outside a loop`, s.loc);
break;
}
case "return":
if (s.value) {
checkExpr(s.value);
expectType(s.value, fn.returnType, "return value");
} else if (!typeEquals(fn.returnType, VOID)) {
err(`bare return in non-void function`, s.loc);
}
break;
default: {
const _exhaustive: never = s;
void _exhaustive;
}
}
}
checkStmts(fn.body);
if (!typeEquals(fn.returnType, VOID) && !alwaysReturns(fn.body, unions)) {
err(`non-void function may complete without returning`, fn.loc);
}
}
/** Conservative "all paths return" — mirrors what tsc already guarantees. */
function alwaysReturns(stmts: IrStmt[], unions: Map<string, IrUnionDef>): boolean {
for (const s of stmts) {
switch (s.kind) {
case "return":
return true;
case "throw":
case "rethrow":
case "runtimeFence":
// Terminates the path like return: control unwinds (to a handler or
// out of the function), never falling off the end. tsc agrees —
// `function f(): T { throw x; }` typechecks without a return.
return true;
case "exprStmt":
// process.exit never returns (fflush + _Exit): the path terminates
// like a throw. Mirrors tsc's own never-based reachability, which
// accepted the function without a trailing return — the
// parseAsync().catch entry handler ends exactly this way.
if (s.expr.kind === "libCall" && s.expr.fn === "process.exit") return true;
break;
case "tryCatch":
// Normal completion requires the try body to complete normally (and
// the catch, when the try raised) — if both always terminate, so
// does the whole statement. Without a catch, an exception keeps
// propagating (never a normal completion), so the try body alone
// decides. A finally that always terminates (throw) also decides.
if (
alwaysReturns(s.tryBody, unions) &&
(s.catchBody === null || alwaysReturns(s.catchBody, unions))
) {
return true;
}
if (s.finallyBody && alwaysReturns(s.finallyBody, unions)) return true;
break;
case "if":
if (s.else_ && alwaysReturns(s.then, unions) && alwaysReturns(s.else_, unions)) return true;
break;
case "while":
// `while (true)` with no break never completes normally (tsc treats
// it the same way), so anything after it is unreachable.
if (s.cond.kind === "boolLit" && s.cond.value && !containsBreak(s.body)) return true;
break;
case "for":
// `for (;;)` — no condition, or a literal-true one — with no break
// never completes normally either (the walk-up-until-root idiom:
// every exit is a return).
if (
(s.cond === null || (s.cond.kind === "boolLit" && s.cond.value)) &&
!containsBreak(s.body)
) {
return true;
}
break;
case "block":
if (alwaysReturns(s.body, unions)) return true;
break;
case "doWhile":
// The body runs at least once: if it returns on all paths, so does
// the loop. `do {} while (true)` with no break never completes.
if (alwaysReturns(s.body, unions)) return true;
if (s.cond.kind === "boolLit" && s.cond.value && !containsBreak(s.body)) return true;
break;
case "switch": {
// A switch always returns when no case body ever breaks out, every
// possible entry point (any case) reaches a return, and dispatch
// cannot miss every case: either a default exists, or the switch is
// an EXHAUSTIVE discriminant switch — the discriminant is a
// `unionDisc` over a union with N arms, every test is a distinct
// literal, and there are at least N of them. (At least: several
// discriminant VALUES can share one deduped IR arm, e.g.
// `{op: 0; a} | {op: 2; a}` is one record shape.) The real
// exhaustiveness guarantee is tsc's — it accepted the function
// without a trailing return (trust-the-checker, like narrowing
// itself); this condition only keeps hand-written IR conservative.
// With no switch-level breaks, execution from case i runs bodies
// i..end as a straight line — check that flattened suffix.
const hasDefault = s.cases.some((c) => c.test === null);
const literalTests = s.cases.map((c) => c.test).filter(
(t): t is IrExpr & { kind: "numLit" | "strLit" | "boolLit" } =>
t !== null && (t.kind === "numLit" || t.kind === "strLit" || t.kind === "boolLit"),
);
const distinct = new Set(literalTests.map((t) => `${t.kind}:${String(t.value)}`));
const exhaustive =
!hasDefault &&
s.disc.kind === "unionDisc" &&
literalTests.length === s.cases.length &&
distinct.size === s.cases.length &&
s.cases.length >= (unions.get(s.disc.unionId)?.arms.length ?? Infinity);
if (!hasDefault && !exhaustive) break;
if (s.cases.some((c) => containsBreak(c.body))) break;
const bodies = s.cases.map((c) => c.body);
const everyEntryReturns = bodies.every((_, i) =>
alwaysReturns(bodies.slice(i).flat(), unions),
);
if (everyEntryReturns) return true;
break;
}
default:
break;
}
}
return false;
}
/** Break at this level (not inside a nested loop or switch, whose bodies
* own their breaks). */
function containsBreak(stmts: IrStmt[]): boolean {
for (const s of stmts) {
switch (s.kind) {
case "break":
return true;
case "if":
if (containsBreak(s.then) || (s.else_ && containsBreak(s.else_))) return true;
break;
case "block":
if (containsBreak(s.body)) return true;
break;
case "tryCatch":
// Plain try/catch does not capture breaks — a break inside binds to
// the enclosing loop/switch (finally-crossing jumps are rejected
// upstream, so reachable IR only has these in plain try/catch).
if (
containsBreak(s.tryBody) ||
(s.catchBody !== null && containsBreak(s.catchBody)) ||
(s.finallyBody !== null && containsBreak(s.finallyBody))
) {
return true;
}
break;
default:
break; // while/for/doWhile/switch bodies own their breaks
}
}
return false;
}