Files
scriptc/packages/compiler/src/ir/validate.ts
T
Chris Tate 67247dd26b Support objectMode streams with any value as a chunk
Readable, Writable, Duplex, Transform and PassThrough take the
objectMode option, and a Duplex, Transform or PassThrough the
readableObjectMode and writableObjectMode options, as true or false
literals in an options object literal or in an options value. An
object-mode side holds any value that converts to a checked-dynamic
value: push, unshift, write, end, pipe, pipeline, for await, 'data'
listeners, Readable.from and the transform callback carry values in
TypeScript and JavaScript, and constructor-function streams and options
held in variables do the same. The object-mode high-water mark counts
values and defaults to 16. A readable buffer that mixes strings, Buffers
and other values holds them all, and push(undefined) and other values on
a byte-mode stream report ERR_INVALID_ARG_TYPE through 'error', as in
Node.

In typed code a write or transform callback, or a _write or _transform
method, may declare its chunk with any such type, which converts when
the callback runs. Writes pass the encoding argument to _write and
_transform as Node does: undefined when absent, null when a callback
took its place, otherwise the value unchanged. A parameter declared
string | null | undefined receives that value and one declared string
receives "" for undefined and null. write(chunk, encoding) on a
byte-mode stream encodes a string chunk with any known encoding and
throws ERR_UNKNOWN_ENCODING for an unknown one. Readable.from takes
typed Maps and Sets of any such value, and arrays that mix strings and
Buffers.

for await over a readable-only stream that will emit 'close' now ends
after 'close', as Node's eos does. Buffered writes that complete inside
clearBuffer run their callbacks in order, and 'finish' waits for a
deferred afterWrite that carries a callback, as Node's pendingcb does,
unless a _final callback schedules it.

The 'data' listener adapters take three slots and write and transform
callbacks read their chunk and encoding through runtime accessors, so
RUNTIME_ABI_VERSION is now 26.
2026-10-22 04:21:55 -05:00

6727 lines
266 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 { validByteNumberToken } from "./byte-numbers.js";
import type {
IrClassDef,
IrExpr,
IrFunction,
IrGlobal,
IrModule,
IrRecordShape,
IrStmt,
IrType,
IrUnionDef,
SrcLoc,
} from "./ir.js";
import {
arrayOf,
BYTES_ELEMENT_NAME,
BOOL,
BYTES_U8,
bytesOf,
canAdaptDynFuncTo,
canDynCheckTo,
canConvertToDyn,
DYN,
DYN_HANDLE_KINDS,
F64,
ffiClassType,
ffiSourceParamTypes,
isDirentShape,
isDynTypedRefType,
isFfiCallbackParam,
isFfiContextParam,
isFfiReleaseParam,
isJsonSafeType,
isJsonStringifySafeType,
isRefCounted,
isSortValuesElement,
isSupportedArrayElem,
isSupportedIndexValue,
isSupportedMapKey,
isSupportedMapValue,
isSupportedSetElem,
isUnitType,
REF_TRUTHY_KINDS,
RUNTIME_EMITTER_CLASS,
RUNTIME_ERROR_CLASSES,
RUNTIME_STREAM_CLASSES,
shapeHasAccessorSlots,
STRING,
typeEquals,
typeKey,
maybePromiseUnionArmsOk,
VOID,
} from "./ir.js";
import { DYN_CLASS_PROPERTIES, callSiteReturnType, classValueConstructor } from "./ir.js";
import { unionWideningTags } from "./analysis.js";
import { alwaysReturns } from "./control-flow.js";
import { dynamicOperationOf, functionTier } from "./tiers.js";
import { LIB_FN_SIGS, pipelinePumpedRuns } from "./builtin-signatures.js";
/** The promisified crypto calls' value kinds besides checked-dynamic. */
const CRYPTO_PROMISE_VALUES: Readonly<Record<string, "buffer" | "view" | "number" | "dyn">> = {
"crypto.randomBytesPromise": "buffer",
"crypto.pbkdf2Promise": "buffer",
"crypto.scryptPromise": "buffer",
"crypto.hkdfPromise": "dyn",
"crypto.randomFillPromise": "view",
"crypto.randomFillPromiseDyn": "dyn",
"crypto.randomIntPromise": "number",
"crypto.generateKeyPromise": "dyn",
};
import { REGEX_INTRINSIC_SIGS, STR_INTRINSIC_SIGS } from "./intrinsic-signatures.js";
/** 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;
}
interface VirtualCallInfo {
declared: boolean;
hasOverride: boolean;
implementation: IrFunction | undefined;
}
interface RecordValidation {
fields: Map<string, IrType>;
initializationFields: Map<string, IrType>;
hasAccessorSlots: boolean;
}
interface ClassValidation {
fields: Map<string, Map<string, IrType>>;
hierarchy: Set<string>;
implementations: Map<string, IrClassDef[]>;
virtualCalls: Map<string, Map<string, VirtualCallInfo>>;
}
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 };
if (mod.workers !== undefined && typeof mod.workers !== "boolean")
errors.push({ message: "worker execution mode must be a boolean", loc: moduleLoc });
if (mod.lifetimeRegions !== undefined && mod.lifetimeRegions !== true)
errors.push({ message: "lifetimeRegions must be true when present", loc: moduleLoc });
if (mod.workers && mod.lib !== undefined)
errors.push({
message: "worker execution requires a native static executable",
loc: moduleLoc,
});
for (const entry of mod.ffiImports ?? []) {
if (
mod.workers &&
entry.params.some(
(parameter) => isFfiCallbackParam(parameter) && parameter.callback.invoke === "foreign",
)
)
errors.push({
message: "worker execution cannot install foreign-thread callbacks",
loc: moduleLoc,
});
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 ?? []) {
if (entry.callbackOperation !== undefined) {
const fail = (detail: string): void => {
errors.push({
message: `FFI callback operation "${entry.name}" ${detail}`,
loc: moduleLoc,
});
};
if (!entry.library || entry.library.includes("\0")) fail("requires a library name");
if (entry.callbackOperation === "register") {
const param = entry.params[0];
if (
entry.params.length !== 1 ||
!param ||
!isFfiCallbackParam(param) ||
param.callback.id !== "callback" ||
param.callback.lifetime !== "retained" ||
param.callback.invoke !== "script-thread" ||
param.callback.params.some(
(p) => typeof p !== "string" || ["cstring", "string", "bytes"].includes(p),
) ||
entry.returns !== "pointer" ||
entry.callbackTarget !== undefined
)
fail("has an invalid registration ABI");
} else if (entry.callbackOperation === "release") {
const target = entry.callbackTarget ? ffiByName.get(entry.callbackTarget) : undefined;
if (
entry.params.length !== 0 ||
entry.returns !== "void" ||
target?.callbackOperation !== "register" ||
target.library !== entry.library
)
fail("has an invalid release target or ABI");
} else fail("has an invalid operation");
} else if (entry.callbackTarget !== undefined) {
errors.push({
message: `FFI binding "${entry.name}" has a callback target without a release operation`,
loc: moduleLoc,
});
}
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.params.some((p) => p === "i64" || p === "u64" || p === "pointer")) {
errors.push({
message: `FFI foreign callback "${entry.name}:${param.callback.id}" has unsupported 64-bit or pointer arguments`,
loc: moduleLoc,
});
}
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" || cls === "cstring"
? 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>();
const classFields = new Map<string, Map<string, IrType>>();
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);
if (cls.localCaptures !== undefined && (cls.runtime || cls.genericOf !== undefined)) {
errors.push({
message: `class ${cls.name}: local class cannot use a runtime or generic family layout`,
loc: cls.loc,
});
}
if (cls.localCaptures !== undefined) {
const environment = cls.fields.find(
(field) => field.name === `%classEnvironment:${cls.name}`,
);
if (environment?.type.kind !== "classval" || environment.type.className !== cls.name) {
errors.push({
message: `class ${cls.name}: local class requires its constructor environment field`,
loc: cls.loc,
});
}
}
if (cls.localBaseCapture !== undefined) {
const base = cls.localCaptures?.[cls.localBaseCapture];
if (
!Number.isInteger(cls.localBaseCapture) ||
base?.type.kind !== "classval" ||
base.type.className !== cls.base
) {
errors.push({ message: `class ${cls.name}: invalid local base capture`, loc: cls.loc });
}
}
if (cls.baseValueGlobal !== undefined) {
const global = mod.globals?.find((global) => global.id === cls.baseValueGlobal);
if (
cls.localBaseCapture !== undefined ||
!cls.base ||
global?.type.kind !== "classval" ||
global.type.className !== cls.base
) {
errors.push({ message: `class ${cls.name}: invalid computed base global`, loc: cls.loc });
}
}
if (cls.jsLength !== undefined && (!Number.isSafeInteger(cls.jsLength) || cls.jsLength < 0)) {
errors.push({ message: `class ${cls.name}: invalid constructor length`, loc: cls.loc });
}
if (
cls.prototypeNames !== undefined &&
new Set(cls.prototypeNames).size !== cls.prototypeNames.length
) {
errors.push({ message: `class ${cls.name}: duplicate prototype member names`, loc: cls.loc });
}
if (
cls.tracksOwnFields &&
(cls.runtime ||
cls.fields.find((field) => field.name === DYN_CLASS_PROPERTIES)?.type.kind !== "dyn")
) {
errors.push({
message: `class ${cls.name}: field presence tracking requires a native dyn property bag`,
loc: cls.loc,
});
}
if (cls.prototypeDataHelper !== undefined) {
const helper = functionsByName.get(cls.prototypeDataHelper);
if (
!helper ||
helper.params.length !== 0 ||
helper.returnType.kind !== "dyn" ||
helper.captures !== undefined ||
helper.classCaptures !== undefined
) {
errors.push({
message: `class ${cls.name}: prototype data helper must be a noncapturing () => dyn function`,
loc: cls.loc,
});
}
}
if (cls.instancePrototypeHelper !== undefined) {
const helper = functionsByName.get(cls.instancePrototypeHelper);
const receiver = helper?.params[0]?.type;
if (
!helper ||
helper.params.length !== 1 ||
receiver?.kind !== "object" ||
receiver.className !== cls.name ||
helper.returnType.kind !== "dyn" ||
helper.captures !== undefined ||
helper.classCaptures !== undefined
) {
errors.push({
message: `class ${cls.name}: instance prototype helper must be a noncapturing (instance) => dyn function`,
loc: cls.loc,
});
}
}
for (const symbol of cls.symbolFields ?? []) {
const global = mod.globals?.find((global) => global.id === symbol.globalId);
if (
global?.type.kind !== "symbol" ||
!cls.fields.some((field) => field.name === symbol.field)
) {
errors.push({ message: `class ${cls.name}: invalid symbol field metadata`, loc: cls.loc });
}
}
// Reuse duplicate detection as the field index for function validation.
// Array.find selected the first field, even in an invalid duplicate layout.
const seen = new Map<string, IrType>();
for (const f of cls.fields) {
if (seen.has(f.name)) {
errors.push({ message: `class ${cls.name}: duplicate field "${f.name}"`, loc: cls.loc });
} else seen.set(f.name, f.type);
if (isUnitType(f.type)) {
errors.push({
message: `class ${cls.name}: field "${f.name}" is ${f.type.kind}`,
loc: cls.loc,
});
}
}
// Duplicate class names, like classesByName, select the last declaration.
if (seen.size > 0) classFields.set(cls.name, seen);
else classFields.delete(cls.name);
// 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;
}
if (
base.localCaptures !== undefined &&
cls.localBaseCapture === undefined &&
cls.baseValueGlobal === undefined
) {
errors.push({
message: `class ${cls.name}: local base requires a captured constructor`,
loc: cls.loc,
});
}
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,
});
}
}
// Family membership is nominal; an instance may have a separately
// specialized base. The family's inherited prefix must remain valid.
const physicalRoot = (cls: IrClassDef): IrClassDef | undefined => {
const seen = new Set<string>();
let current: IrClassDef | undefined = cls;
while (current?.base !== undefined) {
if (seen.has(current.name)) return undefined;
seen.add(current.name);
current = classesByName.get(current.base);
}
return current;
};
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,
});
} else if (family === cls || family.genericOf !== undefined) {
errors.push({
message: `class ${cls.name}: generic family "${cls.genericOf}" must name an erased declaration`,
loc: cls.loc,
});
} else if (physicalRoot(cls) !== physicalRoot(family)) {
errors.push({
message: `class ${cls.name}: generic family "${cls.genericOf}" has a different storage root`,
loc: cls.loc,
});
} else if (
family.fields.some(
(field, index) =>
cls.fields[index]?.name !== field.name ||
!typeEquals(cls.fields[index]!.type, field.type),
)
) {
errors.push({
message: `class ${cls.name}: generic family "${cls.genericOf}" has an incompatible prefix`,
loc: cls.loc,
});
}
}
const noLoc: SrcLoc = { file: mod.sourceFile, start: 0, end: 0 };
const recordsById = new Map<string, IrRecordShape>();
const recordIds = new Set((mod.records ?? []).map((record) => record.id));
const recordValidation = new Map<string, RecordValidation>();
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 fields = new Map<string, IrType>();
for (const f of rec.fields) {
if (fields.has(f.name)) {
errors.push({ message: `record ${rec.id}: duplicate field "${f.name}"`, loc: noLoc });
} else fields.set(f.name, f.type);
// Bare unit and void types have no record-field storage; unit values
// use their union representation.
if (f.type.kind === "void" || isUnitType(f.type)) {
errors.push({
message: `record ${rec.id}: field "${f.name}" is ${f.type.kind}`,
loc: noLoc,
});
}
if (f.type.kind === "record" && !recordIds.has(f.type.shapeId)) {
errors.push({
message: `record ${rec.id}: field "${f.name}" references undeclared shape "${f.type.shapeId}"`,
loc: noLoc,
});
}
}
// Reads historically use the first duplicate field, while literal and
// clone checks use the last. Preserve both on malformed shapes; valid
// shapes share one index. All indexes belong to this validation call.
recordValidation.set(rec.id, {
fields,
initializationFields:
fields.size === rec.fields.length
? fields
: new Map(rec.fields.map((field) => [field.name, field.type])),
hasAccessorSlots: shapeHasAccessorSlots(rec),
});
// 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));
let positionsComplete = true;
for (let i = 0; i < rec.fields.length; i++) {
if (!names.has(String(i))) {
positionsComplete = false;
break;
}
}
if (rec.fields.length === 0 || names.size !== rec.fields.length || !positionsComplete) {
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);
}
for (const u of mod.unions ?? []) {
if (u.arms.length < 2) {
errors.push({ message: `union ${u.id}: fewer than 2 arms`, loc: noLoc });
}
if (u.discriminant) {
const { field, cases } = u.discriminant;
const tags = new Set<number>();
const values = new Set<string>();
for (const candidate of cases) {
const arm = u.arms[candidate.tag];
const record = arm?.kind === "record" ? recordsById.get(arm.shapeId) : undefined;
const member = record ? recordValidation.get(record.id)?.fields.get(field) : undefined;
if (
!Number.isInteger(candidate.tag) ||
candidate.tag < 0 ||
!member ||
tags.has(candidate.tag)
) {
errors.push({
message: `union ${u.id}: invalid discriminant tag ${candidate.tag}`,
loc: noLoc,
});
}
tags.add(candidate.tag);
if (candidate.values.length === 0) {
errors.push({
message: `union ${u.id}: empty discriminant values for tag ${candidate.tag}`,
loc: noLoc,
});
}
for (const value of candidate.values) {
if (value === undefined) {
errors.push({ message: `union ${u.id}: undefined discriminant value`, loc: noLoc });
continue;
}
const fieldTypes =
member?.kind === "union"
? (unionsById.get(member.unionId)?.arms ?? [])
: member
? [member]
: [];
const valid = fieldTypes.some((type) =>
typeof value === "string"
? type.kind === "string"
: typeof value === "boolean"
? type.kind === "bool"
: typeof value === "number" && Number.isFinite(value) && type.kind === "f64",
);
const key = typeof value + ":" + String(value);
if (!valid || values.has(key)) {
errors.push({
message: `union ${u.id}: invalid or repeated discriminant value ${key}`,
loc: noLoc,
});
}
values.add(key);
}
}
for (let tag = 0; tag < u.arms.length; tag++) {
if (u.arms[tag]!.kind === "record" && !tags.has(tag)) {
errors.push({
message: `union ${u.id}: missing discriminant for record arm ${tag}`,
loc: noLoc,
});
}
}
}
u.arms.forEach((arm, i) => {
// The unit kinds (undefinedT/nullT) are valid arms — union membership
// is the ONLY place they may appear. Container payloads preserve
// identity; a union carries at most one promise arm (maybe-promise).
if (
arm.kind === "void" ||
arm.kind === "union" ||
arm.kind === "dyn" ||
arm.kind === "date" ||
arm.kind === "generator"
) {
errors.push({ message: `union ${u.id}: arm ${i} is ${arm.kind}`, loc: noLoc });
}
if (arm.kind === "promise" && !maybePromiseUnionArmsOk(u.arms)) {
errors.push({
message: `union ${u.id}: ${arm.kind} arm ${i} beside another promise arm`,
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 (g.tdz && g.type.kind !== "record" && g.type.kind !== "func" && g.type.kind !== "dyn") {
errors.push({
message: `TDZ global "${g.name}" must have record, function, or checked-value storage`,
loc: noLoc,
});
}
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,
});
}
}
}
// Class metadata is invariant across functions. Keep field lookup, hierarchy
// membership, and virtual-call resolution local to this validation invocation.
const classValidation: ClassValidation = {
fields: classFields,
hierarchy: new Set(),
implementations: new Map(),
virtualCalls: new Map(),
};
for (const cls of classesByName.values()) {
if (cls.base !== undefined) {
classValidation.hierarchy.add(cls.name);
classValidation.hierarchy.add(cls.base);
}
if (cls.name === RUNTIME_EMITTER_CLASS) classValidation.hierarchy.add(cls.name);
for (const method of new Set(cls.methods ?? [])) {
if (cls.abstractMethods?.includes(method)) continue;
let implementations = classValidation.implementations.get(method);
if (!implementations) classValidation.implementations.set(method, (implementations = []));
implementations.push(cls);
}
}
for (const fn of mod.functions) {
validateFunction(
fn,
functionsByName,
ffiByName,
classesByName,
recordsById,
recordValidation,
unionsById,
globalsById,
classValidation,
errors,
mod.workers === true,
mod.lifetimeRegions === true,
new Set(mod.threadSlotKeys ?? []),
);
}
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>,
recordValidation: Map<string, RecordValidation>,
unions: Map<string, IrUnionDef>,
globals: Map<string, IrGlobal>,
classValidation: ClassValidation,
errors: IrValidationError[],
workers: boolean,
lifetimeRegions: boolean,
threadSlotKeys: ReadonlySet<string>,
): 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 });
// Invariant I1 (ir/tiers.ts): a static function reaches dynamic values
// only through boundary operations and moves.
const staticTier = functionTier(fn) === "static";
const checkTier = (e: IrExpr): void => {
const operation = dynamicOperationOf(e);
if (operation !== null)
err(`static-tier function performs the dynamic operation ${operation}`, e.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);
}
// Virtual slots and constructor ABIs follow physical inheritance only.
// Erased family identity does not supply their specialized layout or ABI.
const isPhysicalSubclass = (sub: string, sup: string): boolean => {
const seen = new Set<string>([sub]);
for (let c = classes.get(sub)?.base; c !== undefined; c = classes.get(c)?.base) {
if (seen.has(c)) return false;
seen.add(c);
if (c === sup) return true;
}
return false;
};
// Object casts may also recover an erased family. The module-level
// prefix check validates the fields exposed by that view.
const isStrictSubclass = (sub: string, sup: string): boolean => {
const seen = new Set<string>();
for (let c = classes.get(sub); c; c = c.base === undefined ? undefined : classes.get(c.base)) {
if (seen.has(c.name)) return false;
seen.add(c.name);
if (c.name !== sub && c.name === sup) return true;
const families = new Set<string>();
for (
let family = c.genericOf === undefined ? undefined : classes.get(c.genericOf);
family;
family = family.base === undefined ? undefined : classes.get(family.base)
) {
if (families.has(family.name)) return false;
families.add(family.name);
if (family.name === sup) return true;
}
}
return false;
};
const hierarchy = classValidation.hierarchy;
const virtualCallInfo = (className: string, method: string): VirtualCallInfo => {
let methods = classValidation.virtualCalls.get(className);
const cached = methods?.get(method);
if (cached) return cached;
if (!methods) classValidation.virtualCalls.set(className, (methods = new Map()));
let declared = false;
let implementation: IrFunction | undefined;
for (
let c = classes.get(className);
c;
c = c.base !== undefined ? classes.get(c.base) : undefined
) {
if (c.methods?.includes(method)) {
declared = true;
if (!c.abstractMethods?.includes(method)) {
implementation = functions.get(`%${c.name}.${method}`);
break;
}
}
}
let hasOverride = false;
if (declared) {
// Preserve class-table order when choosing an abstract slot's ABI.
for (const c of classValidation.implementations.get(method) ?? []) {
if (!isPhysicalSubclass(c.name, className)) continue;
hasOverride = true;
implementation ??= functions.get(`%${c.name}.${method}`);
if (implementation) break;
}
}
const result = { declared, hasOverride, implementation };
methods.set(method, result);
return result;
};
// 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 (l.tdz && !l.boxed) err(`TDZ local "${l.name}" must be boxed`, fn.loc);
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);
}
// Exception snapshots may be captured or passed to internal conversion
// helpers. They are not source-level types and never return as values.
if (fn.returnType.kind === "caught") err("return type is caught", fn.loc);
for (const p of fn.params) {
if (p.type.kind === "caught" && !fn.name.startsWith("%caught."))
err(`param "${p.name}" is caught-typed`, fn.loc);
if (!locals.has(p.localId)) {
err(`param "${p.name}" has no local entry "${p.localId}"`, fn.loc);
}
}
for (const c of [...(fn.captures ?? []), ...(fn.classCaptures ?? [])]) {
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);
else if (!typeEquals(local.type, c.type))
err(`capture local "${c.localId}" has the wrong type`, fn.loc);
}
if (fn.classCaptures !== undefined) {
const self = fn.params[0];
const cls = self?.type.kind === "object" ? classes.get(self.type.className) : undefined;
if (fn.captures !== undefined || cls?.localCaptures === undefined) {
err(
"class captures require a local-class instance receiver and no closure environment",
fn.loc,
);
}
for (const capture of fn.classCaptures) {
const slot = cls?.localCaptures?.[capture.slot];
if (!Number.isInteger(capture.slot) || !slot || !typeEquals(slot.type, capture.type)) {
err(`class capture "${capture.name}" has an invalid slot or type`, 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);
}
};
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 checkNullishOperands(e: IrExpr & { kind: "nullish" }): void {
expectType(e.right, e.type, "nullish right operand");
// 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);
return;
}
if (e.left.type.kind !== "union") {
err(`nullish left must be a union, got ${e.left.type.kind}`, e.loc);
return;
}
const def = unions.get(e.left.type.unionId);
if (!def) {
err(`nullish left references unknown union ${e.left.type.unionId}`, e.loc);
return;
}
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);
}
}
}
function checkBranchingTree(root: IrExpr): void {
// Predicates and conditional dispatch can nest in either direction.
// Preserve child-before-parent diagnostics with bounded native stack
// use, including trees that mix the three branching operators.
const pending: { expr: IrExpr; visited: boolean }[] = [{ expr: root, visited: false }];
while (pending.length !== 0) {
const task = pending.pop()!;
const e = task.expr;
if (!task.visited) {
if (e.kind === "logical" || e.kind === "nullish") {
pending.push({ expr: e, visited: true });
pending.push({ expr: e.right, visited: false });
pending.push({ expr: e.left, visited: false });
} else if (e.kind === "ternary") {
pending.push({ expr: e, visited: true });
pending.push({ expr: e.else_, visited: false });
pending.push({ expr: e.then, visited: false });
pending.push({ expr: e.cond, visited: false });
} else {
checkExpr(e);
}
continue;
}
if (e.kind === "logical") {
// checkExpr already checked the root's tier.
if (staticTier && e !== root) checkTier(e);
if (
e.type.kind !== "f64" &&
e.type.kind !== "string" &&
e.type.kind !== "bool" &&
e.type.kind !== "union" &&
e.type.kind !== "dyn"
) {
err(`logical ${e.op} must be f64|string|bool|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`);
} else if (e.kind === "nullish") {
checkNullishOperands(e);
} else if (e.kind === "ternary") {
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);
}
}
}
// Keep expression families in separate functions so native bootstrap builds
// do not feed LLVM a single control-flow graph containing every IR check.
/** fieldAbsent nodes that sit directly in a field-slot write (a
* recordLit field, recordSet value, recordClone override, or a ternary
* arm in one of those positions) — the only places the absent state may
* appear. */
const fieldSlotValues = new Set<IrExpr>();
function checkSlotValue(e: IrExpr): void {
const mark = (v: IrExpr): void => {
if (v.kind === "fieldAbsent") fieldSlotValues.add(v);
else if (v.kind === "ternary") {
mark(v.then);
mark(v.else_);
}
};
mark(e);
checkExpr(e);
}
function checkExpr(e: IrExpr): void {
if (staticTier) checkTier(e);
switch (e.kind) {
case "numLit":
case "strLit":
case "moduleNsRef":
case "boolLit":
case "unitLit":
case "varRef":
case "bin":
case "unary":
case "incDec":
case "fieldIncDec":
case "assignExpr":
case "seqExpr":
case "dynDestrCheck":
case "dynIterN":
case "toBool":
case "logical":
case "nullish":
case "ternary":
case "unionEq":
case "unionFuncEq":
return checkScalarExpr(e);
case "strConcat":
case "strEq":
case "strCmp":
case "optChain":
case "chainRecv":
case "orDefault":
case "toString":
case "strIntrinsic":
case "regexLit":
case "templateStrings":
case "regexIntrinsic":
return checkStringExpr(e);
case "arrayLit":
case "arrayNewLen":
case "arrayGet":
case "arrayHas":
case "arrayState":
case "bytesNew":
case "bytesIntrinsic":
case "arrIntrinsic":
return checkArrayExpr(e);
case "mapNew":
case "mapIntrinsic":
case "setNew":
case "setIntrinsic":
return checkCollectionExpr(e);
case "call":
case "ffiCall":
case "closure":
case "callValue":
return checkCallExpr(e);
case "selfRef":
case "new":
case "fieldGet":
case "promiseVoidWiden":
case "upcast":
case "downcast":
case "classRef":
case "newValue":
case "instanceOfValue":
case "instanceOf":
case "virtualCall":
return checkClassExpr(e);
case "recordLit":
case "recordClone":
case "recordGet":
case "recordKeyGet":
case "recordOvfHas":
case "recordNullProto":
case "recordOvfKeys":
case "recordHas":
case "fieldAbsent":
return checkRecordExpr(e);
case "dynFrom":
case "dynCall":
case "dynInvoke":
case "dynObjLit":
case "dynArrLit":
case "unionWrap":
case "dynTest":
case "dynKeyGet":
case "dynHasKey":
case "dynScalarEq":
case "caughtTest":
case "caughtCheck":
case "caughtToDyn":
case "caughtNarrow":
return checkDynamicExpr(e);
case "unionNarrow":
case "unionDisc":
case "unionKeyGet":
case "unionIsTag":
return checkUnionExpr(e);
case "unionRetype":
return checkUnionRetype(e);
case "intrinsic":
return checkIntrinsicExpr(e);
case "libCall":
return checkLibCall(e);
case "jsonStringify":
case "dynCheck":
return checkConversionExpr(e);
case "awaitExpr":
case "yieldExpr":
case "genResume":
case "awaitUnionExpr":
case "newPromise":
case "promiseWithResolvers":
return checkAsyncExpr(e);
default: {
const _exhaustive: never = e;
void _exhaustive;
}
}
}
function checkScalarExpr(
e: IrExpr & {
kind:
| "numLit"
| "strLit"
| "moduleNsRef"
| "boolLit"
| "unitLit"
| "varRef"
| "bin"
| "unary"
| "incDec"
| "fieldIncDec"
| "assignExpr"
| "seqExpr"
| "dynDestrCheck"
| "dynIterN"
| "toBool"
| "logical"
| "nullish"
| "ternary"
| "unionEq"
| "unionFuncEq";
},
): void {
switch (e.kind) {
case "numLit":
// ±Infinity and NaN are real literals (the globals
// `Infinity`/`NaN`, Number constants) — the backend spells them
// (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 === "regex" ||
e.left.type.kind === "url" ||
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 fieldType = classValidation.fields.get(e.className)?.get(e.field);
if (!cls) err(`fieldIncDec on undeclared class "${e.className}"`, e.loc);
else if (!fieldType) 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 ? fieldType.kind !== "dyn" : fieldType.kind !== "f64") {
err(
`fieldIncDec ${e.className}.${e.field} field/flag mismatch (${fieldType.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. Delegation also needs a local loop. Nested control
// flow is checked recursively; jumps still cannot leave the region.
const allowed = new Set([
"varDecl",
"assign",
"exprStmt",
"fieldSet",
"recordSet",
"recordKeySet",
"arraySet",
"arraySetLength",
"arraySetUndefined",
"arrayDelete",
"bytesSet",
"block",
"if",
"while",
]);
if (e.generatorDelegate) {
if (!fn.generator) err("delegation sequence outside a generator", e.loc);
allowed.add("return");
allowed.add("throw");
allowed.add("rethrow");
allowed.add("runtimeFence");
allowed.add("tryCatch");
}
if (e.diverges) {
const last = e.stmts[e.stmts.length - 1];
if (last?.kind !== "runtimeFence")
err("diverging seqExpr must end in a runtimeFence", e.loc);
allowed.add("runtimeFence");
}
const flat = (ss: IrStmt[]): void => {
for (const s of ss) {
if (!allowed.has(s.kind)) {
err(`seqExpr statement kind "${s.kind}" can leave the expression`, 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;
}
if (s.kind === "while") {
expectType(s.cond, BOOL, "seqExpr while condition");
checkExpr(s.cond);
flat(s.body);
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") err("dynDestrCheck value must be dyn", 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") err("dynIterN value must be dyn", 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":
case "nullish":
case "ternary":
checkBranchingTree(e);
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;
}
default: {
const _exhaustive: never = e;
void _exhaustive;
}
}
}
function checkStringExpr(
e: IrExpr & {
kind:
| "strConcat"
| "strEq"
| "strCmp"
| "optChain"
| "chainRecv"
| "orDefault"
| "toString"
| "strIntrinsic"
| "regexLit"
| "templateStrings"
| "regexIntrinsic";
},
): void {
switch (e.kind) {
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 "optChain": {
checkExpr(e.receiver);
// 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 === 0 || rest.length === def.arms.length) {
err("optChain receiver must have unit arms and at least 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.length === 1 ? rest[0]! : e.receiver.type);
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 "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 === "bigint" ||
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 (!/^[gimsuvy]*$/.test(e.flags)) {
err(`regexLit flags "${e.flags}" outside the supported alphabet (gimsuvy)`, 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 === "execGroups" || e.method === "execSpans" || e.method === "matchGroups") &&
e.args.slice(1).some((mask) => mask.kind !== "numLit")
) {
err(`regexIntrinsic ${e.method} masks must be numeric literals`, e.loc);
}
if (
e.method === "match" ||
e.method === "exec" ||
e.method === "execGroups" ||
e.method === "execSpans" ||
e.method === "matchGroups"
) {
// 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;
}
default: {
const _exhaustive: never = e;
void _exhaustive;
}
}
}
function checkArrayExpr(
e: IrExpr & {
kind:
| "arrayLit"
| "arrayNewLen"
| "arrayGet"
| "arrayHas"
| "arrayState"
| "bytesNew"
| "bytesIntrinsic"
| "arrIntrinsic";
},
): void {
switch (e.kind) {
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);
}
if (
e.capacity !== undefined &&
(!Number.isInteger(e.capacity) || e.capacity < e.elems.length || e.capacity > 1024)
) {
err(`arrayLit capacity ${e.capacity} must be an integer in [elements, 1024]`, 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 === "array") {
if (sk.elem.kind !== "f64") {
err(`bytesNew array source must hold f64, got ${sk.elem.kind}`, e.loc);
}
} else if (sk.kind !== "f64" && sk.kind !== "bytes" && sk.kind !== "dyn") {
err(`bytesNew source of kind ${sk.kind}`, e.loc);
}
}
if (e.from && e.source?.type.kind !== "dyn")
err("bytesNew from requires a dyn source", 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 DataView getters and setters use a bytes<u8>
// view; dataViewNew and byteOffset take ANY elem kind (views form
// over any typed array's storage, owners answer byteOffset 0).
const isDv = e.method.startsWith("dvGet") || e.method.startsWith("dvSet");
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 || isDv;
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);
} else if (
isNum &&
e.args[0]?.kind === "strLit" &&
!validByteNumberToken(e.method, e.args[0].value)
) {
err(`bytesIntrinsic ${e.method} invalid kind token '${e.args[0].value}'`, 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
> = {
buffer: { argTypes: [], minArgs: 0, result: DYN },
setFromDyn: { argTypes: [DYN, F64], minArgs: 1, result: VOID },
copyWithin: { argTypes: [F64, F64, F64], minArgs: 3, result: bytesOf(recv.elem) },
sortNumeric: { argTypes: [F64], minArgs: 1, result: BOOL },
sortCompare: { argTypes: e.args[0] ? [e.args[0].type] : [], minArgs: 1, result: VOID },
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: [
e.args[0]?.type.kind === "bytes"
? e.args[0].type
: 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}`);
});
const order = e.args[0];
if (
e.method === "sortNumeric" &&
(order?.kind !== "numLit" ||
(order.value !== 0 && order.value !== 1 && order.value !== -1))
)
err("bytesIntrinsic sortNumeric order must be the literal 0, 1 or -1", e.loc);
const comparator = e.args[0]?.type;
if (
e.method === "sortCompare" &&
(e.args.length !== 1 ||
comparator?.kind !== "func" ||
comparator.params.length > 2 ||
!comparator.params.every((param) => param.kind === "f64") ||
comparator.ret.kind !== "f64")
)
err("bytesIntrinsic sortCompare comparator must be (number, number) => number", e.loc);
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 === "copyWithin"
? { argTypes: [F64, F64, F64], result: e.receiver.type }
: e.method === "fill"
? { argTypes: [elem, F64, F64], result: e.receiver.type }
: e.method === "fillUndefined"
? { argTypes: [F64, F64], result: e.receiver.type }
: 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 === "indexEq"
? { argTypes: [F64, e.receiver.type, F64], result: BOOL }
: 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" ||
e.method === "sortPrimitive" ||
e.method === "toSortedPrimitive"
? { 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 === "spliceInsert"
? {
argTypes: [F64, F64, e.receiver.type],
result: e.receiver.type,
}
: e.method === "flatCopy" || e.method === "flatOne"
? { argTypes: [e.type], result: e.type }
: e.method === "shift"
? { argTypes: [], result: e.type } // union-checked below
: e.method === "sortValues"
? {
argTypes: [F64, null, F64, null],
result: VOID,
}
: e.method === "sortDense"
? {
argTypes: [null, BOOL, F64, null],
result: BOOL,
}
: { argTypes: [], result: F64 }; // length
if (
(e.method === "sortPrimitive" || e.method === "toSortedPrimitive") &&
elem.kind !== "string" &&
elem.kind !== "f64" &&
elem.kind !== "bool"
) {
err(`arrIntrinsic ${e.method} requires primitive elements, got ${elem.kind}`, e.loc);
}
if (e.method === "sortValues" || e.method === "sortDense") {
const arms = elem.kind === "union" ? unions.get(elem.unionId)?.arms : undefined;
if (!isSortValuesElement(elem, arms))
err(`arrIntrinsic ${e.method} on ${elem.kind} elements`, e.loc);
const cmp = e.args[e.method === "sortValues" ? 1 : 0]?.type;
if (
!cmp ||
cmp.kind !== "func" ||
cmp.params.length > 2 ||
!cmp.params.every((param) => typeEquals(param, elem)) ||
cmp.ret.kind !== "f64"
)
err(`arrIntrinsic ${e.method} comparator must be (elem, elem) => number`, e.loc);
if (e.method === "sortDense" && e.args[1]?.kind !== "boolLit")
err("arrIntrinsic sortDense owned flag must be a boolean literal", e.loc);
// A recognized comparator's key order: [order] for numbers,
// [order, key reader] for record and class elements. An order
// from 2 carries several keys (sortKeysOrder): the key count
// (2..8) in the low byte, a descending bit per key from bit 8, and
// a reader that also takes the key's index.
const order = e.args[2];
if (order) {
const count = order.kind === "numLit" && order.value >= 2 ? order.value & 0xff : 1;
if (
order.kind !== "numLit" ||
(order.value !== 1 &&
order.value !== -1 &&
!(
Number.isInteger(order.value) &&
count >= 2 &&
count <= 8 &&
(order.value >> 8) >> count === 0
))
)
err(`arrIntrinsic ${e.method} key order must be 1, -1 or a key-chain order`, e.loc);
const key = e.args[3]?.type;
if (
elem.kind === "f64"
? key !== undefined || count > 1
: elem.kind !== "record" && elem.kind !== "object"
)
err(`arrIntrinsic ${e.method} key order on ${elem.kind} elements`, e.loc);
const params = count > 1 ? 2 : 1;
if (
elem.kind !== "f64" &&
(!key ||
key.kind !== "func" ||
key.params.length !== params ||
!typeEquals(key.params[0]!, elem) ||
(params === 2 && key.params[1]!.kind !== "f64") ||
key.ret.kind !== "f64")
)
err(
`arrIntrinsic ${e.method} key reader must be (elem${params === 2 ? ", number" : ""}) => number`,
e.loc,
);
}
}
if (e.method === "getNumber" && elem.kind !== "f64") {
err(`arrIntrinsic getNumber requires f64 elements, got ${elem.kind}`, e.loc);
}
if (
e.method === "indexEq" &&
elem.kind !== "f64" &&
elem.kind !== "bool" &&
elem.kind !== "string"
) {
err(`arrIntrinsic indexEq requires primitive elements, got ${elem.kind}`, e.loc);
}
if (e.method === "flatCopy" && !typeEquals(e.type, e.receiver.type)) {
err("arrIntrinsic flatCopy result must match its receiver", e.loc);
}
if (e.method === "flatOne" && (elem.kind !== "array" || !typeEquals(e.type, elem))) {
err("arrIntrinsic flatOne result must match the nested array type", 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") &&
!(elem.kind === "dyn" && e.type.kind === "dyn")
) {
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
: e.method === "sortValues" || e.method === "sortDense"
? 2
: 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;
}
default: {
const _exhaustive: never = e;
void _exhaustive;
}
}
}
function checkCollectionExpr(
e: IrExpr & { kind: "mapNew" | "mapIntrinsic" | "setNew" | "setIntrinsic" },
): void {
switch (e.kind) {
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,
e.type.key.kind === "union" ? unions.get(e.type.key.unionId)?.arms : undefined,
)
) {
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");
if (value.kind === "dyn") {
expectType(e, DYN, "mapIntrinsic get result");
break;
}
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const rest = def ? def.arms.filter((a): boolean => 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: IrType[] =
value.kind === "union"
? (unions.get(value.unionId)?.arms ?? []).filter(
(a): boolean => 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;
}
if (
e.method === "valueSet" &&
!isSupportedSetElem(
value,
value.kind === "union" ? unions.get(value.unionId)?.arms : undefined,
)
) {
err(`mapIntrinsic valueSet element kind ${value.kind} (frontend must fence)`, e.loc);
}
const sig =
e.method === "clone"
? { argTypes: [], result: e.receiver.type }
: e.method === "keySet" || e.method === "valueSet"
? {
argTypes: [],
result: { kind: "set", elem: e.method === "keySet" ? key : value } as IrType,
}
: 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 / iterStart
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,
e.type.elem.kind === "union" ? unions.get(e.type.elem.unionId)?.arms : undefined,
)
) {
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 === "clone"
? { argTypes: [], result: e.receiver.type }
: 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 / iterStart
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;
}
default: {
const _exhaustive: never = e;
void _exhaustive;
}
}
}
function checkCallExpr(e: IrExpr & { kind: "call" | "ffiCall" | "closure" | "callValue" }): void {
switch (e.kind) {
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 (e.adapts === true && locals.get(e.captures[0] ?? "")?.type.kind !== "func") {
err(`closure ${e.fnName}: an adapter's first capture must be a function`, e.loc);
}
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);
// 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);
if (e.receiver !== undefined) {
checkExpr(e.receiver);
expectType(e.receiver, DYN, "callValue receiver");
}
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;
}
default: {
const _exhaustive: never = e;
void _exhaustive;
}
}
}
function checkClassExpr(
e: IrExpr & {
kind:
| "selfRef"
| "new"
| "fieldGet"
| "promiseVoidWiden"
| "upcast"
| "downcast"
| "classRef"
| "newValue"
| "instanceOfValue"
| "instanceOf"
| "virtualCall";
},
): void {
switch (e.kind) {
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 (cls.localCaptures !== undefined)
err(`new ${e.className}: local class requires a class value`, e.loc);
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 fieldType = classValidation.fields.get(e.className)?.get(e.field);
if (!cls) err(`fieldGet on undeclared class "${e.className}"`, e.loc);
else if (!fieldType) 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, fieldType)) {
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 (verified at run time) 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 === "func" && e.value.type.kind === "func") {
const source = e.value.type;
const target = e.type;
if (
source.rest !== target.rest ||
source.restAbi !== target.restAbi ||
source.argumentsAll !== target.argumentsAll ||
source.params.length !== target.params.length ||
!source.params.every((param, i) => typeEquals(param, target.params[i]!)) ||
source.ret.kind !== "object" ||
target.ret.kind !== "object" ||
!isStrictSubclass(source.ret.className, target.ret.className)
)
err(
"function upcast requires identical parameters and a covariant class return",
e.loc,
);
break;
}
if (e.kind === "upcast" && e.type.kind === "classval" && e.value.type.kind === "classval") {
const [sub, sup] = [e.value.type.className, e.type.className];
if (!isPhysicalSubclass(sub, sup)) {
err(`upcast: "${sub}" does not extend "${sup}"`, e.loc);
break;
}
const subClass = classes.get(sub);
const supClass = classes.get(sup);
const subValueCtor = subClass ? classValueConstructor(subClass) : null;
const supValueCtor = supClass ? classValueConstructor(supClass) : null;
const subCtor = subValueCtor ? functions.get(subValueCtor.fn) : undefined;
const supCtor = supValueCtor ? functions.get(supValueCtor.fn) : undefined;
if (!subCtor || !supCtor || !subValueCtor || !supValueCtor) {
err(`classval upcast: "${sub}"/"${sup}" lack constructor functions`, e.loc);
break;
}
const subArgs = subCtor.params.slice(subValueCtor.firstArg);
const supArgs = supCtor.params.slice(supValueCtor.firstArg);
const abiEqual =
subArgs.length === supArgs.length &&
subArgs.every((p, i) => typeEquals(p.type, supArgs[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.jsName === undefined && !cls.runtime) {
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(classValueConstructor(cls).fn)) {
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);
}
if (
(e.captures === undefined) !== (cls.localCaptures === undefined) ||
(e.captures?.length ?? 0) !== (cls.localCaptures?.length ?? 0)
) {
err(`classRef to "${e.className}" must supply its class captures`, e.loc);
}
(e.captures ?? []).forEach((id, index) => {
const local = locals.get(id);
const wanted = cls.localCaptures?.[index];
if (!local?.boxed || (wanted && !typeEquals(local.type, wanted.type))) {
err(`classRef capture "${id}" must name a box with the declared 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 calleeClass = classes.get(cls);
const valueCtor = calleeClass ? classValueConstructor(calleeClass) : null;
const ctor = valueCtor ? functions.get(valueCtor.fn) : undefined;
if (!ctor || !valueCtor) {
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).
const first = valueCtor.firstArg;
if (ctor.params.length !== e.args.length + first) {
err(
`newValue on "${cls}": ${e.args.length} args, ctor expects ${ctor.params.length - first}`,
e.loc,
);
}
e.args.forEach((a, i) => {
const p = ctor.params[i + first];
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 (
classes.get(e.classValue.type.className)?.localCaptures !== undefined &&
e.value.type.kind === "object" &&
classes.get(e.value.type.className)?.localCaptures !== undefined
)
break;
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.
const info = virtualCallInfo(e.className, e.method);
if (!info.declared) {
err(`virtualCall ${e.className}.${e.method}: no declaration on the base chain`, e.loc);
break;
}
if (!info.hasOverride) {
err(
`virtualCall ${e.className}.${e.method}: no concrete override below the static class`,
e.loc,
);
}
const impl = info.implementation;
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}`);
});
const returned = callSiteReturnType(impl);
if (
!typeEquals(e.type, returned) &&
!(
e.type.kind === "object" &&
returned.kind === "object" &&
isStrictSubclass(returned.className, e.type.className)
)
) {
err(`virtualCall ${e.className}.${e.method} result type mismatch`, e.loc);
}
// Closed target sets name distinct implementations at or below the
// static class's chain, each an existing method function.
if (e.targets !== undefined) {
if (e.targets.length < 2 || new Set(e.targets).size !== e.targets.length)
err(
`virtualCall ${e.className}.${e.method}: targets must be distinct, at least two`,
e.loc,
);
for (const target of e.targets) {
if (!functions.has(`%${target}.${e.method}`))
err(
`virtualCall ${e.className}.${e.method}: target ${target} has no implementation`,
e.loc,
);
else if (
target !== e.className &&
!isStrictSubclass(target, e.className) &&
!isStrictSubclass(e.className, target)
)
err(`virtualCall ${e.className}.${e.method}: target ${target} is unrelated`, e.loc);
}
}
break;
}
default: {
const _exhaustive: never = e;
void _exhaustive;
}
}
}
function checkRecordExpr(
e: IrExpr & {
kind:
| "recordLit"
| "recordClone"
| "recordGet"
| "recordKeyGet"
| "recordOvfHas"
| "recordNullProto"
| "recordOvfKeys"
| "recordHas"
| "fieldAbsent";
},
): void {
switch (e.kind) {
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;
}
if (e.nullProto && (!shape.indexValue || shape.tuple))
err(`recordLit nullProto on ${shape.id}: requires an index-signature record`, e.loc);
const want = recordValidation.get(shape.id)!.initializationFields;
const seen = new Set<string>();
for (const f of e.fields) {
if (f.overflow) checkExpr(f.value);
else checkSlotValue(f.value);
if (seen.has(f.name)) err(`recordLit initializes field "${f.name}" twice`, e.loc);
seen.add(f.name);
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).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 || recordValidation.get(shape.id)!.hasAccessorSlots) {
err(`recordClone requires a plain declared-field shape, got ${shape.id}`, e.loc);
}
expectType(e.source, e.type, "recordClone source");
const want = recordValidation.get(shape.id)!.initializationFields;
const seen = new Set<string>();
for (const f of e.overrides) {
checkSlotValue(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 "recordHas": {
checkExpr(e.obj);
const shape = records.get(e.shapeId);
if (!shape) err(`recordHas on undeclared shape "${e.shapeId}"`, e.loc);
else if (!recordValidation.get(e.shapeId)?.fields.has(e.field))
err(`shape ${e.shapeId} has no field "${e.field}"`, e.loc);
expectType(e.obj, { kind: "record", shapeId: e.shapeId }, "recordHas receiver");
if (e.type.kind !== "bool") err("recordHas must be bool", e.loc);
break;
}
case "fieldAbsent": {
const def = unions.get(e.unionId);
if (!def) err(`fieldAbsent of undeclared union "${e.unionId}"`, e.loc);
else if (!def.arms.some((arm) => arm.kind === "undefinedT"))
err(`fieldAbsent of ${e.unionId}: the union has no undefined arm`, e.loc);
if (e.type.kind !== "union" || e.type.unionId !== e.unionId)
err("fieldAbsent must have its union type", e.loc);
if (!fieldSlotValues.has(e)) err("fieldAbsent outside a record field write", e.loc);
break;
}
case "recordGet": {
checkExpr(e.obj);
const shape = records.get(e.shapeId);
const field = recordValidation.get(e.shapeId)?.fields.get(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)) {
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" &&
(() => {
const result = unions.get(e.type.unionId);
const source = t.kind === "union" ? unions.get(t.unionId) : undefined;
return (
!!result &&
(result.arms.some((a) => typeEquals(a, t)) ||
(!!source && unionWideningTags(source.arms, result.arms) !== null))
);
})()) ||
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 "recordOvfHas": {
checkExpr(e.obj);
checkExpr(e.key);
const shape = records.get(e.shapeId);
if (!shape) err(`recordOvfHas on undeclared shape "${e.shapeId}"`, e.loc);
else if (!shape.indexValue || shape.tuple)
err(`recordOvfHas on ${e.shapeId}: requires an index-signature record`, e.loc);
expectType(e.obj, { kind: "record", shapeId: e.shapeId }, "recordOvfHas receiver");
expectType(e.key, STRING, "recordOvfHas key");
if (e.type.kind !== "bool") err("recordOvfHas must be bool", e.loc);
break;
}
case "recordNullProto": {
checkExpr(e.obj);
const shape = records.get(e.shapeId);
if (!shape) err(`recordNullProto on undeclared shape "${e.shapeId}"`, e.loc);
else if (!shape.indexValue || shape.tuple)
err(`recordNullProto on ${e.shapeId}: requires an index-signature record`, e.loc);
expectType(e.obj, { kind: "record", shapeId: e.shapeId }, "recordNullProto receiver");
if (e.type.kind !== "bool") err("recordNullProto must be bool", 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;
}
default: {
const _exhaustive: never = e;
void _exhaustive;
}
}
}
function checkDynamicExpr(
e: IrExpr & {
kind:
| "dynFrom"
| "dynCall"
| "dynInvoke"
| "dynObjLit"
| "dynArrLit"
| "unionWrap"
| "dynTest"
| "dynKeyGet"
| "dynHasKey"
| "dynScalarEq"
| "caughtTest"
| "caughtCheck"
| "caughtToDyn"
| "caughtNarrow";
},
): void {
switch (e.kind) {
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 "dynCall": {
checkExpr(e.callee);
if (e.calleeNameValue !== undefined) {
checkExpr(e.calleeNameValue);
expectType(e.calleeNameValue, STRING, "dynCall callee name");
}
if (e.receiver !== undefined) {
checkExpr(e.receiver);
expectType(e.receiver, DYN, "dynCall receiver");
}
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);
if (e.calleeNameValue !== undefined) {
checkExpr(e.calleeNameValue);
expectType(e.calleeNameValue, STRING, "dynInvoke callee name");
}
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" && f.key.type.kind !== "dyn")
err(`dynObjLit key of kind ${f.key.type.kind} (must be string or dyn)`, 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);
if (
e.bytesElem !== undefined &&
(e.test !== "bytes" || !Object.hasOwn(BYTES_ELEMENT_NAME, e.bytesElem))
) {
err("dynTest bytesElem requires a valid bytes test", e.loc);
}
break;
}
case "dynKeyGet": {
checkExpr(e.value);
checkExpr(e.key);
expectType(e.value, { kind: "dyn" }, "dynKeyGet operand");
if (e.key.type.kind !== "string" && e.key.type.kind !== "dyn")
err(`dynKeyGet key is ${e.key.type.kind}, not string or dyn`, 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 (!classes.has(e.className)) {
err(`caughtTest instanceof against undeclared 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" && classes.has(t.className));
if (!ok) {
err(
`caughtNarrow to ${t.kind === "object" ? `undeclared class "${t.className}"` : t.kind}`,
e.loc,
);
}
break;
}
default: {
const _exhaustive: never = e;
void _exhaustive;
}
}
}
function checkUnionRetype(e: IrExpr & { kind: "unionRetype" }): void {
checkExpr(e.value);
const from = unions.get(e.fromId);
const to = unions.get(e.unionId);
if (!from || !to) {
err(`unionRetype between undeclared unions "${e.fromId}" and "${e.unionId}"`, e.loc);
return;
}
expectType(e.value, { kind: "union", unionId: e.fromId }, "unionRetype operand");
if (!typeEquals(e.type, { kind: "union", unionId: e.unionId }))
err(`unionRetype type ${e.type.kind} != union ${e.unionId}`, e.loc);
if (e.tags.length === 0) err(`unionRetype with no tags`, e.loc);
for (const tag of e.tags) {
const arm = from.arms[tag];
const target = to.arms[tag];
if (!Number.isInteger(tag) || !arm || !target || !typeEquals(arm, target))
err(`unionRetype tag ${tag} is not the same arm in ${e.fromId} and ${e.unionId}`, e.loc);
}
}
function checkUnionExpr(
e: IrExpr & { kind: "unionNarrow" | "unionDisc" | "unionKeyGet" | "unionIsTag" },
): void {
switch (e.kind) {
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"
? recordValidation.get(arm.shapeId)?.fields.get(e.field)
: arm.kind === "object"
? classValidation.fields.get(arm.className)?.get(e.field)
: 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)) ||
(t.kind === "union" &&
!!resultUnion &&
(() => {
const source = unions.get(t.unionId);
return !!source && unionWideningTags(source.arms, resultUnion.arms) !== null;
})());
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 ? recordValidation.get(shape.id)?.fields.get(literal) : 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;
}
default: {
const _exhaustive: never = e;
void _exhaustive;
}
}
}
function checkIntrinsicExpr(e: IrExpr & { kind: "intrinsic" }): void {
if (e.name === "module.tdzCheck") {
const [flag, name] = e.args;
if (e.args.length !== 2 || flag === undefined || name?.kind !== "strLit") {
err("module.tdzCheck takes a flag and a binding-name literal", e.loc);
return;
}
checkExpr(flag);
if (flag.type.kind !== "bool") err("module.tdzCheck flag must be bool", e.loc);
if (e.type.kind !== "void") err("module.tdzCheck must be void", e.loc);
return;
}
if (
e.name === "region.create" ||
e.name === "region.enter" ||
e.name === "region.leave" ||
e.name === "region.release"
) {
if (!lifetimeRegions) err(`${e.name} outside a lifetimeRegions module`, e.loc);
const takesId = e.name === "region.enter" || e.name === "region.release";
if (e.args.length !== (takesId ? 1 : 0))
err(`${e.name} takes ${takesId ? "one region id" : "no arguments"}`, e.loc);
for (const a of e.args) {
checkExpr(a);
if (a.type.kind !== "f64") err(`${e.name} id must be f64`, e.loc);
}
if (e.type.kind !== (e.name === "region.create" ? "f64" : "void"))
err(`${e.name} has type ${e.name === "region.create" ? "f64" : "void"}`, e.loc);
return;
}
if (
e.name === "itable.open" ||
e.name === "itable.find" ||
e.name === "itable.insert" ||
e.name === "itable.value" ||
e.name === "itable.size"
) {
for (const a of e.args) checkExpr(a);
const [first, key, value] = e.args;
const arity = {
"itable.open": 2,
"itable.find": 2,
"itable.insert": 3,
"itable.value": 1,
"itable.size": 1,
}[e.name];
if (e.args.length !== arity) err(`${e.name} takes ${arity} arguments`, e.loc);
else if (e.name === "itable.open") {
if (first!.type.kind !== "string" || key!.type.kind !== "f64")
err("itable.open takes a string name and an f64 key kind", e.loc);
} else if (first!.type.kind !== "f64") err(`${e.name} handle must be f64`, e.loc);
else if (
(e.name === "itable.find" || e.name === "itable.insert") &&
key!.type.kind !== "string" &&
key!.type.kind !== "f64" &&
!(
key!.type.kind === "array" &&
(key!.type.elem.kind === "f64" || key!.type.elem.kind === "string")
)
)
err(`${e.name} key must be string, f64, or an array of either`, e.loc);
else if (e.name === "itable.insert" && !isSupportedMapValue(value!.type))
err(`itable.insert value ${typeKey(value!.type)} is not a table value type`, e.loc);
if (e.name === "itable.value") {
if (!isSupportedMapValue(e.type))
err(`itable.value type ${typeKey(e.type)} is not a table value type`, e.loc);
} else if (e.type.kind !== "f64") err(`${e.name} has type f64`, e.loc);
return;
}
if (
e.name === "slots.get" ||
e.name === "slots.has" ||
e.name === "slots.set" ||
e.name === "slots.delete"
) {
for (const a of e.args) checkExpr(a);
const [name, slots, key, value] = e.args;
const arity = e.name === "slots.set" ? 4 : 3;
const isSlots = (t: IrType): boolean => t.kind === "object" && t.className === "%ThreadSlots";
// The key's class or one of its bases is a declared key.
const keyed = (className: string): boolean => {
for (let c: string | undefined = className; c !== undefined; c = classes.get(c)?.base)
if (threadSlotKeys.has(c)) return true;
return false;
};
if (e.args.length !== arity) err(`${e.name} takes ${arity} arguments`, e.loc);
else if (name!.kind !== "strLit" || !isSlots(slots!.type))
err(`${e.name} takes a name literal and a %ThreadSlots value`, e.loc);
else if (key!.type.kind !== "object" || !keyed(key!.type.className))
err(`${e.name} key ${typeKey(key!.type)} is not a ThreadSlots key class`, e.loc);
else if (e.name === "slots.set") {
const kind = value!.type.kind;
if (
kind !== "object" &&
kind !== "record" &&
kind !== "array" &&
kind !== "map" &&
kind !== "set"
)
err(`slots.set value ${typeKey(value!.type)} is not a ThreadSlots value type`, e.loc);
if (e.type.kind !== "void" && !isSlots(e.type))
err("slots.set has type void or %ThreadSlots", e.loc);
} else if (e.name === "slots.get") {
if (e.type.kind !== "union") err("slots.get has a V | undefined union type", e.loc);
} else if (e.type.kind !== "bool") err(`${e.name} has type bool`, e.loc);
return;
}
if (e.name === "threads.publish") {
if (e.args.length !== 1) err("threads.publish takes exactly one argument", e.loc);
for (const a of e.args) {
checkExpr(a);
if (!typeEquals(a.type, e.type))
err(`threads.publish result ${typeKey(e.type)} differs from its argument`, e.loc);
}
return;
}
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);
return;
}
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);
}
return;
}
if (e.name === "promise.allSettled" || e.name === "promise.any") {
if (e.args.length !== 1) err(`${e.name} 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 ${e.name} (needs an array of promises)`, a.loc);
} else if (e.name === "promise.any" && !typeEquals(a.type.elem, e.type)) {
err("promise.any result must be the entries' promise type", e.loc);
}
}
if (e.type.kind !== "promise") err(`${e.name} must be promise-typed`, e.loc);
else if (
e.name === "promise.allSettled" &&
(e.type.inner.kind !== "array" || e.type.inner.elem.kind !== "union")
) {
err("promise.allSettled result must be a promise of a settled-record union array", e.loc);
}
return;
}
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);
return;
}
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);
}
return;
}
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);
return;
}
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);
}
function checkLibCall(e: IrExpr & { kind: "libCall" }): void {
if (
e.fn === "atomics.op" &&
(e.args[0]?.type.kind !== "bytes" ||
!["u8", "i8", "u16", "i16", "u32", "i32"].includes(e.args[0].type.elem))
) {
errors.push({ message: "integer Atomics require an integer typed array", loc: e.loc });
}
if (e.fn === "worker.new" && !workers)
errors.push({ message: "Worker construction requires worker execution mode", loc: e.loc });
const sig = LIB_FN_SIGS[e.fn];
if (!sig) {
err(`libCall of unknown library function "${e.fn as string}"`, e.loc);
return;
}
// 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 === "emitter.emitAny" ||
e.fn === "emitter.emitMixed" ||
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 === "stream.compose" ||
e.fn === "stream.pipelineStages" ||
e.fn === "stream.pipelineStagesDyn" ||
e.fn === "sp.pipelineStages" ||
e.fn === "sp.pipeline" ||
e.fn === "sp.pipelineOpts" ||
e.fn === "sp.pipelineOptsSignal" ||
e.fn === "writable.write" ||
e.fn === "writable.writeStr" ||
e.fn === "writable.writeU" ||
e.fn === "writable.writeEnc" ||
e.fn === "writable.end" ||
e.fn === "readable.unpipe" ||
e.fn === "rl.create" ||
e.fn === "rl.createSignal";
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 (checkLibValueCall(e)) return;
if (checkLibNetworkCall(e)) return;
if (checkLibFileCall(e)) return;
if (checkLibProcessCall(e)) return;
if (checkLibSpecialCall(e)) return;
if (checkLibStreamCall(e, sig.argTypes.length, sig.result)) return;
if (checkLibEmitterCall(e, sig.result)) return;
if (
e.fn === "error.stack" ||
e.fn === "error.setStack" ||
e.fn === "error.cause" ||
e.fn === "error.hasCause" ||
e.fn === "error.setCause" ||
e.fn === "error.setCode" ||
e.fn === "error.setCodeDyn" ||
e.fn === "error.assign" ||
e.fn === "error.hasKey" ||
e.fn === "error.hasCode" ||
e.fn === "error.defineCause" ||
e.fn === "error.deleteCause"
) {
const recv = e.args[0];
let cls = recv?.type.kind === "object" ? classes.get(recv.type.className) : undefined;
while (cls?.base) cls = classes.get(cls.base);
if (cls?.name !== "%Error") err(`libCall ${e.fn} receiver must be an error object`, e.loc);
}
if (e.fn === "error.ctor" || e.fn === "error.ctorOptions" || 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);
}
}
function checkLibValueCall(e: IrExpr & { kind: "libCall" }): boolean {
const cryptoPromise = CRYPTO_PROMISE_VALUES[e.fn];
if (cryptoPromise) {
const inner = e.type.kind === "promise" ? e.type.inner : undefined;
const view = e.fn === "crypto.randomFillPromise" ? e.args[0]?.type : undefined;
const ok =
inner !== undefined &&
(inner.kind === "dyn" ||
(cryptoPromise === "buffer" && inner.kind === "bytes" && inner.elem === "u8") ||
(cryptoPromise === "view" && view !== undefined && typeEquals(inner, view)) ||
(cryptoPromise === "number" && inner.kind === "f64"));
if (!ok) err(`libCall ${e.fn} must be a promise of its ${cryptoPromise} value`, e.loc);
return true;
}
if (e.fn === "events.once" || e.fn === "events.onceSignal") {
const [recv, , boxer, errorBoxer] = e.args;
const arrayT = e.type.kind === "promise" ? e.type.inner : null;
const boxes = (b: IrExpr | undefined) =>
b?.type.kind === "func" && arrayT !== null && typeEquals(b.type.ret, arrayT);
if (
recv?.type.kind !== "object" ||
arrayT?.kind !== "array" ||
!boxes(boxer) ||
!boxes(errorBoxer)
) {
err(`libCall ${e.fn} must box its event tuples into the promised array`, e.loc);
}
return true;
}
if (
e.fn === "tp.setTimeoutValue" ||
e.fn === "tp.setImmediateValue" ||
e.fn === "tp.setTimeoutSignal" ||
e.fn === "tp.setImmediateSignal" ||
e.fn === "tp.schedulerWaitValue" ||
e.fn === "tp.schedulerWaitSignal"
) {
const value =
e.args[e.fn === "tp.setImmediateValue" || e.fn === "tp.setImmediateSignal" ? 0 : 1];
if (value?.type.kind !== "promise" || !typeEquals(e.type, value.type)) {
err(`libCall ${e.fn} must be the value's promise type`, e.loc);
}
return true;
}
if (
e.fn === "fileHandle.read" ||
e.fn === "fileHandle.writeBytes" ||
e.fn === "fileHandle.writeStr" ||
e.fn === "fileHandle.readv" ||
e.fn === "fileHandle.writev"
) {
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" || e.fn === "fileHandle.readv" ? "bytesRead" : "bytesWritten";
const payloadName =
e.fn === "fileHandle.readv" || e.fn === "fileHandle.writev" ? "buffers" : "buffer";
const payload = e.args[1]?.type;
const count = shape ? recordValidation.get(shape.id)?.fields.get(countName) : undefined;
const buffer = shape ? recordValidation.get(shape.id)?.fields.get(payloadName) : undefined;
const ok =
shape !== undefined &&
!shape.tuple &&
shape.indexValue === undefined &&
shape.fields.length === 2 &&
count?.kind === "f64" &&
payload !== undefined &&
buffer !== undefined &&
typeEquals(buffer, payload);
if (!ok) {
err(
`libCall ${e.fn} must return a promise of { ${countName}: number, ${payloadName} }`,
e.loc,
);
}
return true;
}
if (e.fn === "dyn.objEntryPairs") {
// Result: the native [string, any][] the backend assembles from the
// runtime's owned pair snapshot.
const shape =
e.type.kind === "array" && e.type.elem.kind === "record"
? records.get(e.type.elem.shapeId)
: undefined;
const field = (name: string): IrType | undefined =>
shape?.fields.find((f) => f.name === name)?.type;
const ok =
shape !== undefined &&
shape.tuple === true &&
shape.indexValue === undefined &&
shape.fields.length === 2 &&
field("0")?.kind === "string" &&
field("1")?.kind === "dyn";
if (!ok) err(`libCall dyn.objEntryPairs must return a [string, any] tuple array`, e.loc);
return true;
}
if (e.fn === "array.namedKeys" || e.fn === "array.hasNamedKey") {
if (e.args[0]?.type.kind !== "array") {
err(`libCall ${e.fn} arg 0: expected an array, got ${e.args[0]?.type.kind}`, e.loc);
}
return true;
}
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);
}
return true;
}
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);
}
return true;
}
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);
}
return true;
}
if (e.fn === "string.fromCharCode" || e.fn === "string.fromCodePoint") {
// 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 ${e.fn} arg 0: expected number[] or bytes, got ${t?.kind}`, e.loc);
}
return true;
}
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);
}
return true;
}
if (e.fn === "process.columns" || e.fn === "process.rows") {
// 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 ${e.fn} must return the 'number | undefined' union`, e.loc);
}
return true;
}
return false;
}
function checkLibNetworkCall(e: IrExpr & { kind: "libCall" }): boolean {
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);
}
return true;
}
if (e.fn === "http.serverOnRequest" || e.fn === "http.serverOnCheck") {
// 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);
}
return true;
}
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);
}
return true;
}
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);
}
return true;
}
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);
}
return true;
}
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);
}
return true;
}
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);
}
return true;
}
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);
}
return true;
}
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" ||
e.fn === "http.requestDynCb" ||
e.fn === "https.requestDynCb"
) {
// 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.requestDynCb" || e.fn === "https.requestDynCb"
? 2
: 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);
}
return true;
}
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);
}
return true;
}
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);
}
return true;
}
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);
}
return true;
}
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);
}
return true;
}
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);
}
return true;
}
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);
return true;
}
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);
}
return true;
}
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);
}
return true;
}
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);
return true;
}
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);
return true;
}
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);
}
return true;
}
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);
return true;
}
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);
}
return true;
}
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);
}
return true;
}
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);
}
return true;
}
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);
}
return true;
}
return false;
}
function checkLibFileCall(e: IrExpr & { kind: "libCall" }): boolean {
if (e.fn === "fs.statProbeSync" || e.fn === "fs.lstatProbeSync") {
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 === "stats") &&
def.arms.some((a) => a.kind === "undefinedT");
if (!ok) {
err(`libCall ${e.fn} must return the 'Stats | undefined' union`, e.loc);
}
return true;
}
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" ||
e.fn === "zlib.cbOpts" ||
e.fn === "zlib.cbOptsDyn"
) {
const cbT = e.args[e.fn.startsWith("zlib.cbOpts") ? 3 : 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);
return true;
}
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);
}
return true;
}
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);
}
return true;
}
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);
}
return true;
}
if (
e.fn === "fs.readdirTypesSync" ||
e.fn === "fsp.readdirTypes" ||
e.fn === "fs.globTypesSync" ||
e.fn === "fsp.globTypes"
) {
// Result: the interned Dirent record array — {%dtype: f64,
// name: string, parentPath: string} rows (canonical field
// order; the structure lowerFsReaddirTypesCall pinned). The glob
// forms answer the array itself.
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 && isDirentShape(shape);
if (!ok) {
err(
`libCall ${e.fn} must return ${e.fn === "fsp.readdirTypes" ? "a promise of " : ""}the Dirent record array`,
e.loc,
);
}
return true;
}
if (e.fn === "dir.readSync" || e.fn === "dir.read") {
// Result: `Dirent | null` (a promise of it for dir.read) — the
// interned Dirent row shape readdirTypesSync uses, or null.
const result =
e.fn === "dir.read" ? (e.type.kind === "promise" ? e.type.inner : undefined) : e.type;
const def = result?.kind === "union" ? unions.get(result.unionId) : undefined;
const row = def?.arms.find((a) => a.kind === "record");
const shape = row?.kind === "record" ? records.get(row.shapeId) : undefined;
const ok =
def !== undefined &&
def.arms.length === 2 &&
def.arms.some((a) => a.kind === "nullT") &&
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 === "dir.read" ? "a promise of " : ""}Dirent | null`,
e.loc,
);
}
return true;
}
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,
);
}
return true;
}
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);
}
return true;
}
return false;
}
function checkLibProcessCall(e: IrExpr & { kind: "libCall" }): boolean {
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.onSpawn") {
const cb = e.args[1];
if (
!cb ||
cb.type.kind !== "func" ||
cb.type.params.length !== 0 ||
cb.type.ret.kind !== "void"
)
err("child.onSpawn callback must be () => void", 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);
}
return true;
}
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);
return true;
}
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") || p.kind === "string";
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);
}
return true;
}
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);
}
return true;
}
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);
}
return true;
}
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);
}
return true;
}
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);
}
return true;
}
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);
}
return true;
}
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);
}
return true;
}
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,
);
}
return true;
}
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);
}
return true;
}
if (e.fn === "stream.dynNative" || e.fn === "stream.dynNativePipe") {
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const side = e.args[1];
const className =
side?.kind === "numLit" ? (side.value === 1 ? "%Readable" : "%Writable") : null;
const ok =
def &&
className !== null &&
e.args[0]?.type.kind === "dyn" &&
def.arms.length === 2 &&
def.arms.some((a) => a.kind === "object" && a.className === className) &&
def.arms.some((a) => a.kind === "nullT");
if (!ok) err(`libCall ${e.fn} must return its side's stream class or null`, e.loc);
return true;
}
if (e.fn === "child.stdioStream") {
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const slot = e.args[1];
const className =
slot?.kind === "numLit" ? (slot.value === 0 ? "%Writable" : "%Readable") : null;
const ok =
def &&
className !== null &&
def.arms.length === 2 &&
def.arms.some((a) => a.kind === "object" && a.className === className) &&
def.arms.some((a) => a.kind === "nullT");
if (!ok) err(`libCall ${e.fn} must return the stream class of its stdio slot or null`, e.loc);
return true;
}
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,
);
}
return true;
}
return false;
}
function checkLibSpecialCall(e: IrExpr & { kind: "libCall" }): boolean {
if (
e.fn === "crypto.randomFillView" ||
e.fn === "crypto.randomFillViewRest" ||
e.fn === "crypto.getRandomValues"
) {
// The filled view is the result, at its own element type.
const view = e.args[0]?.type;
if (view?.kind !== "bytes" || !typeEquals(e.type, view)) {
err(`libCall ${e.fn} must return its view argument's type`, e.loc);
}
return true;
}
if (e.fn === "crypto.hashStream" || e.fn === "crypto.hashStreamPiped") {
const value = e.args[0]?.type.kind;
if (value !== "cryptoHash" && value !== "cryptoHmac")
err(`libCall ${e.fn} argument must be a Hash or Hmac`, e.loc);
return true;
}
if (e.fn === "crypto.timingSafeEqual") {
if (e.args[0]?.type.kind !== "bytes" || e.args[1]?.type.kind !== "bytes")
err(`libCall ${e.fn} arguments must be views`, e.loc);
}
if (e.fn === "crypto.randomFillCb") {
if (e.args[0]?.type.kind !== "bytes") err(`libCall ${e.fn} buffer must be a view`, e.loc);
if (e.args[4]?.type.kind !== "func")
err(`libCall ${e.fn} callback must be a function`, e.loc);
}
if (
e.fn === "crypto.randomFillCbDyn" ||
e.fn === "crypto.randomIntCb" ||
e.fn === "crypto.generateKeyCb"
) {
const callback = e.args[e.args.length - 1]?.type;
if (callback?.kind !== "func") err(`libCall ${e.fn} callback must be a function`, e.loc);
}
if (e.fn === "global.undefRead") {
// Always throws — the result type is whatever the declared type
// of the undefined global mapped to (never materialized).
return true;
}
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.
return true;
}
if (e.fn === "error.new" || e.fn === "error.newOptions") {
// Which builtin the runtime constructs is named by the result type.
if (!isBuiltinErrorObject(e.type)) {
err(`libCall ${e.fn} must return a builtin error class, got ${e.type.kind}`, e.loc);
}
return true;
}
if (e.fn === "error.aggregateSetErrors" || e.fn === "error.aggregateErrors") {
// Receiver: exactly %AggregateError (subclassing is fenced — the
// hidden runtime slot admits no other layout).
const recv = e.args[0];
if (!recv || recv.type.kind !== "object" || recv.type.className !== "%AggregateError") {
err(`libCall ${e.fn} receiver must be %AggregateError`, e.loc);
}
return true;
}
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);
}
return true;
}
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);
}
return true;
}
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);
}
return true;
}
if (
e.fn === "assert.refEqBytes" ||
e.fn === "assert.bytesDeepEq" ||
e.fn === "assert.looseBytesEq"
) {
// 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);
}
return true;
}
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);
}
return true;
}
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);
return true;
}
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);
}
return true;
}
return false;
}
function checkLibStreamCall(
e: IrExpr & { kind: "libCall" },
fixedArgs: number,
result: IrType,
): boolean {
if (e.fn.startsWith("zlib.stream")) {
// The zlib stream classes: streamNew answers a runtime Transform;
// the members take one as their receiver, and the Cb forms end in
// the () => void callback.
const isTransform = (t: IrType | undefined): boolean =>
t?.kind === "object" && t.className === "%Transform";
if (e.fn === "zlib.streamNew") {
if (!isTransform(e.type)) err(`libCall ${e.fn} must return a %Transform`, e.loc);
return true;
}
if (!isTransform(e.args[0]?.type)) {
err(`libCall ${e.fn} receiver must be a %Transform`, e.loc);
}
if (e.fn.endsWith("Cb") && e.args[e.args.length - 1]?.type.kind !== "func") {
err(`libCall ${e.fn} callback must be a func`, e.loc);
}
if (!typeEquals(e.type, result)) err(`libCall ${e.fn} result type mismatch`, e.loc);
return true;
}
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.objectMode" ||
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" ||
e.fn === "stream.compose" ||
e.fn === "stream.pipelineStages" ||
e.fn === "stream.pipelineStagesDyn" ||
e.fn === "sp.pipelineStages" ||
e.fn === "stream.addAbortSignal" ||
e.fn === "stream.addAbortSignalFn" ||
e.fn === "stream.finishedOpts" ||
e.fn === "stream.finishedOptsSignal"
) {
// 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 === "stream.addAbortSignal" || e.fn === "stream.addAbortSignalFn") {
// [stream, signal] (the option) or [signal, stream] (the module
// function): the result is the stream argument's type.
const s = e.args[e.fn === "stream.addAbortSignal" ? 0 : 1];
if (!isStreamObject(s?.type) || !typeEquals(e.type, s!.type)) {
err(`libCall ${e.fn} must take and return a stream-hierarchy object`, e.loc);
}
return true;
}
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 = fixedArgs; 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);
}
}
return true;
}
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);
}
return true;
}
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 = fixedArgs; 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);
return true;
}
if (e.fn === "stream.finishedOpts" || e.fn === "stream.finishedOptsSignal") {
// (recv, readable, writable, error[, signal], cb) → the cleanup
// closure; the callback is a func or a checked-dynamic value.
if (!isStreamObject(e.args[0]?.type)) {
err(`libCall ${e.fn} receiver must be a stream-hierarchy object`, e.loc);
}
const cbK = e.args[e.args.length - 1]?.type.kind;
if (cbK !== "func" && cbK !== "dyn")
err(`libCall ${e.fn} callback must be a func or dyn`, e.loc);
if (e.type.kind !== "func") {
err(`libCall ${e.fn} must return the cleanup closure, got ${e.type.kind}`, e.loc);
}
return true;
}
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);
}
return true;
}
if (e.fn === "stream.compose") {
// (count, s1..sn) → a Duplex (n >= 2), or the lone stream.
const count = e.args[0];
const n = count?.kind === "numLit" ? count.value : -1;
if (n < 1 || e.args.length !== n + 1) {
err(`libCall ${e.fn} count/arity mismatch`, e.loc);
return true;
}
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);
}
}
if (!isStreamObject(e.type)) err(`libCall ${e.fn} must return a stream`, e.loc);
return true;
}
if (
e.fn === "stream.pipelineStages" ||
e.fn === "stream.pipelineStagesDyn" ||
e.fn === "sp.pipelineStages"
) {
// (n, k, positions, flags, mask, s1..sn, stage1..stagek,
// helpers..., box?, (signal, abort)?[, cb]): positions bit j marks
// stage j a function, flags bits 1-2 the destination, bit 5 the
// signal group; helpers are three per function run a stream
// follows.
const [n, k, positions, flags] = e.args
.slice(0, 4)
.map((a) => (a?.kind === "numLit" ? a.value : -1));
const total = n! + k!;
const destKind = (flags! >> 1) & 3;
let functions = 0;
for (let j = 0; j < total && j < 53; j++) {
if (Math.floor(positions! / 2 ** j) % 2 === 1) functions++;
}
const lastIsFunction = Math.floor(positions! / 2 ** (total - 1)) % 2 === 1;
const helperCount = 3 * pipelinePumpedRuns(total, positions!);
const groups = helperCount + (destKind === 2 ? 1 : 0) + (flags! & 32 ? 2 : 0);
const cbArgs = e.fn === "sp.pipelineStages" ? 0 : 1;
if (
flags! < 0 ||
n! < 0 ||
k! < 1 ||
positions! < 1 ||
total > 52 ||
positions! >= 2 ** total ||
functions !== k ||
(destKind === 2) !== lastIsFunction ||
total < 2 ||
e.args.length !== 5 + n! + k! + groups + cbArgs
) {
err(`libCall ${e.fn} count/arity mismatch`, e.loc);
return true;
}
for (let i = 5; i < 5 + n!; i++) {
if (!isStreamObject(e.args[i]?.type)) {
err(`libCall ${e.fn} stage ${i - 5} must be a stream-hierarchy object`, e.loc);
}
}
let at2 = 5 + n!;
for (let i = at2; i < at2 + k!; i++) {
if (e.args[i]?.type.kind !== "func")
err(`libCall ${e.fn} stage function ${i} must be a func`, e.loc);
}
at2 += k!;
const helpers = helperCount + (destKind === 2 ? 1 : 0);
for (let i = at2; i < at2 + helpers; i++) {
if (e.args[i]?.type.kind !== "func")
err(`libCall ${e.fn} helper ${i} must be a func`, e.loc);
}
at2 += helpers;
if (flags! & 32) {
if (e.args[at2]?.type.kind !== "dyn" || e.args[at2 + 1]?.type.kind !== "func")
err(`libCall ${e.fn} signal and abort must be a dyn and a func`, e.loc);
}
if (cbArgs) {
const cbK = e.args[e.args.length - 1]?.type.kind;
if (e.fn === "stream.pipelineStages" ? cbK !== "func" : cbK !== "dyn") {
err(`libCall ${e.fn} callback has the wrong kind`, e.loc);
}
if (!isStreamObject(e.type)) err(`libCall ${e.fn} must return a stream`, e.loc);
} else if (e.type.kind !== "promise") {
err(`libCall ${e.fn} must return a promise`, e.loc);
}
return true;
}
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);
return true;
}
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);
}
return true;
}
if (
e.fn === "readable.opMap" ||
e.fn === "readable.opFilter" ||
e.fn === "readable.opFlatMap" ||
e.fn === "readable.opDrop" ||
e.fn === "readable.opTake"
) {
if (!isStreamObject(e.args[0]?.type) || !isStreamObject(e.type)) {
err(`libCall ${e.fn} must take a stream and return a stream class`, e.loc);
}
return true;
}
if (e.fn === "readable.opReduce") {
const inner = e.type.kind === "promise" ? e.type.inner : null;
const kind = e.args[5]?.kind === "numLit" ? e.args[5].value : -1;
const want = [DYN, F64, BOOL, STRING][kind];
if (!isStreamObject(e.args[0]?.type) || !inner || !want || !typeEquals(inner, want)) {
err(`libCall readable.opReduce must return the promise its kind names`, e.loc);
}
return true;
}
if (e.fn === "readable.fromDone") {
if (e.args.length !== 0 || e.type.kind !== "dyn")
err(`libCall ${e.fn} takes no arguments`, e.loc);
return true;
}
if (e.fn === "readable.fromGen" || e.fn === "readable.fromAsyncGen") {
const g = e.args[0]?.type;
const helpers = e.fn === "readable.fromGen" ? 3 : 4;
if (
g?.kind !== "generator" ||
(g.async === true) !== (e.fn === "readable.fromAsyncGen") ||
!e.args.slice(1, 1 + helpers).every((a) => a.kind === "closure") ||
e.args[1 + helpers]?.kind !== "numLit" ||
!isStreamObject(e.type)
) {
err(`libCall ${e.fn} must take a generator, its helpers and the flags`, e.loc);
}
return true;
}
if (e.fn === "readable.fromIter" || e.fn === "readable.fromAsyncIter") {
if (
e.args[0]?.type.kind !== "dyn" ||
e.args[1]?.kind !== "numLit" ||
!isStreamObject(e.type)
) {
err(`libCall ${e.fn} must take an iterator object and the flags`, e.loc);
}
return true;
}
if (e.fn === "readable.fromArr" || e.fn === "readable.fromDyn") {
// No receiver: the source leads; the result is the class.
const src = e.fn === "readable.fromArr" ? "array" : "dyn";
if (e.args[0]?.type.kind !== src || !isStreamObject(e.type)) {
err(`libCall ${e.fn} must take a ${src} source and return a stream class`, e.loc);
}
if (e.fn === "readable.fromArr" && e.args[1]?.kind !== "numLit") {
err(`libCall readable.fromArr flags must be a number literal`, e.loc);
}
return true;
}
if (!isStreamObject(e.args[0]?.type)) {
err(`libCall ${e.fn} receiver must be a stream-hierarchy object`, e.loc);
return true;
}
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);
}
return true;
}
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.objectMode" ||
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);
}
return true;
}
if (e.fn === "readable.pipe" || e.fn === "readable.pipeGeneral") {
if (!isStreamObject(e.args[1]?.type) || !typeEquals(e.type, e.args[1]!.type)) {
err(`libCall ${e.fn} must return its destination's type`, e.loc);
}
return true;
}
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);
return true;
}
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);
}
return true;
}
if (!typeEquals(e.type, result)) {
err(`libCall ${e.fn} must be ${result.kind}, got ${e.type.kind}`, e.loc);
}
return true;
}
return false;
}
function checkLibEmitterCall(e: IrExpr & { kind: "libCall" }, result: IrType): boolean {
if (
(e.fn.startsWith("emitter.") || e.fn === "stream.onDyn") &&
e.fn !== "emitter.setDefaultMax" &&
e.fn !== "emitter.setDefaultMaxChk" &&
e.fn !== "emitter.getDefaultMax" &&
e.fn !== "emitter.esmDefaultMax" &&
e.fn !== "emitter.esmSync" &&
e.fn !== "emitter.errorMonitor" &&
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);
}
return true;
}
if (!isEmitterObject(e.args[0]?.type)) {
err(`libCall ${e.fn} receiver must be an emitter-hierarchy object`, e.loc);
return true;
}
// 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.onAny" ||
e.fn === "stream.onDyn" ||
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);
}
return true;
}
if (e.fn === "emitter.emitData") {
const chunkT = e.args[2]?.type;
const ok =
chunkT !== undefined &&
((chunkT.kind === "bytes" && chunkT.elem === "u8") ||
chunkT.kind === "string" ||
chunkT.kind === "dyn");
if (!ok) err(`libCall emitter.emitData chunk must be bytes<u8>, string or dyn`, e.loc);
if (e.type.kind !== "bool") err(`libCall emitter.emitData must be bool`, e.loc);
return true;
}
if (e.fn === "emitter.countFn" && e.args[2]?.type.kind !== "func") {
err(`libCall emitter.countFn listener must be a func`, e.loc);
return true;
}
if (e.fn === "emitter.emitError" && e.args[2]?.type.kind !== "object") {
err(`libCall emitter.emitError payload must be an error-hierarchy object`, e.loc);
return true;
}
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);
return true;
}
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);
return true;
}
if (e.fn === "emitter.emitFlex" || e.fn === "emitter.emitAny") {
if (e.args.slice(2).some((arg) => arg.type.kind !== "dyn")) {
err(`libCall ${e.fn} payloads must be checked-dynamic`, e.loc);
}
}
if (e.fn === "emitter.emitMixed") {
const count = e.args[2];
const k = count?.kind === "numLit" ? count.value : -1;
if (!Number.isInteger(k) || k < 0 || e.args.length < 3 + k) {
err("libCall emitter.emitMixed needs a literal argument count", e.loc);
} else if (e.args.slice(3, 3 + k).some((arg) => arg.type.kind !== "dyn")) {
err("libCall emitter.emitMixed untyped arguments must be checked-dynamic", e.loc);
}
}
if (
e.fn === "emitter.emit" ||
e.fn === "emitter.emitFlex" ||
e.fn === "emitter.emitAny" ||
e.fn === "emitter.emitMixed" ||
e.fn === "emitter.hasTyped" ||
e.fn === "emitter.listenersDyn" ||
e.fn === "emitter.count" ||
e.fn === "emitter.getMax" ||
e.fn === "emitter.ctor" ||
e.fn === "emitter.countFn" ||
e.fn === "emitter.countDyn" ||
e.fn === "emitter.emitError"
) {
if (!typeEquals(e.type, result)) {
err(`libCall ${e.fn} must be ${result.kind}, got ${e.type.kind}`, e.loc);
}
return true;
}
}
return false;
}
function checkConversionExpr(e: IrExpr & { kind: "jsonStringify" | "dynCheck" }): void {
switch (e.kind) {
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 (
!canDynCheckTo(
e.type,
(id) => records.get(id),
(id) => unions.get(id),
) &&
!undefArmedOk &&
!bytesOk &&
!errorOk &&
!classOk &&
!funcOk &&
!handleOk
) {
err(`dynCheck against non-JSON-representable type ${e.type.kind}`, e.loc);
}
break;
}
default: {
const _exhaustive: never = e;
void _exhaustive;
}
}
}
// The yield a `delegate` genResume is checking as its argument.
let delegatedYield: IrExpr | null = null;
function checkAsyncExpr(
e: IrExpr & {
kind:
| "awaitExpr"
| "yieldExpr"
| "genResume"
| "awaitUnionExpr"
| "newPromise"
| "promiseWithResolvers";
},
): void {
switch (e.kind) {
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);
if (e.adopt === true && fn.generator !== undefined)
err("returned-promise adoption inside a generator", 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 (e.delegated) {
if (delegatedYield !== e) err("delegated yield outside a delegate resume", e.loc);
if (e.type.kind !== "void" || e.captureCompletion) err("invalid delegated yield", e.loc);
} else if (e.captureCompletion) {
if (
fn.async ||
e.type.kind !== "dyn" ||
!typeEquals(e.captureCompletion.returnType, fn.returnType)
) {
err("invalid captured generator completion", e.loc);
}
} else 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.mode === "delegate") {
if (e.arg !== null) {
if (e.arg.kind === "yieldExpr" && e.arg.delegated) delegatedYield = e.arg;
else err("genResume delegate argument is not a delegated yield", e.loc);
checkExpr(e.arg);
delegatedYield = null;
}
const outer = fn.generator;
if (outer === undefined || fn.async || genT.async)
err("genResume delegate outside a sync generator body", e.loc);
else if (
genT.nextT.kind !== "undefinedT" &&
genT.nextT.kind !== "dyn" &&
!typeEquals(genT.nextT, outer.nextT)
)
err(
`genResume delegate into a ${typeKey(genT.nextT)} next-channel from ${typeKey(outer.nextT)}`,
e.loc,
);
if (e.type.kind !== "bool" && e.type.kind !== "record")
err(`genResume delegate result ${typeKey(e.type)}`, e.loc);
break;
}
if (e.arg !== null) checkExpr(e.arg);
if (e.mode === "step" || e.mode === "value" || e.mode === "close") {
if (genT.async) err(`genResume ${e.mode} on an async generator`, e.loc);
if (e.arg !== null) err(`genResume ${e.mode} with an argument`, e.loc);
if (e.mode === "step") {
if (genT.nextT.kind !== "undefinedT" && genT.nextT.kind !== "dyn")
err(`genResume step on a ${typeKey(genT.nextT)} next-channel`, e.loc);
if (e.type.kind !== "bool") err(`genResume step result ${typeKey(e.type)}`, e.loc);
} else if (e.mode === "close") {
if (e.type.kind !== "void") err(`genResume close result ${typeKey(e.type)}`, e.loc);
} else if (
(genT.yieldT.kind === "void" || !typeEquals(e.type, genT.yieldT)) &&
(genT.retT.kind === "void" || !typeEquals(e.type, genT.retT))
) {
err(
`genResume value ${typeKey(e.type)} != yield channel ${typeKey(genT.yieldT)}`,
e.loc,
);
}
break;
}
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 === "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 ? recordValidation.get(rec.id)?.fields.get("done") : undefined;
const valueF = rec ? recordValidation.get(rec.id)?.fields.get("value") : undefined;
if (!rec || rec.fields.length !== 2 || doneF?.kind !== "bool" || valueF === undefined) {
err(`genResume result record ${resultT.shapeId} is not { done: bool, value: V }`, e.loc);
break;
}
if (valueF.kind === "dyn") break;
const vdef = valueF.kind === "union" ? unions.get(valueF.unionId) : undefined;
if (!vdef || !vdef.arms.some((a) => a.kind === "undefinedT")) {
err(
`genResume value slot ${typeKey(valueF)} 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);
}
// A Promise<never> executor types resolve as `(value: never) =>
// void` (one parameter over a void inner): the void resolve never
// reads an argument, and a never value cannot be produced.
if (!expectsArg && rp.params.length > 1) {
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 = recordValidation.get(e.type.shapeId)?.initializationFields;
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;
}
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 (s.initializes && !binding?.tdz && globals.get(s.localId)?.initFlag === undefined) {
err(`initializing assign requires a TDZ binding "${s.localId}"`, s.loc);
}
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) && !locals.get(s.localId)?.tdz) {
err(`assign to immutable local "${binding.name}"`, s.loc);
}
if (binding?.type.kind === "caught") {
// A caught-typed GLOBAL is a module's recorded evaluation error:
// its only write stores the module's own catch binding.
const caughtLocal = s.value.kind === "varRef" ? locals.get(s.value.localId) : undefined;
if (!globals.has(s.localId) || caughtLocal?.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 fieldType = classValidation.fields.get(s.className)?.get(s.field);
if (!cls) err(`fieldSet on undeclared class "${s.className}"`, s.loc);
else if (!fieldType) 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, fieldType, `fieldSet ${s.className}.${s.field}`);
}
break;
}
case "recordSet": {
checkExpr(s.obj);
checkSlotValue(s.value);
const shape = records.get(s.shapeId);
const field = recordValidation.get(s.shapeId)?.fields.get(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, `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) ?? globals.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);
}
}