Files
scriptc/packages/compiler/src/frontend/lowering/lowerer.ts
T
Chris Tate 22e8565d54 feat(compiler): statically compile the TypeScript session lifecycle
- Replace SDK snapshots, projects, and programs with the owned native client.
- Preserve source identity across snapshots with native paths, caches, metadata, and diagnostics.
- Collect request timing and close snapshot state reliably without SDK helpers.
2026-09-28 07:45:35 -05:00

10756 lines
503 KiB
TypeScript

import { InternalCompilerError } from "../../errors.js";
/* AST + checker → IR.
*
* Invariants:
* - Runs only on programs that passed preflight (tsc-clean), so the lowerer
* may assume the checker's guarantees (no undeclared identifiers, no
* ill-typed operators) and every remaining rejection is a *scriptc*
* limitation with its own SC1xxx/SC2xxx code.
* - Collects ALL diagnostics instead of stopping at the first: an
* unsupported construct poisons its enclosing statement (PoisonError),
* the statement is skipped, and lowering continues. The user sees every
* blocker at once — this list is the seed of the coverage report.
* - Lexical scoping is resolved here: locals get function-unique ids
* ("x.0", "x.1" for shadowing); the IR is scope-flat.
*/
import { resolve } from "node:path";
import { bindingSource } from "../binding-source.js";
import { tsgoPath } from "../dts-paths.js";
import * as ts from "../ts7/adapter.js";
import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js";
import {
anyOpRequiresDynamicDiag,
blockedBindingUseDiag,
checkerPanicDiag,
componentTypeDiag,
isCheckerPanic,
genericSignatureTypeDiag,
indexSignatureTypeDiag,
intersectionTypeDiag,
noLoweringDiag,
overloadedSignatureTypeDiag,
recordShapeMismatchDiag,
requiresDynamicApiDiag,
requiresDynamicPackageDiag,
requiresDynamicTypeDiag,
unionMismatchDiag,
UNSUPPORTED,
unsupportedDiag,
unsupportedTypeDiag,
} from "../../diagnostics/diagnostic.js";
import type {
IrClassDef,
IrExpr,
IrFfiImport,
IrFunction,
IrGlobal,
IrLocal,
IrModule,
IrParam,
IrRecordShape,
IrStmt,
IrType,
IrUnionDef,
SrcLoc,
} from "../../ir/ir.js";
import { arrayOf, BOOL, canAdaptDynFuncTo, canDynCheckTo, canConvertToDyn, canCrossIslandBoundary, canExitIslandToType, canMarshalTypedFuncIntoIsland, DYN, DYN_HANDLE_KINDS, F64, isDynTypedRefType, isJsonSafeType, isJsonStringifySafeType, isUndefinedArmedUnion, isUnitType, JSVAL, NULL_T, RUNTIME_ERROR_CLASSES, STRING, typeEquals, UNDEFINED_T, VOID } from "../../ir/ir.js";
import { type DynamicImportResolution, type NpmBuiltinUse, type NpmLazyTrap } from "../npm.js";
import { provenanceActive } from "../provenance-registry.js";
import {
ambientDtsPath,
canonicalBuiltinModule,
type StartupCrash,
cjsExportAssignmentOf,
cjsExportDiscardReason,
fallbackDtsPath,
isCjsExportTableLiteral,
isCjsJsFile,
isJsSourceFile,
isNodeEsmFile,
isNodeTypesPath,
locOf,
orderedImportsOf,
overridesDtsPath,
npmStaticDepSf7,
requireSpecOf,
resolveImport,
workspacePackageOfPath,
} from "../program.js";
import {
containsRecord,
containsUnion,
describeComponentBlocker,
describeRecordMemberBlocker,
formatIrType,
ISLAND_AMBIENT_TYPES,
isUnitOnlyTsType,
jsOpenObjectType,
mapType,
ShapeRegistry,
type DeclaredOrderPriorityRef,
typeKey,
type TypeMapperCtx,
UnionRegistry,
withUndefinedArm as withUndefinedArmCanonical,
} from "../type-mapper.js";
import { CompoundOp, IslandFnEntry, boundaryIntoIslandMsg, boundaryOutOfIslandMsg, BuiltinModuleFn, builtinConstLit, builtinModuleConstOf, builtinModulesArrayLit, builtinFenceHintOf, builtinModuleFnOf, stdlibMemberFence, isStdlibMember, isStdlibSymbol, isStdlibGlobal, stdlibGlobalMember, nodeTypesOnlySymbol } from "./surfaces.js";
import { FileParts, splitFiles, collectProgram, collectNpmImports, collectJsonImports, moduleArtifacts, collectGlobals, declSymbolOf, defaultExportSymbolOf, lowerFileInit, lowerDefaultExport, buildMain, appendDynamicImportModules, appendForkModules } from "./lower-modules.js";
import { prepareCjsModuleGraph } from "./lower-node-module.js";
import { ClassInfo, ClassIteratorInfo, GenericClassInfo, registerBuiltinErrorClasses, registerBuiltinEmitterClass, registerBuiltinStreamClasses, builtinErrorInfoOf, builtinEmitterInfoOf, builtinStreamInfoOf, analyzeClassDecoration, classIteratorDrainCall, classIteratorNextCall, classIteratorOf, classIteratorOpenCall, classIteratorRestDrainCall, classMemberNameOf, classValueRef, collectClassShape, exactClassOfReceiver, collectClassShapeInner, ctorAbiEquals, findMethodOn, findStaticOn, findGenericMethodOn, findGenericStaticOn, genericClassInstanceType, isSubclassOf, inHierarchy, overrideBelow, staticShadowBelow, upcastTo, lowerClassMembers, lowerClassCtor, lowerClassExpression, lowerClassExpressionInfo, lowerClassMethodMember, lowerClassValueProperty, lowerStaticMethod, throwingSetterFn, fieldInitStmts, lowerStaticFieldInits, lowerStaticFieldRead, lowerDerivedCtorBody, superCallStmt, lowerSuperMethodCall, superThisRef, lowerSuperAccessorRead, lowerSuperAccessorWrite, inheritsBuiltinErrorCtor, inheritsBuiltinEmitterCtor, errorConstructorArgs, lowerNew, accessorCall } from "./lower-classes.js";
import { MixinFnShape, mixinCallClassInfoOf, mixinIntersectionInstanceType } from "./lower-mixins.js";
import { ParamShape, FnSig, GenericFnInfo, GenericInstance, bindingNeverReassigned, bodyReadsArguments, funcTypeFromParamShapes, implicitMonoFile, isThisParameter, paramShape, paramShapes, checkDefaultParamBodyType, completeArgs, wrappedUndefined, undefinedArgFor, requireExactArityValue, bodyReturnType, declaredReturnType, collectSignature, collectSignatureInner, collectGenericSignature, genericFnOf, lowerGenericCall, lowerGenericFnValue, inferTypeParamBindings, lowerGenericInstance, lowerCall, lowerFfiCall, lowerTimersMemberCall, lowerPromiseMethodCall, lowerFilterNarrowCall, isTopLevelFnSymbol, lowerNestedFunctionDecl, lambdaSignature, lowerLambda, lowerFunction, validateFfiImports } from "./lower-calls.js";
import { lowerArrayMethodCall, lowerMapMethodCall, lowerMapForEachCall, buildMapForEachFn, lowerRecordOvfCaptureHelper, lowerEnvToPairsHelper, lowerSetMethodCall, lowerSetForEachCall, buildSetForEachFn } from "./lower-containers.js";
import { lowerBufferStaticCall, lowerBytesMethodCall, lowerBytesNew } from "./containers/bytes.js";
import { lowerRegexMethodCall, lowerStringMethodCall } from "./containers/string-and-regexp.js";
import { lowerStreamModuleCall } from "./lower-stream.js";
import { lowerEmitOverrideSpec, type EmitSpecCtx, type EmitSpecRequest } from "./lower-event-emitter.js";
import { builtinImportOf, createRequireBindingDecl, createRequireNamespaceDecl, createRequireProgramModuleOf, createRequireSpecOf, stripTypeCasts, lowerBuiltinModuleCall, lowerNodeModuleCall, lowerTimersPromisesSetInterval, lowerFsToUnixTimestampCall, lowerFsLadderCall, lowerChildArgsArg, lowerSpawnSyncCall, lowerSpawnCall, lowerExecFileCall, lowerExecSyncCall, recordToEnvPairs, lowerJsonMethodCall, fencedBuiltinImportOf, lowerCryptoComposedCall, lowerUrlMethodCall, lowerSearchParamsMethodCall, lowerStatsMethodCall, lowerChildMethodCall, lowerAtomicsCall, lowerBuiltinExtraProperty, registerPromisifiedBuiltinDecl, lowerExecFileAsyncCall, execFileAsyncHelper, lowerStringDecoderMethodCall, strdecHelper, lowerReadlineMethodCall, lowerDcChannelMethodCall, lowerDcChannelProperty, lowerAlsMethodCall, lowerDcTracingChannelMethodCall, lowerDcTracingChannelProperty, lowerJsonProperty, lowerErrorCodeProperty, lowerProcessProperty, isProcessEnv, envValueType, lowerProcessEnvGet, lowerProcessMethodCall, lowerProcessOptionalMethodCall, lowerTimeoutMethodCall, envSnapshotHelper, isConsoleLog, consoleCallMember, lowerNumberStaticCall, lowerNumberStaticProperty, lowerDateCall, lowerTextCodecCall, lowerCryptoModuleCall, lowerFsConstantsProperty, lowerBuiltinConstantsProperty, builtinConstantBindingOf, builtinConstantsDestructureDecl, lowerProcessStreamProperty, lowerStringStaticCall, lowerStringLastIndexOfCall, lowerPromiseStaticCall } from "./lower-builtins.js";
import { fenceFetchObjectAssignment, fenceFetchObjectBinding, fenceStaticAbortControllerMemberRead, fenceStaticHeadersIteration, fenceStaticHeadersMember, fenceStaticReadableStreamMember, fenceStaticResponseMember, fenceUnsupportedFetchConstructorMember, isIslandExpr, islandFuncValueFence, islandRegexpOf, jsvalIn, requireDynamicApi, islandGlobalFnOf, lowerAbortControllerNew, lowerDynamicHeadersIteratorCall, lowerDynamicHeadersSpread, lowerDynamicImportCall, lowerFetchCall, lowerFetchElementMethodCall, lowerResponseNew, lowerStaticFetchCompanionCall, lowerStaticAbortControllerCall, lowerStaticAbortSignalListenerCall, lowerStaticReadableStreamCancelCall, lowerStaticReadableStreamControllerCall, lowerStaticReadableStreamNew, lowerStaticReadableStreamReaderCall, lowerStaticResponseCall, lowerIslandMethodCall, lowerMathProperty, npmPackageOf, npmMemberFence, npmPackageOfSymbol } from "./lower-island.js";
import { lowerHttpHeadersElement, lowerNetModuleCall, lowerServerMethodCall, lowerServerProperty, lowerTlsRootCertificates } from "./lower-server.js";
import { lowerDgramDnsModuleCall, lowerDgramMethodCall } from "./lower-dgram.js";
import { lowerNodeTestModuleCall, lowerTestDirectCall, lowerTestMethodCall, lowerTestCtxProperty } from "./lower-test.js";
import { lowerAssertModuleCall, lowerAssertDirectCall } from "./lower-assert.js";
import { lowerUtilModuleCall } from "./lower-inspect.js";
import { lowerComptime, comptimeBakeable, rejectComptimeCaptures, comptimeValueToIr } from "./lower-comptime.js";
import { lowerStmts, noteBlockedBindings, isBlockedBinding, lowerScopedBlock, predeclareForwardCapture, predeclareForwardFnDecl, predeclareForwardVar, lowerStmt, lowerVarStatement, lowerDestructuringDecl, lowerDestructuringAssignParts, lowerBindingPattern, lowerJsvalBindingPattern, checkBindingElement, bindPatternTarget, isParseArgsDynCheckerType, lowerVarDeclList, lowerVarDecl, lowerSwitch, lowerTry, lowerExprStatement, lowerForOf, lowerForStatement } from "./lower-stmts.js";
import { FieldTarget, lowerExpr, maybeNarrow, lowerUnitComparison, lowerNullishCoalesce, lowerCondition, ensureBool, requireTruthyUnion, eqComparableUnion, lowerIntrinsicProperty, lowerArrayLiteral, lowerElementAccess, lowerElementWrite, lowerRecordKeyRead, ensureString, lowerTemplate, lowerAsExpression, lowerPrefixUnary, lowerBinary, lowerCaughtTypeofTest, caughtRead, caughtLocalOf, caughtToString, lowerInstanceOf, lowerRegexLiteral, lowerFieldRead, lowerUnionProperty, fieldTarget, fieldGetExpr, fieldSetStmt, lowerFieldCompound, uniqueSymbolKeyOf } from "./lower-exprs.js";
import { finishOptionalChain, isOptionalChainTail, lowerOptionalChain } from "./expressions/optional-chains.js";
import { foldedStringKeyOf, lowerDynObjectLiteral, lowerObjectLiteral, lowerShorthandValue, rejectThisInObjectMethod } from "./expressions/object-literals.js";
import type { ExpandoMember } from "./lower-expando.js";
import { lowerRecordFieldCall, lowerObjectMethodCall } from "./lower-calls.js";
import { fenceCrossBlockNsRef, nsPathPrefix } from "./lower-namespaces.js";
import { numLit, varRef } from "../../ir/build.js";
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
/** Entry function name. '%' cannot appear in a TS identifier, so a user
* function can never collide with it (mangling is injective per prefix). */
export const ENTRY_NAME = "%main";
interface GenericDemandOwner {
priority?: readonly [phase: number, order: number];
functionDemands: GenericInstance[];
classDemands: ClassInfo[];
}
/** One step of the copy-reshape width relation (widthLiftPlan): how a
* source-typed value enters a destination slot. Pure data — the plan half;
* applyWidthLift is the build half. */
export type WidthLift =
| { how: "copy" }
| { how: "wrap"; tag: number }
| { how: "retag" }
| { how: "liftWrap"; tag: number; arm: IrType }
| { how: "width" }
| { how: "arr" }
| { how: "tupleArr" }
| { how: "emptyArr" }
| { how: "objWidth" }
| { how: "clsWidth" }
| { how: "narrow" }
| { how: "dynIn" }
| { how: "upcast" }
| { how: "funcAdapt" };
export class PoisonError extends Error {}
/** A function-shaped value that exists only in the compiler's static
* projection graph. Declared-function aliases point at the original
* immortal closure/direct-call target; adapters describe a call rewrite
* whose JavaScript function object does not otherwise need storage. */
export type StaticCallableProjection =
| { kind: "declared-function"; signature: FnSig }
| { kind: "builtin-function"; module: string; member: string }
| { kind: "promisified-exec-file" }
| { kind: "promisified-builtin"; module: string; member: string };
/** Own-property lookup for the surface tables. They are plain object
* literals, so a bare `table[name]` would also find Object.prototype
* members ("toLocaleString", "constructor", "valueOf") — genuine member
* NAMES user code can spell now that the real lib declares them; treating
* an inherited function as a table entry would mis-lower or ICE. */
export function own<T>(table: Record<string, T | undefined>, key: string): T | undefined {
return Object.hasOwn(table, key) ? table[key] : undefined;
}
/** Sentinel binding key for `this` (which has no ts.Symbol): a stable
* object identity used in the same scope/capture maps as real symbols, so
* arrows capturing `this` ride the ordinary capture machinery. */
const THIS_BINDING = { escapedName: "%this" } as unknown as ts.Symbol;
/* ── the island boundary, in one voice ────────────────────────────────
* Whether a value can cross between the static world and the island is
* ONE question — canCrossIslandBoundary (ir.ts), asked here through
* boundarySafe() — and each rejected direction has ONE message builder,
* so the rule and its wording cannot drift apart across the implicit
* coercion path, the explicit marshal path, and the exact-type fence. */
/** Per-function lowering context. A stack of these models nested functions:
* identifier resolution walks outward, and a hit in an enclosing context
* turns into a capture (boxing the binding at its origin and threading it
* through every function in between). */
export interface FnCtx {
locals: IrLocal[];
scopes: Map<ts.Symbol, IrLocal>[];
localCounters: Map<string, number>;
/** Lifted functions only: capture entries (also present in `locals`,
* boxed), in closure caps[] order. undefined ⇔ plain declared function. */
captures: IrParam[] | null;
/** Parent-function localIds feeding each capture, parallel to captures. */
captureSources: string[];
captureBySymbol: Map<ts.Symbol, IrLocal>;
/** Named function expressions/declarations: the function's own name
* symbol. Self-references become `selfRef` (NOT a capture — a box holding
* its own closure would be an RC cycle and leak). */
selfSymbol: ts.Symbol | null;
selfType: IrType | null;
/** Await is legal here (async function body). */
isAsync?: boolean;
/** Yield is legal here (generator function body): the yield/next value
* channels the yield lowering types itself against. */
generator?: { yieldT: IrType; nextT: IrType; resultType: IrType & { kind: "record" } } | null;
/** VARIADIC `arguments` form (rest-marked func type with no declared
* rest param): the synthetic trailing dyn-array param `arguments`
* reads resolve to. */
argumentsLocal?: IrLocal | null;
/** Declared return type — lets `return` detect record-shape mismatches
* (SC2002) before the validator would ICE on them. */
returnType: IrType;
/** Implicit-any instance RETURN INFERENCE (resolveInferredReturn):
* present ⇔ `return` statements lower their values BARE (no coercion)
* and record themselves here; the post-pass unifies the types and wraps
* each return onto the settled one. `returnType` holds the DYN pin. */
inferReturn?: { entries: { stmt: IrStmt; node: ts.Expression | null }[] } | null;
/** Enclosing jump targets, innermost last. `labels` carries the source
* statement's JS label names so labeled break/continue resolve. Finally
* regions do not appear here: the backends route every abrupt completion
* through the cleanup regions it crosses. Per function, so a nested
* function's jumps never bind to enclosing constructs. */
ctl: { kind: "loop" | "switch" | "block"; labels?: string[] }[];
}
export function newFnCtx(
lifted: boolean,
selfSymbol: ts.Symbol | null,
selfType: IrType | null,
returnType: IrType,
): FnCtx {
return {
locals: [],
scopes: [new Map()],
localCounters: new Map(),
captures: lifted ? [] : null,
captureSources: [],
captureBySymbol: new Map(),
selfSymbol,
selfType,
returnType,
ctl: [],
};
}
export interface LowerStats {
/** Statements the lowerer attempted (nested statements count individually;
* statements inside a poisoned construct were never reached and don't). */
statementsTotal: number;
statementsFailed: number;
/** Statements that LOWERED but contain island constructs (jsOp/jsExit —
* package calls, island-backed lib members): they compile, but their
* work runs in the embedded engine. Only a --dynamic analysis produces
* these; coverage renders them as "compile dynamically". */
statementsIsland: number;
/** Functions whose signature couldn't be analyzed (bodies not counted). */
functionsSkipped: number;
}
/** IrStmt discriminants — the island walk below must not descend into
* NESTED statements (each is counted individually by its own lowerStmts
* visit; descending would attribute a nested island statement to every
* enclosing construct too). The top-level statement object itself is
* always visited. */
const IR_STMT_KINDS = new Set([
"varDecl", "assign", "exprStmt", "if", "while", "doWhile", "switch",
"arraySet", "arraySetLength", "arraySetUndefined", "arrayDelete", "forOf", "return", "fieldSet", "recordSet", "break",
"continue", "block", "tryCatch", "throw", "rethrow", "runtimeFence",
]);
/** True when a lowered statement's OWN expressions contain island
* constructs — a generic JSON walk (like moduleUsesRegex): `kind`
* discriminants live only on IR objects, so user string values can never
* false-positive. Nested statements are skipped (counted separately). */
/** Every identifier a binding name binds: the identifier itself, or all
* identifiers of a (possibly nested) destructuring pattern in source
* order. */
export function boundIdentifiersOf(name: ts.BindingName): ts.Identifier[] {
if (ts.isIdentifier(name)) return [name];
const out: ts.Identifier[] = [];
for (const el of name.elements) {
// Elisions: OmittedExpression in 5.9.3, a NAMELESS BindingElement in 7
// (the parity battery's pinned finding 2) — both spell "no binding".
if (ts.isOmittedExpression(el) || el.name === undefined) continue;
out.push(...boundIdentifiersOf(el.name));
}
return out;
}
export function stmtUsesIsland(stmts: IrStmt | IrStmt[]): boolean {
let found = false;
const visit = (v: unknown, root: boolean): void => {
if (found || v === null || typeof v !== "object") return;
if (Array.isArray(v)) {
for (const item of v) visit(item, root);
return;
}
const kind = (v as { kind?: unknown }).kind;
if (!root && typeof kind === "string" && IR_STMT_KINDS.has(kind)) return;
const fn = (v as { fn?: unknown }).fn;
if (
kind === "jsOp" || kind === "jsExit" || kind === "jsBridgePromise" ||
fn === "island.eval" || fn === "island.import" || fn === "island.importDyn" ||
fn === "island.castFail"
) {
found = true;
return;
}
for (const value of Object.values(v)) visit(value, false);
};
visit(stmts, true);
return found;
}
export interface LowerResult {
/** Present iff diagnostics is empty. */
module: IrModule | null;
diagnostics: ScrDiagnostic[];
/** JS statements whose compile fences DEFERRED to runtime (runtimeFence
* statements in the module) — off the build, on the coverage report. */
runtimeFences: ScrDiagnostic[];
stats: LowerStats;
/** --provenance-sources only: per-file statement attribution (the
* coverage report aggregates it per provenance package). */
statsByFile?: Map<string, { total: number; failed: number; island: number }>;
/** --provenance-sources only: diagnostics of elided pure-annotated dead
* consts in fetched source modules — off the build, on the report. */
provenanceElided?: ScrDiagnostic[];
/** Coverage only (LowerOptions.coverage): the unreached remainder,
* lowered in a throwaway pass — blockers in it can never fail a build. */
unreached?: { diagnostics: ScrDiagnostic[]; stats: LowerStats };
/** --dynamic only: every Node builtin the embedded npm graph imports,
* shimmed or not — the coverage report's island honesty. */
npmBuiltins?: NpmBuiltinUse[];
/** --dynamic only: unresolvable specifiers reached ONLY by require()/
* import() edges — the build embeds Node's call-time error as a runtime
* trap; the coverage report lists them beside the builtins. */
npmLazyTraps?: NpmLazyTrap[];
}
export interface LowerOptions {
/** --dynamic: the island engine is linked, so island constructs
* (__island_eval) may lower. Off by default — without it they produce a
* requires-dynamic diagnostic instead. */
dynamic?: boolean;
/** Coverage: additionally lower the unreached remainder (bodies nothing
* on the entry path reaches) in a throwaway pass and report its
* diagnostics and stats under `unreached` — the whole-program analysis
* builds deliberately gave up. */
coverage?: boolean;
/** The platform the build TARGETS ("win32" under a windows cross triple,
* the host platform otherwise — see buildTargetPlatform in index.ts).
* The whole program compiles for one platform, so the platform-keyed
* surfaces are compile-time constants: on win32 the bare path module
* binds path.win32 (Node on Windows IS path.win32) and path.sep /
* path.delimiter / os.EOL lower as the win32 literals; path.posix and
* path.win32 keep answering THEIR platform everywhere, like Node's. */
targetPlatform?: string;
/** Node's startup refusal (LoadResult.startupCrash — preflight's
* resolution walk and named-import link checks): the program
* compiles to that startup crash. */
startupCrash?: StartupCrash | null;
/** LIBRARY mode's reachability roots: the profile-mapped exports of the
* entry module. Executable builds root at the entry's top level alone
* (an unreferenced export dead-strips); a library's exports are called from
* OUTSIDE the graph, so discovery seeds them alongside the init
* bodies. Names are the entry file's unqualified declaration names. */
libRoots?: readonly string[];
/** Outbound native FFI declarations from a validated format-1 manifest.
* Calls of their exact ambient TypeScript bindings lower to direct C ABI
* imports; without this option ambient declarations keep Node's ordinary
* ReferenceError behavior. */
ffiImports?: readonly IrFfiImport[];
/** LIBRARY mode's host-callback surface marker: the `ffiImports` above
* are profile-declared callback channels, not native-manifest bindings.
* Flips the binding diagnostics to the library flavor (SC4024), keeps a
* channel legal when no program declaration references it (an unused
* channel is capacity, not an error), and refuses a CALL of any other
* program-authored signature-only ambient function with the callback
* teaching instead of the ambient ReferenceError lowering. */
libraryCallbacks?: boolean;
/** Coverage-only external host type surfaces. Their declarations inform
* the checker, while every runtime value use remains an SC1010 fence. */
externalTypes?: ReadonlyMap<string, string>;
/** The mapped entries plus relative declaration dependencies, attributed
* to their owning external specifier. */
externalTypeSpecifiersByFile?: ReadonlyMap<string, readonly string[]>;
}
interface RuntimeFenceFallback {
code: `SC${number}`;
message: string;
}
interface RuntimeFenceBase {
/** Used only by catches that can legitimately have no fresh diagnostic. */
fallback?: RuntimeFenceFallback;
/** Legacy function-body fences throw the diagnostic text without a site suffix. */
bareMessage?: boolean;
/** The closure-probe fallback historically applies inside diagnostic captures. */
allowDiagSink?: boolean;
}
interface RuntimeFenceStatementTarget extends RuntimeFenceBase {
kind: "statement";
}
interface RuntimeFenceFunctionTarget extends RuntimeFenceBase {
kind: "function";
name: string | (() => string);
params?: IrParam[];
returnType: IrType;
paramsMutable?: boolean;
async?: true;
generator?: NonNullable<IrFunction["generator"]>;
}
interface RuntimeFenceClosureTarget extends Omit<RuntimeFenceFunctionTarget, "kind"> {
kind: "closure";
type: IrType & { kind: "func" };
}
type RuntimeFenceTarget =
| RuntimeFenceStatementTarget
| RuntimeFenceFunctionTarget
| RuntimeFenceClosureTarget;
/** The Lowerer's pass configuration (see lowerToIr). */
export interface LowererMode {
/** Names of bodies a prior reachability pass reached; null lowers everything. */
reachable?: ReadonlySet<string> | null;
/** Coverage remainder: lower ONLY bodies outside `reachable`, skip the
* always-reachable init bodies and module building, and report deferred
* collection diagnostics nothing flushed. */
remainder?: boolean;
/** Symbols whose deferred diagnostics reachable emit already flushed —
* the remainder must not report them a second time. */
alreadyFlushed?: ReadonlySet<ts.Symbol>;
/** The build's target platform (LowerOptions.targetPlatform — lowerToIr
* passes it to every pass). Defaults to the host. */
targetPlatform?: string;
/** Node's startup refusal (preflight's resolution walk / named-import
* link checks): %main opens with exactly this throw, before any
* module init — Node refuses the whole graph before anything evaluates,
* so nothing runs. */
startupCrash?: StartupCrash | null;
/** The build's outbound native FFI declarations. */
ffiImports?: readonly IrFfiImport[];
/** LowerOptions.libraryCallbacks (see there). */
libraryCallbacks?: boolean;
/** Statically resolved child_process.fork program roots, in stable target-id order. */
forkTargets?: readonly ts.SourceFile[];
/** Program-validated ambient declaration symbols for each FFI name.
* Undefined in discovery's legacy call-local validation path. */
ffiBindingSymbols?: ReadonlyMap<string, ReadonlySet<ts.Symbol>>;
/** LowerOptions.externalTypes, threaded through every lowering pass. */
externalTypes?: ReadonlyMap<string, string>;
/** LowerOptions.externalTypeSpecifiersByFile, shared by every pass. */
externalTypeSpecifiersByFile?: ReadonlyMap<string, readonly string[]>;
}
function directExternalTypeSpecifiersByFile(
externalTypes: ReadonlyMap<string, string>,
): ReadonlyMap<string, readonly string[]> {
const out = new Map<string, string[]>();
for (const [specifier, file] of externalTypes) {
const key = tsgoPath(resolve(file));
const owners = out.get(key);
if (owners === undefined) out.set(key, [specifier]);
else if (!owners.includes(specifier)) owners.push(specifier);
}
return out;
}
/** Build lowering runs as a reachability worklist over the ts.Program:
*
* 1. REACHABLE EMIT — a worklist computes the set of reachable bodies.
* Seeds are
* the per-file init bodies (module top-level statements always run, in
* import order); lowering a body yields IR whose call/closure/new/
* virtualCall nodes are the edges that enqueue further bodies. The pass
* retains that IR. Once the graph closes, those functions are assembled in
* deterministic declaration order beside the already-lowered init,
* generic-instance, and lifted bodies. Checker-backed IR construction
* therefore happens once instead of once for discovery and again for
* emission.
*
* `coverage: true` adds a second pass — the REMAINDER — that lowers only
* the bodies reachable emit did NOT mark (plus deferred collection
* diagnostics nothing flushed), reported separately: whole-program analysis without
* letting unreached code fail builds. */
export function lowerToIr(
program: ts.Program,
entry: ts.SourceFile,
moduleOrder: ts.SourceFile[],
options: LowerOptions = {},
): LowerResult {
const phaseTiming = process.env["SCRIPTC_TIMING"] === "1";
const phaseStarted = performance.now();
let phaseLast = phaseStarted;
const timing = (phase: string, detail: Record<string, unknown> = {}): void => {
if (!phaseTiming) return;
const now = performance.now();
process.stderr.write(
`scriptc lowering ${JSON.stringify({
phase,
phase_ms: Math.round((now - phaseLast) * 10) / 10,
total_ms: Math.round((now - phaseStarted) * 10) / 10,
...detail,
})}\n`,
);
phaseLast = now;
};
const dynamic = options.dynamic ?? false;
const targetPlatform = options.targetPlatform ?? process.platform;
const startupCrash = options.startupCrash ?? null;
// Modules reachable only through literal import() of the program's own
// files join the compiled graph here, ONCE, before any
// pass constructs (nothing calls their %init at startup — the import()
// site's namespace builder does, on the engine microtask, Node's
// evaluation point for them). Inadmissible static cycles inside the
// added subgraph are minted here and handed to reachable emit after this
// extension of the shared array; no later pass re-walks the subgraph.
const extraModuleCycleDiags: ScrDiagnostic[] = [];
const cycleKeys = new Set<string>();
const forkTargets: ts.SourceFile[] = [];
const onExtraCycle = (cycle: string, reason: string): void => {
const key = `${cycle}\0${reason}`;
if (cycleKeys.has(key)) return;
cycleKeys.add(key);
extraModuleCycleDiags.push(
unsupportedDiag("SC1016", { file: entry.fileName, start: 0, end: 0 }, `circular imports (${cycle}; ${reason})`),
);
};
for (let pass = 0; pass < 32; pass++) {
const before = moduleOrder.length;
appendDynamicImportModules(program, moduleOrder, onExtraCycle);
appendForkModules(program, moduleOrder, forkTargets, onExtraCycle);
if (moduleOrder.length === before) break;
if (pass === 31) throw new Error("dynamic import and fork module discovery did not converge");
}
const ffiImports = options.ffiImports ?? [];
const libraryCallbacks = options.libraryCallbacks ?? false;
const externalTypes = options.externalTypes ?? new Map<string, string>();
const externalTypeSpecifiersByFile = options.externalTypeSpecifiersByFile ??
directExternalTypeSpecifiersByFile(externalTypes);
const validation = new Lowerer(program, entry, moduleOrder, dynamic, {
targetPlatform,
startupCrash,
ffiImports,
libraryCallbacks,
forkTargets,
externalTypes,
externalTypeSpecifiersByFile,
});
const ffiValidation = validateFfiImports(validation);
timing("ffi-validate");
// Reachability must use the same exact-symbol ownership as FFI validation.
// Otherwise a local function shadowing a configured ambient name is mistaken for FFI
// while computing reachability, even though ordinary lowering would
// correctly handle it as TypeScript. FFI-free builds reuse the validation
// lowerer because validation is an immediate no-op there.
const reachableEmit = ffiImports.length === 0
? validation
: new Lowerer(program, entry, moduleOrder, dynamic, {
targetPlatform,
startupCrash,
ffiImports,
libraryCallbacks,
forkTargets,
externalTypes,
externalTypeSpecifiersByFile,
ffiBindingSymbols: ffiValidation.symbolsByName,
});
for (const d of extraModuleCycleDiags) reachableEmit.pushDiag(d);
for (const d of ffiValidation.diagnostics) reachableEmit.pushDiag(d);
const emitted = reachableEmit.emitReachable(options.libRoots);
const { reachable } = emitted;
let result = emitted.result;
let resultLowerer = reachableEmit;
timing("reachable-emit", { reachable: reachable.size });
// A generic class-rest support decision can depend on record metadata
// whose historical owner is discovered only while retained bodies lower.
// If the settled answer is a fence, rerun the ordinary reachable emit so
// the PoisonError occurs in its original statement window: later
// declarators stay unvisited, bindings block, cascades and stats match the
// historical compiler. This is a rare compatibility fallback; programs
// without such a settled fence retain checker-backed IR exactly once.
if (reachableEmit.requiresHistoricalOrderRelower) {
const emit = new Lowerer(program, entry, moduleOrder, dynamic, {
reachable,
targetPlatform,
startupCrash,
ffiImports,
libraryCallbacks,
forkTargets,
ffiBindingSymbols: ffiValidation.symbolsByName,
externalTypes,
externalTypeSpecifiersByFile,
});
for (const d of extraModuleCycleDiags) emit.pushDiag(d);
for (const d of ffiValidation.diagnostics) emit.pushDiag(d);
result = emit.run();
resultLowerer = emit;
timing("historical-order-relower");
}
if (options.coverage !== true) return result;
const remainder = new Lowerer(program, entry, moduleOrder, dynamic, {
reachable,
remainder: true,
alreadyFlushed: resultLowerer.flushedSymbols,
targetPlatform,
ffiImports,
libraryCallbacks,
forkTargets,
ffiBindingSymbols: ffiValidation.symbolsByName,
externalTypes,
externalTypeSpecifiersByFile,
});
const rem = remainder.run();
return { ...result, unreached: { diagnostics: rem.diagnostics, stats: rem.stats } };
}
/** The island-handle type a `import(...)` initializer gives a binding
* whose DECLARED type has no static mapping (`Promise<typeof
* import("./m")>` — module-namespace types don't map): the direct form
* holds the static promise-of-handle, the awaited form holds the handle
* itself. Null for every other initializer shape. */
export function importCallHandleType(expr: ts.Expression | undefined): IrType | null {
if (!expr) return null;
let e = expr;
let awaited = false;
for (;;) {
if (ts.isParenthesizedExpression(e)) {
e = e.expression;
} else if (ts.isAwaitExpression(e)) {
awaited = true;
e = e.expression;
} else {
break;
}
}
if (ts.isCallExpression(e) && e.expression.kind === ts.SyntaxKind.ImportKeyword) {
return awaited ? JSVAL : { kind: "promise", inner: JSVAL };
}
return null;
}
/** The native namespace type produced by a literal static import() of a
* compiled program module or supported Node builtin. This syntax-level
* fallback is used where the checker exposes an anonymous builtin-module
* object type with no stable module symbol (locals/globals are still
* represented nominally by the value they actually receive). */
export function staticImportNamespaceType(lowerer: Lowerer, expr: ts.Expression | undefined): IrType | null {
if (!expr || lowerer.dynamic) return null;
let e = expr;
let awaited = false;
for (;;) {
if (ts.isParenthesizedExpression(e)) e = e.expression;
else if (ts.isAwaitExpression(e)) {
awaited = true;
e = e.expression;
} else break;
}
if (!ts.isCallExpression(e) || e.expression.kind !== ts.SyntaxKind.ImportKeyword) return null;
const arg = e.arguments[0];
if (!arg || !ts.isStringLiteralLike(arg)) return null;
const builtin = canonicalBuiltinModule(arg.text);
let moduleId: string | null = builtin === null ? null : `builtin:${builtin}`;
if (moduleId === null) {
const symbol = lowerer.checker.getSymbolAtLocation(arg);
const source = symbol && lowerer.checker.declarationsOf(symbol).find(
(decl): decl is ts.SourceFile => ts.isSourceFile(decl) && !decl.isDeclarationFile,
);
if (
source && !source.fileName.endsWith(".cts") && !isCjsJsFile(source) &&
lowerer.moduleOrder.includes(source)
) {
moduleId = `file:${tsgoPath(resolve(source.fileName))}`;
}
}
if (moduleId === null) return null;
const ns: IrType = { kind: "moduleNs", moduleId };
return awaited ? ns : { kind: "promise", inner: ns };
}
/** True when `expr` is a call that resolved to an overload SIGNATURE of a
* source-implemented function whose implementation returns an island value
* (`any` under --dynamic): tsc never checks overload return types against
* the body — only the implementation signature is checked — so the
* overload's return is an unverifiable claim about an island value. The
* binding stores the HANDLE instead of trap-extracting the claimed type
* (reconcileOverloadReturn keeps the call jsval by the same rule), and
* uses dispatch to engine ops — exactly the value Node's binding holds.
* Ambient (.d.ts) declarations never reach this: they have no compiled
* implementation, so their calls lower through the island/builtin paths
* whose validated exits keep the checker-trust trap. */
export function uncheckedOverloadHandleCall(lowerer: Lowerer, expr: ts.Expression | undefined): boolean {
if (!lowerer.dynamic || !expr) return false;
let e = expr;
while (ts.isParenthesizedExpression(e)) e = e.expression;
// Tagged templates are calls too (tag(strings, ...values)) and resolve
// overload sets the same way — foo1`${1}` against a TemplateStringsArray
// overload of an any-returning implementation stores the handle.
if (!ts.isCallExpression(e) && !ts.isTaggedTemplateExpression(e)) return false;
const rsig = lowerer.checker.getResolvedSignature(e);
const rdecl = rsig ? lowerer.checker.signatureDeclaration(rsig) : undefined;
if (!rsig || !rdecl) return false;
if (!(ts.isFunctionDeclaration(rdecl) || ts.isMethodDeclaration(rdecl)) || rdecl.body) return false;
const name = rdecl.name;
const symbol = name ? lowerer.checker.getSymbolAtLocation(name) : undefined;
if (!symbol) return false;
const impl = lowerer.checker
.declarationsOf(symbol)
.find((d) => (ts.isFunctionDeclaration(d) || ts.isMethodDeclaration(d)) && (d as ts.FunctionDeclaration).body !== undefined);
if (!impl) return false;
const implSig = lowerer.checker.getSignatureFromDeclaration(impl);
if (!implSig) return false;
return lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(implSig))?.kind === "jsval";
}
/** The JavaScript declaration fallback for unmappable binding types (see
* irTypeOf): `any` and every other inference residue is the checked-
* dynamic 'unknown' kind, and array types keep their array-ness with the
* fallback applied to the ELEMENT (any[]/never[] evolving arrays become
* unknown[], so length/push/index still lower). Null for TypeScript
* files and for void (no value exists to represent). */
/** A JS-file type carrying `never[]` (or a never element) ANYWHERE in its
* array/tuple/union structure: tsc's inference residue for evolving and
* information-free shapes — the bare `const gb = []` (never[]), the mixed
* command tuple `['pwd', []]` ((string | never[])[]). never's f64
* representation (mapType's uninhabited stance, sound for genuinely dead
* TS reads) must not capture these VALUES — a later dyn push would
* dynCheck strings into a number array, a union arm would re-tag as
* number[] and fence. Callers treat a tainted type as unmappable so the
* checked-dynamic fallbacks apply, the pre-never-mapping behavior. Bare
* `never` at the ROOT stays out (`for (const v of [])`'s loop var — the
* dead read the f64 mapping is FOR). */
export function neverTaintedJsType(lowerer: Lowerer, node: ts.Node, t: ts.Type): boolean {
if (!isJsSourceFile(node.getSourceFile())) return false;
const walk = (x: ts.Type, depth: number): boolean => {
if (depth === 0) return false;
if (x.isUnionType()) return ts.constituentTypes(x).some((a) => walk(a, depth - 1));
if (lowerer.checker.isArrayType(x) || lowerer.checker.isTupleType(x)) {
return lowerer.checker
.getTypeArguments(x as ts.TypeReference)
.some((a) => (a.flags & ts.TypeFlags.Never) !== 0 || walk(a, depth - 1));
}
return false;
};
return walk(t, 4);
}
/** The dyn undefined value — what an uninitialized checked-dynamic
* binding holds (JS: declared bindings read `undefined` before any
* assignment). A NULL dyn slot is a trap, never a value, so every dyn
* binding that is READABLE before its first assignment must start here:
* `let x;` declarations, hoisted `var`s (function and module scope,
* forward captures included), and the implicit-return completion
* (lower-calls' own copy of this pattern predates the helper). */
export function dynUndefinedExpr(loc: SrcLoc): IrExpr {
return {
kind: "dynFrom",
value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
type: DYN,
loc,
};
}
/** An always-throwing Node-parity error expression (the error.nodeThrow
* libCall — the lowered form of arms Node rejects unconditionally:
* ERR_INVALID_THIS receivers, ERR_MISSING_ARGS arity ladders, the
* symbol-to-string TypeError). kind 0 Error / 1 TypeError / 2 RangeError;
* an empty code means no code slot. `type` is the replaced expression's
* own (never materialized — the global.undefRead pattern). */
export function nodeThrowExpr(kind: 0 | 1 | 2, code: string, message: string, type: IrType, loc: SrcLoc): IrExpr {
return {
kind: "libCall",
fn: "error.nodeThrow",
args: [
{ kind: "numLit", value: kind, type: F64, loc },
{ kind: "strLit", value: code, type: STRING, loc },
{ kind: "strLit", value: message, type: STRING, loc },
],
type,
loc,
};
}
/** The post-validation fence STRING a validation-ladder Chk libCall
* throws after its Node-order checks pass: the same SC2020 text the
* per-statement runtime fence would have thrown (message + "[code at
* file:line]"), rendered eagerly so the runtime can throw it verbatim
* (scr_throw_lowering_fence). The diagnostic joins the runtime-fence
* ledger exactly like a deferred statement fence — nothing silently
* drops off the coverage report. */
export function ladderFenceExpr(lowerer: Lowerer, surface: string, node: ts.Node, hint?: string): IrExpr {
const loc = locOf(node);
const d = noLoweringDiag(surface, loc, hint);
lowerer.runtimeFences.push(d);
const sf = node.getSourceFile();
const pos = ts.getLineAndCharacterOfPosition(sf, loc.start);
return {
kind: "strLit",
value: `${d.message} [${d.code} at ${loc.file}:${pos.line + 1}]`,
type: STRING,
loc,
};
}
/** The checked-dynamic declaration fallback for unmappable binding types
* (see irTypeOf), two gates over one story:
*
* JAVASCRIPT files: `any` and every other inference residue is the
* checked-dynamic 'unknown' kind, and array types keep their array-ness
* with the fallback applied to the ELEMENT (any[]/never[] evolving arrays
* become unknown[], so length/push/index still lower).
*
* TYPESCRIPT files: genuine checker-`any` residue ONLY — a bare `any`
* binding (`flags & Any`), or a single-call-signature function type whose
* only unmappable pieces are `any` (`(value: any) => value is string` —
* the arrow the binding holds lowers those params to dyn, so the binding
* keeps its func-ness with the same per-piece fallback). The honest
* static subset of `any` is a binding whose VALUES are dyn-representable:
* the binding is 'unknown' storage with the boundary conversions
* coerceToExpected already applies (dynFrom into the slot, validated
* dynCheck out) and per-site SC2011 fences for the operations the checked-dynamic tree
* cannot carry JS-exactly (the island still lifts those). Every OTHER
* unmappable TS type keeps its own diagnostic — annotations exist there,
* and the fence names the real blocker. `--dynamic` builds never reach
* this fallback for `any` (mapType answers jsval first).
*
* Null for void (no value exists to represent). */
export function dynFallbackType(lowerer: Lowerer, node: ts.Node, t: ts.Type): IrType | null {
if (t.flags & ts.TypeFlags.Void) return null;
if (!isJsSourceFile(node.getSourceFile())) {
if (t.flags & ts.TypeFlags.Any) return DYN;
// TS single-call-signature function types: per-piece fallback, but
// ONLY `any` pieces fall to dyn — any other unmappable piece keeps
// the whole type's own fence.
return anyPiecedFuncType(lowerer, node, t);
}
// JS inference commonly spells a callback field initialized to null as
// `((value: any) => any) | null`. Preserve the callable arm and its
// checked-dynamic pieces instead of collapsing the whole slot to dyn.
// Besides keeping direct calls static, this leaves closure captures
// visible to the native cycle collector (a dyn-boxed closure edge is
// intentionally opaque to it).
if (t.isUnionType()) {
const arms: IrType[] = [];
let functions = 0;
for (const part of ts.constituentTypes(t)) {
if (part.flags & ts.TypeFlags.Null) {
arms.push(NULL_T);
continue;
}
if (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) {
arms.push(UNDEFINED_T);
continue;
}
const mapped = lowerer.mapTypeOf(part) ?? jsFallbackFunctionType(lowerer, node, part);
if (mapped?.kind !== "func") {
functions = -1;
break;
}
functions++;
arms.push(mapped);
}
if (functions === 1 && arms.length > 1) {
const canonical = [...new Map(arms.map((arm) => [typeKey(arm), arm])).entries()]
.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
.map(([, arm]) => arm);
return canonical.length === 1
? canonical[0]!
: { kind: "union", unionId: lowerer.unions.intern(canonical) };
}
}
if (lowerer.checker.isArrayType(t)) {
const elem = lowerer.checker.getTypeArguments(t as ts.TypeReference)[0];
const elemTainted =
elem !== undefined &&
((elem.flags & ts.TypeFlags.Never) !== 0 || neverTaintedJsType(lowerer, node, elem));
const mappedElem = elem !== undefined && !elemTainted ? lowerer.mapTypeOf(elem) : null;
// A mappable element keeps the static array; an unmappable one makes
// the WHOLE value dyn (the checked-dynamic tree has real arrays — length/index/push
// read through the keyed-dyn paths; dyn-element STATIC arrays have no
// backend representation).
if (mappedElem) return { kind: "array", elem: mappedElem };
}
// A PURE single-call-signature type (an implicit-any JS function —
// `exports.check = function (certs) {...}`, common/tls's shape): keep
// its func-ness like arrays keep array-ness, with the fallback applied
// per PIECE — unmappable params/returns become the checked-dynamic
// kind, so direct calls stay static calls and value uses cross the
// boundary by boxing (canBoxFuncIntoDyn). Generics, rest params,
// construct signatures, overloads, and function-with-properties shapes
// stay out (the whole value falls to dyn below, where every reached
// use meets its own fence or boxes as-is).
const fallbackFunction = jsFallbackFunctionType(lowerer, node, t);
if (fallbackFunction !== null) return fallbackFunction;
return DYN;
}
function jsFallbackFunctionType(lowerer: Lowerer, node: ts.Node, t: ts.Type): IrType | null {
const sig = pureSingleCallSignatureOf(lowerer, t);
if (!sig) return null;
const params = sig.getParameters().map((p): IrType => {
const pt = lowerer.checker.getTypeOfSymbolAtLocation(p, node);
let mapped = lowerer.mapTypeOf(pt) ?? DYN;
// The fallback must preserve the same completed ABI as mapType and
// lambdaSignature: a spelled optional/defaulted parameter accepts an
// omitted call, represented by an undefined arm in its fixed native
// slot. JS inference reports the body type (`boolean` for
// `enabled = true`) rather than that call-site type, so recover the
// syntactic optionality from the parameter declaration.
const decl = lowerer.checker.valueDeclarationOf(p);
mapped = jsOpenObjectType(decl, mapped, lowerer.shapes, lowerer.unions);
const optional =
decl !== undefined &&
ts.isParameter(decl) &&
(decl.questionToken !== undefined || decl.initializer !== undefined);
if (
optional &&
mapped.kind !== "dyn" &&
mapped.kind !== "jsval" &&
!lowerer.bareUndefinedArmedUnion(mapped)
) {
mapped = lowerer.withUndefinedArm(mapped);
}
return mapped;
});
const retT = lowerer.checker.getReturnTypeOfSignature(sig);
const ret: IrType = retT.flags & ts.TypeFlags.Void ? VOID : lowerer.mapTypeOf(retT) ?? DYN;
return { kind: "func", params, ret };
}
/** A JavaScript `arguments` reader has the spelled parameters plus a hidden
* array of all supplied arguments in the native ABI. Recover that ABI from
* the implementation when the function flows through a value binding. */
function jsArgumentsFunctionType(lowerer: Lowerer, t: ts.Type): IrType | null {
if (!(t.flags & ts.TypeFlags.Object)) return null;
const sigs = lowerer.checker.getCallSignatures(t);
if (sigs.length !== 1 || sigs[0]!.getTypeParameters().length !== 0 || lowerer.checker.getConstructSignatures(t).length !== 0 || lowerer.checker.getPropertiesOfType(t).length !== 0) return null;
const decl = lowerer.checker.signatureDeclaration(sigs[0]!);
if (
decl === undefined || !(ts.isFunctionDeclaration(decl) || ts.isFunctionExpression(decl)) ||
!isJsSourceFile(decl.getSourceFile()) || !isNodeEsmFile(decl.getSourceFile()) ||
decl.parameters.length === 0 ||
decl.parameters.some((p) => p.dotDotDotToken !== undefined) ||
!bodyReadsArguments(decl as { body?: ts.Node })
) return null;
const shapes = paramShapes(lowerer, decl.parameters);
const retType = lowerer.checker.getReturnTypeOfSignature(sigs[0]!);
const ret = retType.flags & ts.TypeFlags.Void ? VOID : lowerer.mapTypeOf(retType) ?? DYN;
return funcTypeFromParamShapes([...shapes, { type: DYN, mode: "arguments" }], ret);
}
/** The one call signature of a PURE function type — single signature, no
* properties, no construct signatures, no type parameters, no rest params
* (declared or synthesized from an `arguments` read). Null for every
* other shape. The structural gate both dynFallbackType arms share. */
function pureSingleCallSignatureOf(lowerer: Lowerer, t: ts.Type): ts.Signature | null {
if (!(t.flags & ts.TypeFlags.Object)) return null;
const sigs = lowerer.checker.getCallSignatures(t);
if (
sigs.length === 1 &&
lowerer.checker.getPropertiesOfType(t).length === 0 &&
lowerer.checker.getConstructSignatures(t).length === 0 &&
sigs[0]!.getTypeParameters().length === 0 &&
sigs[0]!.getParameters().every(
(p) => {
const pDecl = lowerer.checker.valueDeclarationOf(p);
return !pDecl || !ts.isParameter(pDecl) || pDecl.dotDotDotToken === undefined;
},
) &&
// A SYNTHESIZED rest param (tsc's `arguments` inference — no
// valueDeclaration to carry the dotDotDot): param-count mismatch
// against the signature's declaration; the whole value stays dyn.
(() => {
const sigDecl = lowerer.checker.signatureDeclaration(sigs[0]!);
const declParams = sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters : undefined;
if (declParams !== undefined && declParams.length !== sigs[0]!.getParameters().length) return false;
// tsgo never synthesizes the `arguments` pseudo-rest into the
// inferred signature (5.9.3 did — the count mismatch above was the
// whole detector there), so ask the declaration's body directly.
return !(sigDecl !== undefined && ts.isFunctionLike(sigDecl) && bodyReadsArguments(sigDecl as { body?: ts.Node }));
})()
) {
return sigs[0]!;
}
return null;
}
/** The TS arm's function-shape fallback: a pure single-call-signature
* type whose only UNMAPPABLE pieces are `any`-flavored keeps its
* func-ness with those pieces as dyn (`(value: any) => value is string`
* — the arrow the binding holds lowers its params through the same
* irTypeOf fallback, so the binding type and the closure type agree).
* A piece that fails to map for any other reason answers null — the
* whole type keeps its own diagnostic. */
function anyPiecedFuncType(lowerer: Lowerer, node: ts.Node, t: ts.Type): IrType | null {
const sig = pureSingleCallSignatureOf(lowerer, t);
if (!sig) return null;
const params: IrType[] = [];
for (const p of sig.getParameters()) {
const pt = lowerer.checker.getTypeOfSymbolAtLocation(p, node);
const mapped = lowerer.mapTypeOf(pt) ?? (pt.flags & ts.TypeFlags.Any ? DYN : null);
if (!mapped || mapped.kind === "void") return null;
params.push(mapped);
}
const retT = lowerer.checker.getReturnTypeOfSignature(sig);
const ret: IrType | null =
retT.flags & (ts.TypeFlags.Void | ts.TypeFlags.Never) ? VOID
: lowerer.mapTypeOf(retT) ?? (retT.flags & ts.TypeFlags.Any ? DYN : null);
if (!ret) return null;
return { kind: "func", params, ret };
}
/** The best-effort JS `Function.prototype.name` of an expression flowing
* into a dyn slot (the boxed function kind's inspect/error name):
* identifier and property reads answer the referenced NAME (a
* REFERENCE-SITE approximation of JS's creation-site naming — an aliased
* binding reports the alias; SEMANTICS.md), named function expressions
* their own name, anonymous function/arrow expressions their
* NamedEvaluation home (a variable initializer or property assignment).
* Null when nothing names the value (the box stays anonymous). */
export function jsFuncNameOf(node: ts.Node): string | null {
let n: ts.Node = node;
while (ts.isParenthesizedExpression(n)) n = n.expression;
if (ts.isIdentifier(n)) return n.text;
if (ts.isPropertyAccessExpression(n)) return n.name.text;
if ((ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n)) && n.name) return n.name.text;
if (ts.isFunctionExpression(n) || ts.isArrowFunction(n)) {
const p = n.parent;
if (p && ts.isVariableDeclaration(p) && p.initializer === n && ts.isIdentifier(p.name)) {
return p.name.text;
}
if (p && ts.isPropertyAssignment(p) && p.initializer === n && ts.isIdentifier(p.name)) {
return p.name.text;
}
}
return null;
}
export class Lowerer {
readonly checker: ts.TypeChecker;
readonly diags: ScrDiagnostic[] = [];
readonly fnSigsBySymbol = new Map<ts.Symbol, FnSig>();
/** Immutable callable aliases and compiler-generated adapters. The map
* is symbol-keyed, so shadowing and cross-module imports keep the same
* exact binding discipline as globals/functions. */
readonly staticCallables = new Map<ts.Symbol, StaticCallableProjection>();
readonly genericFnsBySymbol = new Map<ts.Symbol, GenericFnInfo>();
/** Object-literal GENERIC methods (`{ m<T>(x: T) {...} }`), interned by
* their function-like node — instances ride the same monomorphization
* queue (objLitGenericFnInfoOf). */
readonly objLitGenericFns = new Map<ts.Node, GenericFnInfo>();
/** Generic arrow/function-expression INITIALIZERS of never-reassigned
* bindings (`const f = <T>(x: T) => x`), interned by the function-like
* node — registered in genericFnsBySymbol under the binding's symbol
* (and a named function expression's own inner name), so calls and
* pinned values resolve through genericFnOf exactly like top-level
* generic function declarations (bindingGenericFnInfoOf). */
readonly bindingGenericFns = new Map<ts.Node, GenericFnInfo>();
/** Per-symbol result of the never-reassigned file scan
* (bindingNeverReassigned — object-literal generic-method receivers). */
readonly neverReassignedCache = new Map<ts.Symbol, boolean>();
/** TRAP bindings: declarations whose initializer provably throws before
* producing a value (its chain roots at an ambient-undefined name —
* ambientUndefVarRootOf). Module init unwinds at the declaration, so no
* reference to the binding can ever execute; the statement lowers to the
* root's throw, no storage exists, and references lower to the same
* trap shape (never reached — sound whatever the type). */
readonly trapBindings = new Set<ts.Symbol>();
/** NULLISH bindings of unmappable (generic-signature) types: `const i:
* I<A & B> = null as any` — the binding provably holds null/undefined
* forever (every write's RHS is nullish too), so no storage exists and
* each READ knows the value. Member reads and method calls through one
* lower to Node's exact TypeError ("Cannot read properties of null
* (reading 'fn')"); nullish-to-nullish flows lower to nothing. The map
* answers which unit the binding holds (the TypeError names it). Null
* entries cache probed non-qualifiers. */
readonly nullishBindings = new Map<ts.Symbol, "null" | "undefined" | null>();
/** DEAD bindings of unmappable types: never READ anywhere in the
* program, declared with no initializer or a value-only one (a function
* literal), every write's RHS side-effect-free. Node materializes the
* value and drops it — zero observable effect — so the declaration and
* its writes lower to nothing, and no type fence fires for a value the
* program never consumes. */
readonly deadBindings = new Set<ts.Symbol>();
/** IMPLICIT-ANY function-value bindings (npm-static JS — `const knownBy
* = (cmd) => ...`), by their VariableDeclaration: the registered info,
* or null for probed non-qualifiers (implicitLocalFnNodeOf). */
readonly implicitLocalFns = new Map<ts.Node, GenericFnInfo | null>();
/** Monomorphization worklist: instances queued by call sites, drained in
* run() (processing an instance body can queue more). */
readonly instantiationQueue: { info: GenericFnInfo; inst: GenericInstance }[] = [];
/** Historical emit rank of the retained declaration/init body currently
* lowering, and the earliest such owner that demanded each generic
* instance. Reachability can encounter a later caller first; the minimum
* rank recovers the old emitter's source-order monomorphization queue. */
private genericDemandOwner: GenericDemandOwner | null = null;
private readonly genericDemandRoots: GenericDemandOwner[] = [];
private readonly genericFunctionDemandOwner = new Map<GenericInstance, GenericDemandOwner>();
private readonly genericClassDemandOwner = new Map<ClassInfo, GenericDemandOwner>();
private readonly genericDemandPriority = new Map<GenericInstance, DeclaredOrderPriorityRef>();
private readonly genericClassDemandPriority = new Map<ClassInfo, DeclaredOrderPriorityRef>();
private withGenericDemandOwner<T>(
owner: GenericDemandOwner,
fn: () => T,
): T {
const previous = this.genericDemandOwner;
this.genericDemandOwner = owner;
try {
return fn();
} finally {
this.genericDemandOwner = previous;
}
}
noteGenericInstanceDemand(inst: GenericInstance): void {
const owner = this.genericDemandOwner;
owner?.functionDemands.push(inst);
const ref = this.genericDemandPriority.get(inst) ?? { rank: [4, Number.MAX_SAFE_INTEGER] };
const currentRank = owner?.priority;
if (currentRank && (
currentRank[0] < ref.rank[0]! ||
(currentRank[0] === ref.rank[0] && currentRank[1] < (ref.rank[1] ?? 0))
)) {
ref.rank = currentRank;
}
this.genericDemandPriority.set(inst, ref);
}
noteGenericClassInstanceDemand(info: ClassInfo): void {
const owner = this.genericDemandOwner;
owner?.classDemands.push(info);
const ref = this.genericClassDemandPriority.get(info) ?? { rank: [4, Number.MAX_SAFE_INTEGER] };
const currentRank = owner?.priority;
if (currentRank && (
currentRank[0] < ref.rank[0]! ||
(currentRank[0] === ref.rank[0] && currentRank[1] < (ref.rank[1] ?? 0))
)) {
ref.rank = currentRank;
}
this.genericClassDemandPriority.set(info, ref);
}
/** The old emitter lowered all reachable declarations in source order,
* then every init, before draining generic instances FIFO. Reorder the
* retained queue from those recorded demands before any generic body
* lowers, so immediate support decisions see the same shape metadata too. */
private restoreGenericInstanceOrder(from = 0): void {
const tail = this.instantiationQueue.slice(from);
const discoveryOrder = new Map(tail.map((entry, index) => [entry.inst, index] as const));
tail.sort((left, right) => {
const a = this.genericDemandPriority.get(left.inst)?.rank;
const b = this.genericDemandPriority.get(right.inst)?.rank;
if (a !== undefined && b !== undefined) {
const phase = a[0]! - b[0]!;
if (phase !== 0) return phase;
const order = a[1]! - b[1]!;
if (order !== 0) return order;
} else if (a !== undefined) {
return -1;
} else if (b !== undefined) {
return 1;
}
return discoveryOrder.get(left.inst)! - discoveryOrder.get(right.inst)!;
});
this.instantiationQueue.splice(from, tail.length, ...tail);
}
private restoreGenericClassInstanceOrder(from = 0): void {
const tail = this.genericClassInstances.slice(from);
const discoveryOrder = new Map(tail.map((info, index) => [info, index] as const));
tail.sort((left, right) => {
const a = this.genericClassDemandPriority.get(left)?.rank;
const b = this.genericClassDemandPriority.get(right)?.rank;
if (a !== undefined && b !== undefined) {
const phase = a[0]! - b[0]!;
if (phase !== 0) return phase;
const order = a[1]! - b[1]!;
if (order !== 0) return order;
} else if (a !== undefined) {
return -1;
} else if (b !== undefined) {
return 1;
}
return discoveryOrder.get(left)! - discoveryOrder.get(right)!;
});
this.genericClassInstances.splice(from, tail.length, ...tail);
}
private settleGenericDemandPriorities(): void {
const functionQueue: GenericInstance[] = [];
const classQueue: ClassInfo[] = [];
const seenFunctions = new Set<GenericInstance>();
const seenClasses = new Set<ClassInfo>();
const enqueue = (owner: GenericDemandOwner): void => {
for (const info of owner.classDemands) {
if (seenClasses.has(info)) continue;
seenClasses.add(info);
classQueue.push(info);
}
for (const inst of owner.functionDemands) {
if (seenFunctions.has(inst)) continue;
seenFunctions.add(inst);
functionQueue.push(inst);
}
};
for (const root of [...this.genericDemandRoots].sort((a, b) => {
const left = a.priority!;
const right = b.priority!;
return left[0] - right[0] || left[1] - right[1];
})) enqueue(root);
let classIndex = 0;
let functionIndex = 0;
let order = 0;
while (classIndex < classQueue.length || functionIndex < functionQueue.length) {
while (classIndex < classQueue.length) {
const info = classQueue[classIndex++]!;
this.genericClassDemandPriority.get(info)!.rank = [4, order++];
const owner = this.genericClassDemandOwner.get(info);
if (owner) enqueue(owner);
}
while (functionIndex < functionQueue.length) {
const inst = functionQueue[functionIndex++]!;
this.genericDemandPriority.get(inst)!.rank = [4, order++];
const owner = this.genericFunctionDemandOwner.get(inst);
if (owner) enqueue(owner);
}
}
for (const info of this.genericClassInstances) {
const ref = this.genericClassDemandPriority.get(info);
if (ref && ref.rank[1] === Number.MAX_SAFE_INTEGER) ref.rank = [4, order++];
}
for (const { inst } of this.instantiationQueue) {
const ref = this.genericDemandPriority.get(inst);
if (ref && ref.rank[1] === Number.MAX_SAFE_INTEGER) ref.rank = [4, order++];
}
}
/** Non-null while an instance body lowers: type-parameter symbol →
* concrete IR type, consulted inside mapType's recursion. */
typeParamBindings: Map<ts.Symbol, IrType> | null = null;
/** The ts-level twin of typeParamBindings, non-null while a CALL-keyed
* instance body lowers: type-parameter symbol → the bound CHECKER type,
* consulted where the mapped IrType has already widened away information
* the body needs — indexed accesses (`T[K]` needs K's literal key) and
* keyed record reads (`o[k]` where k's type is a literal-bound K). */
typeParamTsBindings: Map<ts.Symbol, ts.Type> | null = null;
/** Non-null while an IMPLICIT-ANY instance body lowers (npm-static JS —
* lower-calls' implicit-monomorphization section): bound param symbol →
* the call site's checker type, consulted by typeOf for identifier
* references the checker still types `any`. The implicit twin of
* typeParamBindings — the checker has no `T` to substitute, so the
* binding rides the node-type accessor instead of mapType. */
implicitParamTypes: Map<ts.Symbol, ts.Type> | null = null;
/** IMPLICIT-ANY instances lowered EAGERLY at first demand (their return
* types are inferred from the body — the call site needs them settled),
* collected here for run()'s function list (the liftedFns discipline). */
readonly implicitFns: IrFunction[] = [];
/** ALIASED-TYPEOF narrowing (npm-static JS — ms's `var type = typeof
* val; if (type === 'string') ...`): while a branch such a test proves
* lowers, the tested operand's symbol maps to the proven ARM's checker
* type here, and typeOf answers it — the checker only narrows const
* aliases, so this carries the var/let form the checker cannot.
* Scoped strictly by narrowingAliases (lowerIf / lowerCondition). */
readonly aliasNarrowTypes = new Map<ts.Symbol, ts.Type>();
/** Locals widened beyond the checker's type because an inferred indexed
* read can be absent at runtime. Bare reads preserve that union until a
* surrounding JavaScript guard/default consumes it. */
readonly runtimeOptionalLocals = new Set<IrLocal>();
/** An effectful switch test can change the original binding after the
* discriminant was captured. A case match narrows the captured value,
* not that mutable binding; preserve its tagged representation on reads. */
readonly unstableSwitchBindings = new Set<ts.Symbol>();
/** All storage slots widened for runtime absence, including slots whose
* current control-flow branch has temporarily narrowed the value. */
readonly runtimeOptionalStorageLocals = new Set<IrLocal>();
/** Globals whose checker-bare type was widened because an indexed read can
* carry undefined at runtime. Identifier reads keep the union tag so
* typeof and later boundaries observe the real value. */
readonly runtimeOptionalGlobals = new Set<IrGlobal>();
/** Bindings whose unchecked string arithmetic can produce either a number
* (NaN for an absent read) or a string. Preserve that result union at bare
* reads instead of applying the checker-bare string narrowing adapter. */
readonly runtimeOptionalArithmeticLocals = new Set<IrLocal>();
readonly runtimeOptionalArithmeticGlobals = new Set<IrGlobal>();
/** Binding symbols whose IR storage type was widened by the indexed-read
* prepass. This includes explicit annotations: unchecked TS annotations do
* not prove that an array property read produced a value. */
readonly runtimeOptionalBindingTypes = new Map<ts.Symbol, IrType>();
/** Concise callback/lambda returns promoted by the HOF callback prepass;
* arrows have no declaration symbol to key in fnSigsBySymbol. */
readonly runtimeOptionalFunctionReturns = new WeakMap<ts.Node, IrType>();
/** Arithmetic over an unchecked string read can answer either NaN or a
* string. Bindings and returns use this marker to retain that result union
* through checker-bare string annotations. */
readonly runtimeOptionalArithmeticTypes = new WeakMap<ts.Node, IrType>();
/** Record fields promoted to an undefined-armed union by an indexed-read
* value. The key is the concrete emitted shape and field name. */
readonly runtimeOptionalFields = new Set<string>();
/** Callback pattern parameters carry an optional source without making
* every name destructured from a present source optional. */
readonly runtimeOptionalPatternTypes = new WeakMap<ts.Node, IrType>();
readonly runtimeOptionalReduceTypes = new WeakMap<ts.CallExpression, IrType>();
/** Capture entries and their origin share one mutable box. Normalize each
* entry to the origin so writes and flow proofs stay synchronized. */
readonly runtimeOptionalRoots = new Map<IrLocal, IrLocal>();
runtimeOptionalRootOf(local: IrLocal): IrLocal {
return this.runtimeOptionalRoots.get(local) ?? local;
}
runtimeOptionalBindingType(node: ts.Node): IrType | null;
runtimeOptionalBindingType(node: ts.Node, fallback: IrType): IrType;
runtimeOptionalBindingType(node: ts.Node, fallback?: IrType): IrType | null {
const patternType = this.runtimeOptionalPatternTypes.get(node);
if (patternType) return patternType;
const symbol = ts.isIdentifier(node) ? this.resolveValueSymbol(node) : this.checker.getSymbolAtLocation(node);
if (!symbol) return fallback ?? null;
const known = this.runtimeOptionalBindingTypes.get(symbol);
if (known) return known;
// A module-scope function alias is collected as a global before the
// optional-read fixed point settles. Derive its promoted function ABI
// directly from the aliased declaration as a final read-side hook, so
// the initializer cannot fall back to a narrowing adapter.
if (ts.isIdentifier(node) && fallback?.kind === "func") {
const decl = this.checker.valueDeclarationOf(symbol);
if (decl && ts.isVariableDeclaration(decl) && decl.initializer) {
let init: ts.Expression = decl.initializer;
while (ts.isParenthesizedExpression(init)) init = init.expression;
if (ts.isIdentifier(init)) {
const source = this.resolveValueSymbol(init);
const sig = source ? this.fnSigsBySymbol.get(source) : undefined;
if (sig) {
return {
...fallback,
params: sig.params.filter((p) => p.mode !== "dynRest" && p.mode !== "arguments").map((p) => p.type),
ret: sig.returnType,
};
}
}
}
}
return fallback ?? null;
}
runtimeOptionalIdentifierValue(node: ts.Expression): { value: IrExpr; present: IrType; unionId: string } | null {
if (!ts.isIdentifier(node) || this.chainRecvByNode.has(node)) return null;
const local = this.resolveLocal(node);
const symbol = this.resolveValueSymbol(node);
const storage = local ?? (symbol ? this.globalsBySymbol.get(symbol) : undefined);
if (!storage || storage.type.kind !== "union" || this.armTag(storage.type.unionId, UNDEFINED_T) < 0) return null;
const present = this.stripUndefinedArm(storage.type);
if (isUnitType(present)) return null;
return { value: varRef(storage.id, storage.type, locOf(node)), present, unionId: storage.type.unionId };
}
/** Recover the original optional union when a checked single-arm helper
* was installed before a strict property consumer could name its member.
* Recovery is limited to array element reads and storage roots marked by
* the optional-read analysis, so ordinary assertions keep their generic
* checked-narrow behavior. */
runtimeOptionalSourceValue(node: ts.Expression, value: IrExpr): IrExpr | null {
let origin: ts.Expression = node;
while (ts.isParenthesizedExpression(origin)) origin = origin.expression;
let optionalOrigin = ts.isElementAccessExpression(origin);
if (ts.isIdentifier(origin)) {
const local = this.resolveLocal(origin);
const root = local ? this.runtimeOptionalRootOf(local) : null;
const global = this.globalOf(origin);
optionalOrigin =
(!!root && this.runtimeOptionalStorageLocals.has(root)) ||
(!!global && (this.isRuntimeOptionalGlobal(global) ||
(global.type.kind === "union" && this.armTag(global.type.unionId, UNDEFINED_T) >= 0)));
}
if (!optionalOrigin) return null;
let source = value;
if (
value.kind === "call" &&
this.checkedNarrowHelpers.has(value.callee) &&
value.args.length === 1 &&
value.args[0]?.type.kind === "union"
) {
source = value.args[0];
}
return source.type.kind === "union" && this.armTag(source.type.unionId, UNDEFINED_T) >= 0
? source
: null;
}
/** A strict property or method receiver over an array-derived optional
* value. Reuse a plain read directly or stabilize an effectful receiver
* once, throw Node's member-read TypeError for a unit arm, and extract the
* expected present arm otherwise. */
runtimeOptionalPropertyReceiver(
node: ts.Expression,
value: IrExpr,
expected: IrType,
member: string,
): IrExpr | null {
const source = this.runtimeOptionalSourceValue(node, value);
if (source?.type.kind !== "union") return null;
const def = this.unions.get(source.type.unionId);
const valueTag = this.armTag(source.type.unionId, expected);
if (
!def ||
valueTag < 0 ||
!def.arms.every((arm) => typeEquals(arm, expected) || isUnitType(arm))
) return null;
const loc = locOf(node);
const repeatSource = isSafeToRepeat(source);
const stable = repeatSource ? null : this.declareHiddenLocal("%propertyRecv", source.type);
const stableRef = (): IrExpr => repeatSource ? source : varRef(stable!.id, source.type, loc);
let result: IrExpr = {
kind: "unionNarrow",
unionId: source.type.unionId,
tag: valueTag,
value: stableRef(),
type: expected,
loc,
};
for (let i = def.arms.length - 1; i >= 0; i--) {
const arm = def.arms[i];
if (!arm || !isUnitType(arm)) continue;
const unit = arm.kind === "nullT" ? "null" : "undefined";
result = {
kind: "ternary",
cond: {
kind: "unionIsTag",
unionId: source.type.unionId,
tag: i,
negated: false,
value: stableRef(),
type: BOOL,
loc,
},
then: nodeThrowExpr(1, "", `Cannot read properties of ${unit} (reading '${member}')`, expected, loc),
else_: result,
type: expected,
loc,
};
}
if (stable === null) return result;
return {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: stable.id, init: source, loc }],
result,
type: expected,
loc,
};
}
runtimeOptionalFieldKey(shapeId: string, field: string): string {
return `${shapeId}:${field}`;
}
isRuntimeOptionalField(shapeId: string, field: string): boolean {
return this.runtimeOptionalFields.has(this.runtimeOptionalFieldKey(shapeId, field));
}
isRuntimeOptionalGlobal(global: IrGlobal): boolean {
return this.runtimeOptionalGlobals.has(global);
}
isRuntimeOptionalArithmeticGlobal(global: IrGlobal): boolean {
return this.runtimeOptionalArithmeticGlobals.has(global);
}
/** The union produced when an unchecked array read reaches a typed slot
* whose checker type is the corresponding bare arm. */
runtimeOptionalWidening(actual: IrType, expected: IrType): IrType | null {
if (actual.kind !== "union" || this.armTag(actual.unionId, UNDEFINED_T) < 0) return null;
return typeEquals(this.stripUndefinedArm(actual), expected) ? actual : null;
}
runtimeOptionalType(t: IrType): IrType {
if (t.kind === "union") return this.armTag(t.unionId, UNDEFINED_T) >= 0 ? t : this.withUndefinedArmOf(t) ?? t;
if (t.kind === "void" || t.kind === "dyn" || t.kind === "jsval" || t.kind === "generator") return t;
return this.withUndefinedArm(t);
}
promoteRuntimeOptionalParameter(node: ts.Node, type: IrType): IrType {
const widened = this.runtimeOptionalType(type);
const symbol = this.checker.getSymbolAtLocation(node);
if (symbol) this.runtimeOptionalBindingTypes.set(symbol, widened);
return widened;
}
promoteRuntimeOptionalFunctionReturn(node: ts.Node, type: IrType): IrType {
const widened = this.runtimeOptionalType(type);
this.runtimeOptionalFunctionReturns.set(node, widened);
return widened;
}
runtimeOptionalFunctionReturnType(node: ts.Node, fallback: IrType): IrType {
return this.runtimeOptionalFunctionReturns.get(node) ?? fallback;
}
/** Promote one field of a record storage shape to the value's runtime
* type. Shapes are immutable by identity, so this interns a sibling shape
* and records the field as runtime-optional for property-read narrowing. */
runtimeOptionalRecordField(type: IrType, field: string, fieldType: IrType): IrType {
if (type.kind !== "record") return type;
const shape = this.shapes.get(type.shapeId);
if (!shape) return type;
const existing = shape.fields.find((f) => f.name === field);
if (!existing || typeEquals(existing.type, fieldType)) {
if (existing) this.runtimeOptionalFields.add(this.runtimeOptionalFieldKey(type.shapeId, field));
return type;
}
const shapeId = this.shapes.intern(
shape.fields.map((f) => ({ name: f.name, type: f.name === field ? fieldType : f.type })),
shape.tuple === true,
shape.indexValue,
shape.declaredOrder,
);
this.runtimeOptionalFields.add(this.runtimeOptionalFieldKey(shapeId, field));
return { kind: "record", shapeId };
}
/** Runs `fn` with the given aliased-typeof narrows applied (and restored
* after) — the branch-scoping primitive. */
narrowingAliases<T>(narrows: readonly { sym: ts.Symbol; tsArm: ts.Type }[], fn: () => T): T {
if (narrows.length === 0) return fn();
const saved = narrows.map((n) => [n.sym, this.aliasNarrowTypes.get(n.sym)] as const);
for (const n of narrows) this.aliasNarrowTypes.set(n.sym, n.tsArm);
try {
return fn();
} finally {
for (const [sym, old] of saved) {
if (old === undefined) this.aliasNarrowTypes.delete(sym);
else this.aliasNarrowTypes.set(sym, old);
}
}
}
/** Non-null while an instance body lowers: appended to every diagnostic
* so a body error names WHICH instantiation triggered it. */
instantiationContext: string | null = null;
/** True while re-lowering a base function's 2nd+ instance: the same source
* statements were already counted for the first instance. */
suppressStats = false;
/** Synthetic array-HOF loop functions (map/filter/forEach desugar),
* interned per method + element/callback-result type: key → fn name. */
readonly arrHofHelpers = new Map<string, string>();
/** Per-arm calls through union-typed class receivers, interned by the
* receiver union, member, completed argument ABI, and result type. */
readonly unionCallHelpers = new Map<string, string>();
/** Derived shape metadata that depends on another shape's declaration
* order. These settle before helper bodies rebuild from that metadata. */
readonly shapeOrderMetadataFinalizers: (() => void)[] = [];
/** Settled generic class-rest metadata requires the historical emit
* fallback so its fence can poison the original statement atomically. */
requiresHistoricalOrderRelower = false;
/** Helpers that snapshot shape declaration order into their bodies.
* Reachability lowers inits before the declarations they discover, so
* these rebuild after the worklist restores historical shape metadata. */
readonly shapeOrderHelperFinalizers: (() => void)[] = [];
/** Emit-override specializations (`%C.emit:<event>` — lower-event-emitter.ts's
* emit-overrides block): interned names, the drive-loop queue, and the
* currently-lowering specialization's context (the super-forward
* interception reads it). */
readonly emitSpecDone = new Set<string>();
readonly emitSpecQueue: EmitSpecRequest[] = [];
emitSpecCtx: EmitSpecCtx | null = null;
/** Width-coercion helpers (%rec.width.N / %arr.width.N), interned per
* (from, to) shape pair — see widthCoerce. */
readonly widthHelpers = new Map<string, string>();
/** (fromShape, toShape) pairs whose width plan is being computed — the
* cycle guard for RECURSIVE shapes (a self-referential record narrowing
* into a self-referential subset). Re-entering an in-progress pair
* answers "assume coercible" (the greatest fixed point: every OTHER
* constraint of the cycle is still checked by the outer call, and the
* built helper terminates because recordWidthHelper interns its name
* before building the body, so the recursive reference resolves to the
* helper itself). */
private readonly widthPlanning = new Set<string>();
/** Interned node:assert helpers (deep-equality comparisons keyed by
* typeKey, throws wrappers keyed by callback type + expected class) —
* the widthHelpers pattern with its own namespace. */
readonly assertHelpers = new Map<string, string>();
/** util.inspect's per-type traversal helpers (%util.insp.N), interned
* by typeKey — the assertHelpers pattern with its own namespace. */
readonly inspectHelpers = new Map<string, string>();
/** Union re-tag helpers (%union.retag.N), interned per (from, to)
* unionId pair — see unionRetagHelper. */
readonly retagHelpers = new Map<string, string>();
/** Callee names of every interned coercion helper whose CALL mints a
* FRESH closure per evaluation (%fn.width.*, %fn.adapt.*,
* %fnval.spawnres.*). Registered at the mint site — NOT recovered by
* name-prefix matching — because retained-FFI release identity is the
* runtime closure pointer: a coercion adapter allocates a different
* closure at the registration and release sites, so lowerFfiCall must
* refuse these forms at compile time (SC5003). Any new closure-minting
* adapter helper MUST add its name here, or the identity guard silently
* reopens and the mismatch surfaces as a runtime release trap instead. */
readonly freshClosureAdapters = new Set<string>();
/** Symbols bound by `const process = globalThis.process` (and the other
* stdlib-global snapshot spellings): pure alias plumbing — receiver
* checks resolve through this map (stdlibGlobalNameOf), declarations
* emit nothing. */
readonly stdlibGlobalAliases = new Map<ts.Symbol, string>();
/** CJS export-table ACCESSORS (`module.exports = { get path() {...} }`),
* lifted lazily as module-level functions and interned per accessor
* declaration: member reads call the getter (lower-exprs). */
readonly cjsAccessorFns = new Map<ts.Node, { fnName: string; type: IrType & { kind: "func" } }>();
readonly narrowHelpers = new Map<string, string>();
/** Exact members of narrowHelpers produced by narrowedArmHelper. This
* lets property consumers recognize an earlier checked extraction by
* provenance instead of relying on its generated-name prefix. */
private readonly checkedNarrowHelpers = new Set<string>();
/** Interned `%iter.drain.<n>` helpers (classIteratorDrainCall): one per
* receiver class — the eager drain of a class iterable's protocol into
* a fresh element array, behind array/call spreads. */
readonly iterDrainHelpers = new Map<string, string>();
/** Island-lift builder helpers (%jsin.rec.N / %jsin.arr.N /
* %jsin.elems.N), interned per source type — see jsvalLiftExpr. */
readonly jsinHelpers = new Map<string, string>();
/** Synthetic Map.forEach loop functions, interned per key/value type +
* callback arity: key → fn name (see lowerMapForEachCall). */
readonly mapHofHelpers = new Map<string, string>();
/** Synthetic Set.forEach loop functions, interned per element type +
* callback arity/return — Map's pattern. */
readonly setHofHelpers = new Map<string, string>();
/** Synthetic URLSearchParams.forEach loop functions, interned per
* callback arity/return — Map's pattern over the sp index walk. */
readonly spHofHelpers = new Map<string, string>();
/** The primitive-constructor VALUES (`String`/`Number`/`Boolean` as
* bare identifiers — CLI option tables store and compare them): one
* synthesized coercion function per constructor and typed/dyn ABI,
* interned here so references under one source-world ABI are the SAME
* zero-capture closure (see primitiveCtorClosure). */
readonly primitiveCtorFns = new Map<string, string>();
/** Builtin-module FUNCTIONS admitted as values by their surface-table
* entries. The resolved runtime function plus ABI is the identity key,
* so named imports, namespace reads, CommonJS bindings, and alias chains
* all reuse one immortal zero-capture closure. */
readonly builtinCallableValueFns = new Map<string, string>();
/** Optional-chain lowering state. While a chain body lowers, the guarded
* receiver NODE reads as a chainRecv (typed by the narrowed arm) instead
* of re-lowering, its checker type reads non-nullish (typeOf), and the
* node carrying the ?. token is marked handled so the receiver-typed
* lowerings stop declining it (chainBlocked). */
readonly chainRecvByNode = new Map<ts.Node, IrExpr>();
readonly chainNarrowedType = new Map<ts.Node, ts.Type>();
readonly chainHandled = new Set<ts.Node>();
/** for-of-over-matchAll bindings whose `.index` reads the companion-index
* array: binding SYMBOL → the hidden number[] of match start indices plus
* the hidden cursor holding THIS iteration's position (registered while
* the loop body lowers; the property path serves `m.index` as
* idxs[cur] — computed only at an actual read, so a drain row is never
* touched for bodies that ignore it). */
readonly matchAllIndexBindings = new Map<ts.Symbol, { idxsLocalId: string; curLocalId: string }>();
/** STORED matchAll drains: `const rows = s.matchAll(re)` lowers through
* matchAllInto with a hidden companion index array, registered here so a
* later `for (const m of rows)` in the SAME function serves `m.index`
* (the ctx guard keeps hidden locals out of closures — a cross-function
* walk falls back to the plain array walk and the fence). */
readonly matchAllDrainIndexes = new Map<ts.Symbol, { idxsLocalId: string; ctx: FnCtx }>();
/** STORED numeric value iterators: `const it = numbers.values()` (and
* the equivalent `[Symbol.iterator]()` spelling) over number[] or a
* represented typed array has no first-class IR value, so its statically
* known protocol state lives in hidden source/cursor/done locals. A
* later for-of in the SAME function reads and advances them.
* `doneLocalId` is sticky: once next() observes the end, later source
* changes do not revive the exhausted iterator, exactly like Node. */
readonly numericIterators = new Map<
ts.Symbol,
{ sourceLocalId: string; sourceType: IrType; indexLocalId: string; doneLocalId: string; ctx: FnCtx }
>();
chainCounter = 0;
/** Keyed by program-wide qualified class name (what IR object types carry). */
readonly classes = new Map<string, ClassInfo>();
readonly classBySymbol = new Map<ts.Symbol, ClassInfo>();
/** Inferred JS methods participating in an override chain keep a vtable
* ABI instead of call-site specialization. Filled before class collection. */
readonly virtualJsMethods = new WeakSet<ts.MethodDeclaration>();
/** The class whose members are lowering — `super` binds lexically to it
* (arrows inside methods lower within this window, so they see it too). */
currentClass: ClassInfo | null = null;
readonly globalsBySymbol = new Map<ts.Symbol, IrGlobal>();
/** Expando function members (`foo.bar = 12` on a module-level function
* or callable const): per function symbol, each written member's module
* global — string keys for spelled/folded names, ts.Symbols for
* unique-symbol keys (lower-expando.ts). */
readonly expandoMembers = new Map<ts.Symbol, Map<string | ts.Symbol, ExpandoMember>>();
/** CJS export globals ALSO key by their declaration NODE: the checker
* hands importers a distinct (late-bound) symbol for `module.exports`
* property exports — different object, same declaration — so globalOf
* falls back through the shared node (collectGlobals registers both). */
readonly globalsByDeclNode = new Map<ts.Node, IrGlobal>();
readonly globalsList: IrGlobal[] = [];
/** npm-import init statements (--dynamic), keyed by file AND import
* declaration: the island.import assignments/side-effect loads for that
* statement. lowerFileInit splices them into the importing file's %init
* header at the statement's position — Node evaluates each imported
* module (island packages included) where the import appears, so an
* `import "polyfill"` before an `import "./app.js"` runs the package
* top-level BEFORE app's init, not after. */
readonly npmInitActions = new Map<ts.SourceFile, Map<ts.Statement, IrStmt[]>>();
/** Per-file %init PRELUDE statements for JSON imports: bakeable DATA
* assignments with no observable evaluation order of their own —
* prepended by lowerFileInit so the bindings are live before any
* top-level statement runs. */
readonly jsonInitActions = new Map<ts.SourceFile, IrStmt[]>();
/** The embedded npm runtime graph (collectNpmImports), attached to the
* emitted module. Null without npm imports or without --dynamic. */
npmEmbedded: IrModule["embedded"] | null = null;
npmBuiltins: NpmBuiltinUse[] | null = null;
npmLazyTraps: NpmLazyTrap[] | null = null;
/** Dynamic `import("literal")` resolutions, keyed
* `fileName\u0000specifier` (collectDynamicImports fills it during npm
* collection; lowerDynamicImportCall reads it per site). */
readonly dynImports = new Map<string, DynamicImportResolution>();
/** createRequire-require resolutions of BARE npm specifiers, keyed
* `fileName\u0000specifier` (collectCreateRequires fills it during npm
* collection under --dynamic — the require-condition entry key plus its
* embedded format; lowerCreateRequireCall reads it per site; "" marks a
* failed resolution already reported at collection). */
readonly createRequireImports = new Map<string, { entryKey: string; format: "esm" | "cjs" | "json" } | "">();
/** Module → the name of its synthesized namespace-BUILDER function
* (lowerOwnModuleImport): every `import()` of the same program module
* shares one builder. */
readonly dynNsBuilders = new Map<ts.SourceFile, string>();
/** Parameters forced to the island-handle type (jsval) regardless of
* their checker type: then-handler params whose settled value is an
* engine handle (a dynamic import's namespace object) — paramShape's
* early-out. */
readonly jsvalParamOverrides = new Set<ts.ParameterDeclaration>();
/** Inline Promise.then parameters whose contextual type is a builtin
* module namespace anonymous object. The settled native token is the
* ABI truth even when the checker type has no stable module symbol. */
readonly moduleNsParamOverrides = new Map<ts.ParameterDeclaration, IrType & { kind: "moduleNs" }>();
/** File → qualifier prefix: "" for the entry, "%mI." otherwise. */
readonly fileTag = new Map<ts.SourceFile, string>();
/** Embedded fork roots and their private startup ids. */
readonly forkTargets: readonly ts.SourceFile[];
readonly forkTargetIdByPath = new Map<string, number>();
/** Namespace ModuleBlocks this program lowers, filled by splitFiles:
* "flattened" — an instantiated namespace whose body joined the file's
* parts (members resolve statically); "typeOnly" — a skipped
* non-instantiated one (its only value members are import= aliases,
* still resolved statically). Ambient blocks never register — their
* members keep the ReferenceError/fence paths (lower-namespaces.ts). */
readonly nsBlocks = new Map<ts.Node, "flattened" | "typeOnly">();
/** File → its %init function name, filled by prepareModuleInits before
* any body lowers: import headers and inline require statements call
* dependency inits by these names. */
readonly initNameOf = new Map<ts.SourceFile, string>();
/** CommonJS module objects are scalar handles into the runtime registry.
* IDs are assigned in deterministic module order before any body lowers;
* the feature gate stays false for programs that never inspect the graph. */
readonly cjsModuleIdOf = new Map<ts.SourceFile, number>();
readonly cjsModuleFiles: ts.SourceFile[] = [];
cjsModuleGraphEnabled = false;
/** File → the id of its run-once guard global (a bool module global,
* false at program start). Every non-entry module gets one: its %init
* may be called from several importers/requirers, and the guard is what
* makes each call after the first a Node-style cache hit. The entry has
* none — %main calls it exactly once (a dependency edge back to the
* entry would be a fenced cycle). */
readonly moduleGuardOf = new Map<ts.SourceFile, string>();
/** Files whose module evaluation is asynchronous: direct top-level
* await/for-await modules plus their static ESM importers. Their %init
* bodies run on fibers and every async dependency edge awaits the
* dependency promise before the importer body starts. Synchronous files
* stay synchronous — adding even an already-settled await would insert
* an observable microtask hop. */
readonly asyncInitFiles = new Set<ts.SourceFile>();
/** Async module → its cached evaluation-promise global. The emitted
* spawn wrapper fills this on first evaluation and returns a retained
* reference on cache hits, matching Node's one ModuleJob promise per
* module even across diamonds and concurrent dynamic imports. */
readonly modulePromiseOf = new Map<ts.SourceFile, string>();
/** Async import-cycle member → the cycle's deterministic graph
* representative. Used to recognize internal SCC edges; this is NOT
* necessarily the runtime evaluation root, because a dynamically-only
* cycle can first be entered through any member. */
readonly asyncCycleRepresentativeOf = new Map<ts.SourceFile, ts.SourceFile>();
/** Async import-cycle member → the shared completion-promise global for
* its SCC. Every member's spawn wrapper temporarily publishes its own
* promise while eager recursive evaluation unwinds; the outermost
* wrapper (the member actually requested first at runtime) writes last
* and therefore becomes the cycle's evaluation root. Dynamic imports
* wait on this shared verdict rather than a build-time-selected member. */
readonly asyncCyclePromiseOf = new Map<ts.SourceFile, string>();
/** Record-shape interner: canonical (name-sorted) field list → shapeId.
* Threaded into every mapType call; its `shapes` array becomes
* IrModule.records. */
readonly shapes = new ShapeRegistry();
/** Union interner: canonical (typeKey-sorted) arm list → unionId.
* Threaded into every mapType call; its `unions` array becomes
* IrModule.unions. An arm's index in the canonical list is its runtime
* tag. */
readonly unions = new UnionRegistry();
/** Context-free successful type mappings. See TypeMapperCtx.typeMemo. */
readonly typeMemo = new Map<string, IrType>();
readonly ambient = ambientDtsPath();
readonly overridesAmbient = overridesDtsPath();
readonly fallbackAmbient = fallbackDtsPath();
/** The one mapType context: registries + hooks, assembled in the
* constructor (typeParamResolver reads the CURRENT instantiation bindings
* through `this`, so the same ctx serves generic bodies too). */
readonly typeCtx: TypeMapperCtx;
readonly stats: LowerStats = {
statementsTotal: 0,
statementsFailed: 0,
statementsIsland: 0,
functionsSkipped: 0,
};
/** Reachability edge sink: every resolution of
* a reference to a lowerable body reports its name here — recorded even
* when the enclosing statement later poisons. */
onEdge: ((name: string) => void) | null = null;
// Stack of function contexts (bottom = the function being declared at
// top level, top = the innermost nested function currently lowering).
fnStack: FnCtx[] = [];
readonly liftedFns: IrFunction[] = [];
lambdaCounter = 0;
/** Statement lists currently mid-lowering, innermost last: the forward-
* capture machinery needs to know which later statements of an OPEN list
* a symbol's declaration sits in, which scope frame list-level declarations
* register into, and where to insert predeclarations before closures or
* earlier source statements need them. */
readonly activeStmtLists: {
stmts: readonly ts.Statement[];
index: number;
ctx: FnCtx;
frame: Map<ts.Symbol, IrLocal>;
out: IrStmt[];
}[] = [];
/** Forward-captured bindings pre-declared as TDZ boxes, keyed by symbol:
* lowerVarDecl consumes the entry when the source declaration arrives and
* emits the initializing `assign` instead of a fresh declaration. */
readonly tdzPredeclared = new Map<ts.Symbol, IrLocal>();
/** Nested function DECLARATIONS lowered eagerly by the forward-hoisting
* machinery (predeclareForwardFnDecl — a reference above the declaration
* in the same function, JS's function hoisting): the statement loop skips
* the source statement when it arrives. */
readonly hoistedFnDecls = new Set<ts.FunctionDeclaration>();
/** `var` bindings hoisted to their function root (hoistVarBinding), keyed
* by the checker's merged symbol — every same-name `var` in one function
* is one symbol, so one slot. Module-scope vars live in globalsBySymbol
* instead. */
readonly hoistedVars = new Map<ts.Symbol, IrLocal>();
/** Per-file `var` module globals whose type carries an undefined arm:
* lowerFileInit assigns them the interned undefined right after the
* run-once guard — JS hoists module vars to `undefined` at entry, so a
* function called above the declaration statement reads that, never a
* NULL slot. Filled by collectGlobals. */
readonly varGlobalEntryInits = new Map<ts.SourceFile, IrGlobal[]>();
get ctx(): FnCtx {
const top = this.fnStack[this.fnStack.length - 1];
if (!top) throw new InternalCompilerError("lowerer bug: no active function context");
return top;
}
get scopes(): Map<ts.Symbol, IrLocal>[] {
return this.ctx.scopes;
}
/** Names of bodies a prior reachability pass reached; null lowers everything. */
readonly reachable: ReadonlySet<string> | null;
/** Reachability computed by this same Lowerer when retained worklist IR
* is assembled directly. The configured reachable set remains null so
* demand-driven instance lowering keeps its existing gates. */
reachableForArtifacts: ReadonlySet<string> | null = null;
/** Coverage remainder mode: the reachability gate inverts (see wantBody)
* and no module is built. */
readonly remainder: boolean;
/** Deferred collection diagnostics (failed signatures/class shapes) by
* declaration symbol: an unreached declaration must not fail the build,
* so its diagnostics wait until a reference makes them relevant. */
readonly deferredDiags = new Map<ts.Symbol, ScrDiagnostic[]>();
/** Deferred classes by qualified IR name — for flush sites that only
* know the class name (typed receivers, module class retention). */
readonly deferredClassByName = new Map<string, ts.Symbol>();
/** Symbols whose deferred diagnostics THIS pass flushed (handed to the
* coverage remainder as alreadyFlushed). */
readonly flushedSymbols = new Set<ts.Symbol>();
readonly alreadyFlushed: ReadonlySet<ts.Symbol>;
/** The build's target platform ("win32" | "darwin" | "linux" | ...):
* selects the platform-keyed builtin surfaces (builtinModuleFnsOf /
* builtinModuleConstOf in surfaces.ts). */
readonly targetPlatform: string;
/** LowererMode.startupCrash — buildMain opens %main with the throw. */
readonly startupCrash: StartupCrash | null;
/** Outbound native bindings by their source-level ambient name. */
readonly ffiImports: readonly IrFfiImport[];
readonly libraryCallbacks: boolean;
readonly ffiImportsByName: ReadonlyMap<string, IrFfiImport>;
/** Non-null after whole-program FFI declaration validation. */
readonly ffiBindingSymbols: ReadonlyMap<string, ReadonlySet<ts.Symbol>> | null;
/** Exact specifier mappings and their reverse declaration-file lookup. */
readonly externalTypes: ReadonlyMap<string, string>;
readonly externalTypeSpecifiersByFile: ReadonlyMap<string, readonly string[]>;
/** Symbols a POISONED declaration statement would have bound: the
* declaration's own diagnostic is already recorded, and no local/global
* registered, so later references fall through every resolution step —
* the fallthroughs report the inherited-blocker cascade (SC2004)
* instead of misattributing the reference. */
readonly blockedBindings = new Set<ts.Symbol>();
/** True while collectProgram runs: resolution helpers must not flush
* deferred diagnostics (collection itself resolves symbols — extends
* clauses — and collection order must not decide what reports). */
collecting = false;
/** Non-null redirects pushDiag into a capture buffer (the deferred
* collection wrapper). */
diagSink: ScrDiagnostic[] | null = null;
/** Diagnostics converted into runtimeFence statements (JS sources —
* see lowerStmts): off the build, preserved here so coverage reporting
* can still name every deferred fence. */
readonly runtimeFences: ScrDiagnostic[] = [];
/** --provenance-sources: diagnostics of ELIDED pure-annotated dead
* consts in fetched source modules (lowerStmts's elision rule) — off
* the build entirely (the statement lowers to its poisoned bindings and
* nothing throws), preserved for the coverage report's provenance
* section. */
readonly provenanceElided: ScrDiagnostic[] = [];
/** --provenance-sources: per-file statement attribution (mirrors the
* stats counters, keyed by fileName) so the coverage report can answer
* "did the PACKAGE's statements compile static?" per provenance
* package. Only populated while the registry is active; the remainder
* pass skips it (attribution describes the build). */
readonly statsByFile = new Map<string, { total: number; failed: number; island: number }>();
/** Bumps the per-file attribution counter (no-op unless provenance is
* active and this is the emit/discovery lane — mirror the CALLER's
* suppressStats guard, this method only gates remainder). */
bumpFileStat(file: string, kind: "total" | "failed" | "island"): void {
if (this.remainder || !provenanceActive()) return;
let s = this.statsByFile.get(file);
if (!s) this.statsByFile.set(file, (s = { total: 0, failed: 0, island: 0 }));
s[kind]++;
}
constructor(
readonly program: ts.Program,
readonly entry: ts.SourceFile,
readonly moduleOrder: ts.SourceFile[],
readonly dynamic: boolean,
mode: LowererMode = {},
) {
this.reachable = mode.reachable ?? null;
this.remainder = mode.remainder ?? false;
this.alreadyFlushed = mode.alreadyFlushed ?? new Set();
this.targetPlatform = mode.targetPlatform ?? process.platform;
this.startupCrash = mode.startupCrash ?? null;
this.ffiImports = mode.ffiImports ?? [];
this.libraryCallbacks = mode.libraryCallbacks ?? false;
this.forkTargets = mode.forkTargets ?? [];
this.forkTargets.forEach((sf, id) => {
this.forkTargetIdByPath.set(tsgoPath(resolve(sf.fileName)), id);
});
this.ffiImportsByName = new Map(this.ffiImports.map((entry) => [entry.name, entry]));
this.ffiBindingSymbols = mode.ffiBindingSymbols ?? null;
this.externalTypes = mode.externalTypes ?? new Map();
this.externalTypeSpecifiersByFile = mode.externalTypeSpecifiersByFile ??
directExternalTypeSpecifiersByFile(this.externalTypes);
this.checker = program.getTypeChecker();
this.typeCtx = {
checker: this.checker,
shapes: this.shapes,
unions: this.unions,
classNamer: this.classNamer,
resolveTypeParam: this.typeParamResolver,
resolveTypeParamTs: this.typeParamTsResolver,
genericClassInstance: (decl, ref) => this.genericClassInstanceType(decl, ref),
mixinClassInstance: (decl) =>
this.mixinTypeContext && this.mixinTypeContext.classNode === decl
? { kind: "object", className: this.mixinTypeContext.className }
: null,
mixinIntersectionInstance: (widened) => mixinIntersectionInstanceType(this, widened),
isStdlibFile: this.isStdlibFile,
isNpmFile: this.isNpmFile,
isExternalTypeFile: (sf) =>
this.externalTypeSpecifiersByFile.has(tsgoPath(resolve(sf.fileName))),
dynamic: this.dynamic,
typeMemo: this.typeMemo,
canMemoizeType: () =>
this.typeParamBindings === null &&
this.typeParamTsBindings === null &&
this.mixinTypeContext === null,
// fileTag is filled just below; the hook is only ever CALLED during
// lowering, long after the constructor completes.
isProgramFile: (sf) => this.fileTag.has(sf),
moduleNamespaceId: (type) => this.moduleNamespaceIdOfType(type),
};
// Modules reachable only through literal import() joined
// moduleOrder BEFORE any pass constructed — lowerToIr runs
// appendDynamicImportModules once on the shared array (a per-pass run
// here would repeatedly extend the graph and duplicate cycle reports).
this.moduleOrder.forEach((sf, i) => {
this.fileTag.set(sf, sf === entry ? "" : `%m${i}.`);
});
if (this.moduleOrder.length === 0) this.fileTag.set(entry, "");
this.registerBuiltinErrorClasses();
registerBuiltinEmitterClass(this);
registerBuiltinStreamClasses(this);
}
registerBuiltinErrorClasses(): void {
return registerBuiltinErrorClasses(this);
}
/** The nominal identity of a checker module-namespace type. Program
* modules use their normalized absolute source name; supported builtins
* use the lowering tables' canonical bare name. */
moduleNamespaceIdOfType(type: ts.Type): string | null {
let sym = type.getSymbol();
if (!sym) return null;
if (sym.flags & ts.SymbolFlags.Alias) sym = this.checker.getAliasedSymbol(sym);
for (const decl of this.checker.declarationsOf(sym)) {
if (
ts.isSourceFile(decl) && !decl.isDeclarationFile &&
!decl.fileName.endsWith(".cts") && !isCjsJsFile(decl) &&
this.moduleOrder.includes(decl)
) {
return `file:${tsgoPath(resolve(decl.fileName))}`;
}
if (ts.isModuleDeclaration(decl) && ts.isStringLiteral(decl.name)) {
const builtin = canonicalBuiltinModule(decl.name.text);
if (builtin !== null) return `builtin:${builtin}`;
}
}
return null;
}
sourceFileOfModuleNamespace(type: IrType): ts.SourceFile | null {
if (type.kind !== "moduleNs" || !type.moduleId.startsWith("file:")) return null;
const path = type.moduleId.slice("file:".length);
return this.moduleOrder.find((sf) => tsgoPath(resolve(sf.fileName)) === path) ?? null;
}
builtinErrorInfoOf(symbol: ts.Symbol | null | undefined): ClassInfo | null {
return builtinErrorInfoOf(this, symbol);
}
builtinEmitterInfoOf(symbol: ts.Symbol | null | undefined): ClassInfo | null {
return builtinEmitterInfoOf(this, symbol);
}
builtinStreamInfoOf(symbol: ts.Symbol | null | undefined): ClassInfo | null {
return builtinStreamInfoOf(this, symbol);
}
/** Program-wide qualified name for a top-level declaration. */
qualify(sf: ts.SourceFile, name: string): string {
return `${this.fileTag.get(sf) ?? ""}${name}`;
}
/** The IR name mapType gives class instance types — must agree with
* collectClassShape's registration. Namespace-nested classes carry the
* namespace path (nsPathPrefix), so `namespace A { export class C }`
* and a top-level `class C` never collide. Class EXPRESSIONS name by
* SOURCE POSITION (`%cx<start>.<name>`): deterministic across the
* builds (no counter can drift between invocations),
* program-unique through the file qualifier, and collision-free with
* user identifiers ('%'). */
readonly classNamer = (decl: ts.ClassLikeDeclaration): string =>
ts.isClassExpression(decl)
? this.qualify(decl.getSourceFile(), `%cx${decl.getStart()}.${decl.name?.text ?? ""}`)
: this.qualify(decl.getSourceFile(), nsPathPrefix(decl) + (decl.name ? decl.name.text : "%anon"));
/** Follows import aliases to the original declaration's symbol. Every
* value reference resolves through here, so it doubles as the flush
* point for deferred collection diagnostics: resolving a reference to a
* broken declaration reports what collection deferred. */
resolveValueSymbol(ident: ts.Identifier): ts.Symbol | null {
let symbol = this.checker.getSymbolAtLocation(ident);
// A shorthand property's NAME resolves to the property symbol; the
// VALUE binding it reads is the checker's shorthand-value symbol
// (the option-object parsers lower `{ cwd }` through the identifier).
if (ident.parent && ts.isShorthandPropertyAssignment(ident.parent) && ident.parent.name === ident) {
symbol = this.checker.getShorthandAssignmentValueSymbol(ident.parent) ?? symbol;
}
// createRequire's return type is intentionally `any`, so tsgo does not
// make the binding an import alias. When the target replaces
// module.exports with one represented scalar/function/class value,
// reconnect the binding to that export symbol explicitly; ordinary
// identifier/call/new/global paths then apply unchanged.
const createdWhole = this.createRequireWholeExportSymbolOf(ident);
if (createdWhole) symbol = createdWhole;
const createdMember = this.createRequireDestructuredExportSymbolOf(ident);
if (createdMember) symbol = createdMember;
// tsgo synthesizes no expando symbol at a CJS MEMBER-EXPORT use site
// (`common.GREETING` where the exporter attached GREETING with
// `module.exports.GREETING = ...` — 5.9.3 answered the expando
// property symbol here), but the exporter's MODULE symbol still
// carries the member in its exports table; resolve through it so both
// ends of the export key one symbol identity, like 5.9.3's.
if (ident.parent && ts.isPropertyAccessExpression(ident.parent) && ident.parent.name === ident) {
const recv = ident.parent.expression;
if (ts.isIdentifier(recv) && this.cjsLocalModuleBindingOf(recv)) {
const dep = this.localModuleBindingDepOf(recv);
const exported = dep ? this.cjsModuleExportSymbol(dep, ident.text) : undefined;
// A createRequire call is typed as `any`, so tsgo can manufacture a
// transient property symbol with no relation to the target module.
// Prefer the real export-table symbol whenever the receiver is a
// proven static module binding; ordinary direct require aliases
// resolve to the same identity.
if (exported) symbol = exported;
}
}
if (!symbol) return null;
// Bare references across MERGED-namespace blocks fence here (Node's
// transform throws ReferenceError where tsc's emit would qualify —
// lower-namespaces.ts); a no-op for programs without namespaces.
fenceCrossBlockNsRef(this, ident, symbol);
if (symbol.flags & ts.SymbolFlags.Alias) {
// SNAPSHOT aliases own storage keyed by the PRE-alias symbol
// (`import x = N.y` of a mutable target — collectGlobals): the
// reference reads the snapshot, never the live target, exactly like
// Node's emitted `var x = N.y`. Only those aliases register this
// way; every other alias resolves through to its declaration.
if (this.globalsBySymbol.has(symbol)) {
this.flushDeferred(symbol);
return symbol;
}
// DEFAULT-SNAPSHOT storage lives on the EXPORTER'S default alias
// symbol (`export default someLet` — collectGlobals registers the
// Node-semantics snapshot there). getAliasedSymbol would resolve
// PAST it to the live let; walk the default-import hops and stop at
// the first default symbol carrying storage instead.
const snap = this.defaultSnapshotSymbolOf(symbol);
if (snap) {
this.flushDeferred(snap);
return snap;
}
symbol = this.checker.getAliasedSymbol(symbol);
}
// CommonJS export plumbing: a binding that resolved to a PROPERTY of a
// top-level `module.exports = { ... }` literal (shorthand, or a plain
// identifier value — renames included) re-resolves to the local
// declaration the property references. The export table is then pure
// alias plumbing, exactly like an ESM export list: importers land on
// the original function/const/class symbols and every existing
// registry (globals, fn signatures, classes) applies unchanged.
const cjsValue = this.cjsExportValueSymbol(symbol);
if (cjsValue) symbol = cjsValue;
this.flushDeferred(symbol);
return symbol;
}
/** Whether this exact direct call belongs to a manifest-validated native
* binding. Declaration classification uses this as a probe before normal
* expression lowering: resolving must not flush deferred diagnostics or
* trigger the merged-namespace fence just because it is asking ownership.
* Call lowering remains the authority for every ABI and call-shape
* diagnostic once this answers true. */
ownsFfiCall(expr: ts.CallExpression): boolean {
if (!ts.isIdentifier(expr.expression)) return false;
const binding = this.ffiImportsByName.get(expr.expression.text);
if (binding === undefined || this.ffiBindingSymbols === null) return false;
const validSymbols = this.ffiBindingSymbols.get(binding.name);
if (validSymbols === undefined) return false;
const wasCollecting = this.collecting;
this.collecting = true;
try {
const symbol = this.resolveValueSymbol(expr.expression);
return symbol !== null && validSymbols.has(symbol);
} finally {
this.collecting = wasCollecting;
}
}
/** The configured external host module owning an expression's runtime
* value, or null. Alias chains are followed to their declaration file so
* direct imports and local re-export facades classify identically. Type
* references never call this helper and remain ordinary checker input. */
externalTypeSpecifierOf(expr: ts.Expression): string | null {
if (this.externalTypes.size === 0) return null;
let value: ts.Expression = expr;
while (
ts.isParenthesizedExpression(value) ||
ts.isAsExpression(value) ||
ts.isTypeAssertion(value) ||
ts.isNonNullExpression(value)
) {
value = value.expression;
}
if (ts.isCallExpression(value)) {
if (value.expression.kind === ts.SyntaxKind.ImportKeyword) {
const spec = value.arguments[0];
return spec !== undefined && ts.isStringLiteralLike(spec) && this.externalTypes.has(spec.text)
? spec.text
: null;
}
if (
ts.isIdentifier(value.expression) &&
value.expression.text === "require" &&
value.arguments.length === 1
) {
const spec = value.arguments[0]!;
if (ts.isStringLiteralLike(spec) && this.externalTypes.has(spec.text)) return spec.text;
}
return this.externalTypeSpecifierOf(value.expression);
}
if (ts.isNewExpression(value)) return this.externalTypeSpecifierOf(value.expression);
if (ts.isTaggedTemplateExpression(value)) {
return this.externalTypeSpecifierOf(value.tag);
}
if (ts.isPropertyAccessExpression(value) || ts.isElementAccessExpression(value)) {
// Follow the runtime RECEIVER, not the property's declaration: a
// project-owned record may use an interface declared by the mapped
// file and remains ordinary static data (`const x: HostType = ...;
// x.field`). Only a value rooted in the imported module is external.
const receiver = this.externalTypeSpecifierOf(value.expression);
if (receiver !== null) return receiver;
const member = ts.isPropertyAccessExpression(value)
? value.name.text
: value.argumentExpression !== undefined && ts.isStringLiteralLike(value.argumentExpression)
? value.argumentExpression.text
: null;
return member !== null
? this.externalTypeSpecifierOfNamespaceMember(value.expression, member)
: null;
}
if (!ts.isIdentifier(value)) return null;
return this.externalTypeSpecifierOfSymbol(this.checker.getSymbolAtLocation(value));
}
/** The source file a checker-resolved module-specifier node names. The
* checker path is preferred; resolveImport is the canonical fallback for
* every project-module spelling. */
private moduleSourceFileOf(from: ts.SourceFile, spec: ts.StringLiteral): ts.SourceFile | null {
const moduleSymbol = this.checker.getSymbolAtLocation(spec);
for (const decl of moduleSymbol ? this.checker.declarationsOf(moduleSymbol) : []) {
if (ts.isSourceFile(decl)) return decl;
}
return resolveImport(this.program, from, spec.text);
}
/** Follow one project-module export through the checker's resolved export
* table, then recover its exact external route where alias declarations
* retain one. */
private externalTypeSpecifierOfModuleExport(
sf: ts.SourceFile,
exportName: string,
seenSymbols: Set<ts.Symbol>,
seenExports: Set<string>,
): string | null {
const exportKey = `${tsgoPath(resolve(sf.fileName))}\0${exportName}`;
if (seenExports.has(exportKey)) return null;
seenExports.add(exportKey);
// Ask the checker which symbol the module ACTUALLY exports under this
// name. Syntax-only `export *` scanning cannot answer shadowing: a local
// or explicit export wins over a same-named star export, and a star
// contributes only names its target really exports. The resolved symbol
// retains route-aware ExportSpecifier/NamespaceExport declarations for
// exact mappings, while star exports resolve to the mapped declaration
// owner through externalTypeSpecifiersByFile.
const moduleSymbol = this.checker.getSymbolAtLocation(sf);
const exported = moduleSymbol?.getExports().get(exportName as ts.__String);
return this.externalTypeSpecifierOfSymbol(exported, seenSymbols, seenExports);
}
private externalTypeSpecifierOfNamespaceMember(expr: ts.Expression, member: string): string | null {
let value = expr;
while (
ts.isParenthesizedExpression(value) ||
ts.isAsExpression(value) ||
ts.isTypeAssertion(value) ||
ts.isNonNullExpression(value)
) {
value = value.expression;
}
if (!ts.isIdentifier(value)) return null;
const symbol = this.checker.getSymbolAtLocation(value);
const namespaceDecl = symbol
? this.checker.declarationsOf(symbol).find(ts.isNamespaceImport)
: undefined;
if (namespaceDecl === undefined) return null;
const importDecl = namespaceDecl.parent.parent;
if (!ts.isImportDeclaration(importDecl) || !ts.isStringLiteral(importDecl.moduleSpecifier)) return null;
if (this.externalTypes.has(importDecl.moduleSpecifier.text)) return importDecl.moduleSpecifier.text;
const dep = this.moduleSourceFileOf(importDecl.getSourceFile(), importDecl.moduleSpecifier);
return dep !== null && !dep.isDeclarationFile
? this.externalTypeSpecifierOfModuleExport(dep, member, new Set(), new Set())
: null;
}
private externalTypeSpecifierOfSymbol(
symbol: ts.Symbol | undefined,
seenSymbols: Set<ts.Symbol> = new Set(),
seenExports: Set<string> = new Set(),
): string | null {
if (symbol === undefined || seenSymbols.has(symbol)) return null;
seenSymbols.add(symbol);
const declarations = this.checker.declarationsOf(symbol);
// Route-aware alias hops run before declaration-file ownership. An
// exact import must keep the specifier it actually named, rather than
// inheriting whichever alias happened to register the shared file last.
for (const decl of declarations) {
let specNode: ts.Expression | undefined;
let importedName: string | null = null;
if (ts.isImportSpecifier(decl)) {
const importDecl: ts.Node = decl.parent.parent.parent;
if (ts.isImportDeclaration(importDecl)) specNode = importDecl.moduleSpecifier;
importedName = (decl.propertyName ?? decl.name).text;
} else if (ts.isImportClause(decl)) {
if (ts.isImportDeclaration(decl.parent)) specNode = decl.parent.moduleSpecifier;
importedName = "default";
} else if (ts.isNamespaceImport(decl)) {
const importDecl: ts.Node = decl.parent.parent;
if (ts.isImportDeclaration(importDecl)) specNode = importDecl.moduleSpecifier;
importedName = null;
} else if (ts.isExportSpecifier(decl)) {
const exportDecl: ts.Node = decl.parent.parent;
if (ts.isExportDeclaration(exportDecl)) specNode = exportDecl.moduleSpecifier;
importedName = (decl.propertyName ?? decl.name).text;
} else if (ts.isNamespaceExport(decl)) {
const exportDecl: ts.Node = decl.parent;
if (ts.isExportDeclaration(exportDecl)) specNode = exportDecl.moduleSpecifier;
importedName = "*";
} else {
continue;
}
if (specNode === undefined || !ts.isStringLiteral(specNode)) continue;
if (this.externalTypes.has(specNode.text)) return specNode.text;
// A namespace OBJECT from a project module is not wholly external;
// property accesses resolve their selected member separately above.
if (importedName === null) return null;
const dep = this.moduleSourceFileOf(decl.getSourceFile(), specNode);
return dep !== null && !dep.isDeclarationFile
? this.externalTypeSpecifierOfModuleExport(dep, importedName, seenSymbols, seenExports)
: null;
}
for (const decl of declarations) {
const owners = this.externalTypeSpecifiersByFile.get(
tsgoPath(resolve(decl.getSourceFile().fileName)),
);
if (owners !== undefined && owners.length > 0) return owners[0]!;
}
if ((symbol.flags & ts.SymbolFlags.Alias) === 0) return null;
return this.externalTypeSpecifierOfSymbol(
this.checker.getAliasedSymbol(symbol),
seenSymbols,
seenExports,
);
}
/** The default-snapshot storage symbol a DEFAULT-import alias chain
* lands on, or null. A mutable entity-name default (`export default
* someLet`) registers its Node-semantics snapshot global under the
* exporter's default ALIAS symbol; the checker's getAliasedSymbol
* resolves through that symbol to the live let, so this walk follows
* the default hops syntactically — default import clauses, `{ default
* as x }` specifiers, `export { default } from` re-exports — and stops
* at the first default symbol carrying registered storage. Local
* `export { x as default }` specifiers (no module specifier) are LIVE
* bindings in Node and fall through to ordinary alias resolution. */
private defaultSnapshotSymbolOf(alias: ts.Symbol): ts.Symbol | null {
let sym: ts.Symbol | undefined = alias;
for (let hop = 0; sym !== undefined && hop < 32; hop++) {
if (hop > 0 && sym.flags & ts.SymbolFlags.Alias && this.globalsBySymbol.has(sym)) return sym;
const d = this.checker
.declarationsOf(sym)
.find((x) => ts.isImportClause(x) || ts.isImportSpecifier(x) || ts.isExportSpecifier(x));
let spec: ts.Expression | undefined;
let name: string | undefined;
if (d && ts.isImportClause(d) && ts.isImportDeclaration(d.parent)) {
spec = d.parent.moduleSpecifier;
name = "default";
} else if (d && ts.isImportSpecifier(d)) {
const idecl: ts.Node = d.parent.parent.parent;
if (ts.isImportDeclaration(idecl)) spec = idecl.moduleSpecifier;
name = (d.propertyName ?? d.name).text;
} else if (d && ts.isExportSpecifier(d)) {
const edecl: ts.Node = d.parent.parent;
if (ts.isExportDeclaration(edecl)) spec = edecl.moduleSpecifier;
name = (d.propertyName ?? d.name).text;
}
if (d === undefined || spec === undefined || !ts.isStringLiteral(spec) || name !== "default") return null;
const dep = resolveImport(this.program, d.getSourceFile(), spec.text);
if (!dep) return null;
sym = defaultExportSymbolOf(this, dep) ?? undefined;
}
return null;
}
/** A module's CJS export-table member symbol by NAME (the checker's
* module-symbol exports map — present in tsgo even where no expando
* property symbol exists at the attachment/use sites). */
cjsModuleExportSymbol(sf: ts.SourceFile, name: string): ts.Symbol | undefined {
const moduleSym = this.checker.getSymbolAtLocation(sf);
const exports = moduleSym?.getExports();
const direct = exports?.get(name as ts.__String);
if (direct) return direct;
// tsgo represents `module.exports = { ... }` as one `export=` symbol
// and does not duplicate the object literal's properties into the
// module export map. Resolve through that root type so createRequire's
// any-typed namespace can recover the same property symbols an
// ordinary direct require receives from checker inference.
const root = exports?.get("export=" as ts.__String);
return root ? this.checker.getPropertyOfType(this.checker.getTypeOfSymbol(root), name) : undefined;
}
/** The local VALUE symbol behind a CJS export-table property symbol —
* see resolveValueSymbol. Null when `symbol` is not such a property (or
* the property's value is not a plain identifier reference). */
private cjsExportValueSymbol(symbol: ts.Symbol): ts.Symbol | null {
const d = this.checker.declarationsOf(symbol)[0];
if (!d) return null;
// MEMBER-form class exports (`exports.C = C` — commander's error.js):
// alias plumbing exactly like a table entry, so importers land on the
// class declaration and the class registry applies unchanged (a class
// VALUE global would fence — builtin-derived classes have no
// first-class value form). Only CLASS targets re-resolve this way;
// every other member export keeps its snapshot storage semantics.
const memberClass = this.cjsMemberExportClassSymbol(d);
if (memberClass) return memberClass;
const isShorthand = ts.isShorthandPropertyAssignment(d);
const isIdentProp = ts.isPropertyAssignment(d) && ts.isIdentifier(d.initializer);
if (!isShorthand && !isIdentProp) return null;
if (!ts.isObjectLiteralExpression(d.parent) || !isCjsExportTableLiteral(d.parent)) return null;
let value = isShorthand
? this.checker.getShorthandAssignmentValueSymbol(d)
: this.checker.getSymbolAtLocation((d as ts.PropertyAssignment).initializer as ts.Identifier);
if (!value) return null;
if (value.flags & ts.SymbolFlags.Alias) value = this.checker.getAliasedSymbol(value);
return value;
}
/** The CLASS symbol a member-form CJS export declaration forwards to:
* `d` (an export property symbol's declaration) sits in a top-level
* `exports.C = <ident>` / `module.exports.C = <ident>` statement of a
* JS module, the statement is not discarded by a later table, and the
* identifier resolves to a class declaration. Null otherwise. */
cjsMemberExportClassSymbol(d: ts.Node): ts.Symbol | null {
const assign = ts.isBinaryExpression(d)
? d
: ts.isPropertyAccessExpression(d) && d.parent !== undefined && ts.isBinaryExpression(d.parent)
? d.parent
: null;
if (!assign || assign.operatorToken.kind !== ts.SyntaxKind.EqualsToken) return null;
if (!ts.isIdentifier(assign.right)) return null;
const stmt = assign.parent;
if (!stmt || !ts.isExpressionStatement(stmt) || !ts.isSourceFile(stmt.parent)) return null;
if (!isJsSourceFile(stmt.parent)) return null;
const cjs = cjsExportAssignmentOf(stmt);
if (cjs?.kind !== "member" || cjs.expr !== assign) return null;
if (cjsExportDiscardReason(stmt) !== null) return null;
let value = this.checker.getSymbolAtLocation(assign.right);
if (!value) return null;
if (value.flags & ts.SymbolFlags.Alias) value = this.checker.getAliasedSymbol(value);
const isClass = this.checker
.declarationsOf(value)
.some((decl) => ts.isClassDeclaration(decl));
return isClass ? value : null;
}
/** The CommonJS JS module a DEFAULT-import binding's declaration loads
* (`import d from "./lib.cjs"`), or null: Node's ESM-CJS interop binds
* the default to module.exports — exactly a require binding — so those
* bindings ride the CJS namespace machinery below. ESM dependencies
* (any .ts, ESM-syntax .js/.mjs) answer null and keep the ESM default
* machinery. */
private cjsDefaultImportDepOf(clause: ts.ImportClause): ts.SourceFile | null {
const importDecl = clause.parent;
if (!ts.isImportDeclaration(importDecl) || !ts.isStringLiteral(importDecl.moduleSpecifier)) {
return null;
}
const spec = importDecl.moduleSpecifier.text;
// Project aliases/self-references resolve through the same entry point as
// relative imports; an opted-in --npm-static package is the fallback.
const dep = resolveImport(this.program, importDecl.getSourceFile(), spec) ??
npmStaticDepSf7(this.program, importDecl.getSourceFile(), spec);
if (!dep || !isJsSourceFile(dep) || isNodeEsmFile(dep)) return null;
return dep;
}
/** The compiled module behind a require-like namespace binding: an
* ordinary CommonJS require declaration, a canonical createRequire
* declaration, or an ESM default import of CommonJS. One resolver keeps
* member-symbol fallback and namespace classification in lockstep. */
private localModuleBindingDepOf(expr: ts.Identifier): ts.SourceFile | null {
const sym = this.checker.getSymbolAtLocation(expr);
const decls = sym ? this.checker.declarationsOf(sym) : [];
const decl = decls.find(ts.isImportClause) ?? decls[0];
if (!decl) return null;
if (ts.isImportClause(decl)) return this.cjsDefaultImportDepOf(decl);
if (!ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name) || !decl.initializer) return null;
const directSpec = requireSpecOf(decl.initializer);
if (directSpec !== null) {
return resolveImport(this.program, decl.getSourceFile(), directSpec) ??
npmStaticDepSf7(this.program, decl.getSourceFile(), directSpec);
}
return createRequireProgramModuleOf(this, decl.initializer)?.dep ?? null;
}
/** The represented single value behind a createRequire binding, or null
* when the target is a namespace/table module. Collection has already
* registered executable exports before entry-module uses lower, so the
* registries are the authority for whether the export= value exists in
* static storage or callable/class metadata. */
private createRequireWholeExportSymbolOf(ident: ts.Identifier): ts.Symbol | null {
const binding = this.checker.getSymbolAtLocation(ident);
const decl = binding ? this.checker.declarationsOf(binding).find(ts.isVariableDeclaration) : undefined;
if (!decl || !ts.isIdentifier(decl.name) || !decl.initializer) return null;
const target = createRequireProgramModuleOf(this, decl.initializer);
if (target === null) return null;
const moduleSym = this.checker.getSymbolAtLocation(target.dep);
let symbol = moduleSym?.getExports().get("export=" as ts.__String);
if (!symbol) return null;
const wholeAssignment = this.checker.declarationsOf(symbol).find((d): d is ts.BinaryExpression => {
if (!ts.isBinaryExpression(d) || !ts.isExpressionStatement(d.parent) || !ts.isSourceFile(d.parent.parent)) return false;
const exported = cjsExportAssignmentOf(d.parent);
return exported?.kind === "table" && exported.obj === null && cjsExportDiscardReason(d.parent) === null;
});
if (wholeAssignment === undefined) return null;
let rhs: ts.Expression = wholeAssignment.right;
while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression;
if (ts.isIdentifier(rhs)) {
const value = this.checker.getSymbolAtLocation(rhs);
if (value) {
const resolved = value.flags & ts.SymbolFlags.Alias ? this.checker.getAliasedSymbol(value) : value;
return resolved;
}
}
if (symbol.flags & ts.SymbolFlags.Alias) symbol = this.checker.getAliasedSymbol(symbol);
symbol = this.cjsExportValueSymbol(symbol) ?? symbol;
return symbol;
}
/** The target export behind `const { member: local } = require("...")`
* through createRequire. The binding is import-style alias plumbing;
* plain identifiers, renames, and string-literal property names share the
* target module's real export symbol. */
private createRequireDestructuredExportSymbolOf(ident: ts.Identifier): ts.Symbol | null {
const binding = this.checker.getSymbolAtLocation(ident);
const element = binding ? this.checker.declarationsOf(binding).find(ts.isBindingElement) : undefined;
if (!element || element.name === undefined || !ts.isIdentifier(element.name) || !ts.isObjectBindingPattern(element.parent)) return null;
const declaration = element.parent.parent;
if (!ts.isVariableDeclaration(declaration) || !declaration.initializer) return null;
const target = createRequireProgramModuleOf(this, declaration.initializer);
if (target === null) return null;
const property = element.propertyName;
const name = property && (ts.isIdentifier(property) || ts.isStringLiteralLike(property))
? property.text
: ident.text;
return this.cjsModuleExportSymbol(target.dep, name) ?? null;
}
/** True when `expr` is an identifier bound by a top-level
* `const x = require("./local")` of a project module — relative,
* tsconfig-aliased, or package.json-mediated — or of a bare specifier
* naming an opted-in --npm-static package (its CJS entry is a program
* module, so the binding is the same namespace over the same export table), or by a
* DEFAULT import of a CommonJS JS module (`import d from "./lib.cjs"`:
* Node binds d to module.exports, the same value require answers).
* Member accesses on it resolve through the export table (property
* symbols → resolveValueSymbol); the bare value keeps the
* namespace-object fence, like ESM namespace imports of builtins. */
cjsLocalModuleBindingOf(expr: ts.Expression): boolean {
if (!ts.isIdentifier(expr)) return false;
const sym = this.checker.getSymbolAtLocation(expr);
if (this.localModuleBindingDepOf(expr) === null) return false;
if (this.createRequireWholeExportSymbolOf(expr) !== null) return false;
// SINGLE-VALUE exporters (`module.exports = Countdown` / `= double` /
// `= 42`): the requirer's binding IS the exported value, not a
// namespace over an export table — the alias resolves straight to the
// class/function/const declaration (or the scalar export= statement)
// and every ordinary identifier path applies (new, calls, bare value).
// Exported-const TABLES (a VariableDeclaration whose initializer is
// the object literal) keep the namespace reading — member accesses
// resolve through the table's property symbols.
if (sym && sym.flags & ts.SymbolFlags.Alias) {
const d = this.checker.declarationsOf(this.checker.getAliasedSymbol(sym))[0];
if (d && (ts.isClassDeclaration(d) || ts.isFunctionDeclaration(d))) return false;
if (d && ts.isVariableDeclaration(d)) {
let init = d.initializer;
while (init && ts.isParenthesizedExpression(init)) init = init.expression;
if (!init || !ts.isObjectLiteralExpression(init)) return false;
}
// A represented whole export declares AT the `module.exports =`
// statement itself. Scalar/function/factory-call roots register one
// snapshot global on that declaration; table/Proxy replacements do
// not. A binding whose declaration owns such storage is therefore a
// single VALUE, not a namespace whose members must appear in Node's
// CJS lexer export table.
if (d && ts.isBinaryExpression(d)) {
if (this.globalsByDeclNode.has(d)) return false;
let r: ts.Expression = d.right;
while (ts.isParenthesizedExpression(r)) r = r.expression;
const scalar =
ts.isNumericLiteral(r) || ts.isStringLiteral(r) || ts.isNoSubstitutionTemplateLiteral(r) ||
r.kind === ts.SyntaxKind.TrueKeyword || r.kind === ts.SyntaxKind.FalseKeyword ||
(ts.isPrefixUnaryExpression(r) && r.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(r.operand));
if (scalar) return false;
}
}
return true;
}
/** Assignment-target resolution: a function local (possibly captured) or
* a module global. tsc has already rejected writes to consts. */
resolveWritable(ident: ts.Identifier): IrLocal | null {
const local = this.resolveLocal(ident);
if (local?.type.kind === "caught") {
// tsc admits writes (the binding types as `unknown`), but the
// snapshot is read-only by design — bind a new local instead.
this.unsupported("SC1090", ident, "assignments to catch bindings");
}
if (local) return local;
const g = this.globalOf(ident);
if (g) return g;
return null;
}
fnSigOf(ident: ts.Identifier): FnSig | null {
const symbol = this.resolveValueSymbol(ident);
if (!symbol) return null;
const projection = this.staticCallables.get(symbol);
return this.fnSigsBySymbol.get(symbol) ??
(projection?.kind === "declared-function" ? projection.signature : null);
}
globalOf(ident: ts.Identifier): IrGlobal | null {
const symbol = this.resolveValueSymbol(ident);
if (!symbol) return null;
const g = this.globalsBySymbol.get(symbol);
if (g) return g;
for (const d of this.checker.declarationsOf(symbol)) {
const byDecl = this.globalsByDeclNode.get(d);
if (byDecl) return byDecl;
}
return null;
}
splitFiles(): FileParts[] {
return splitFiles(this);
}
collectProgram(parts: FileParts[]): void {
return collectProgram(this, parts);
}
/** Names every file's %init and registers the run-once guard globals
* (EVERY module, the entry included: an admissible import cycle can
* close back on the entry, whose init call must be the cache hit Node's
* revisit is — not a recursion) BEFORE any body lowers: function bodies
* and init bodies alike may contain require statements that lower to
* calls of these names. Runs in every pass so the ids are
* deterministic. */
prepareModuleInits(parts: FileParts[]): void {
parts.forEach((fp, i) => this.initNameOf.set(fp.sf, `%init.${i}`));
for (const fp of parts) {
const rawTag = this.fileTag.get(fp.sf) ?? "";
const tag = rawTag === "" ? "e." : rawTag.replace(/^%/, "");
// '%' cannot appear in a user identifier, so the id can never
// collide with a collected module global of the same file.
const id = `%g.${tag}%loaded`;
this.moduleGuardOf.set(fp.sf, id);
this.globalsList.push({ id, name: "%loaded", type: BOOL, mutable: true });
}
// A module is intrinsically async when an await/for-await occurs
// outside every nested function-like boundary. Then propagate that
// status backwards through STATIC ESM edges: Node does not start an
// importer's body until each async dependency has completed. CJS
// import/require edges deliberately do not propagate — Node refuses
// require(esm) when the graph contains top-level await, and the call
// sites below keep that as a named unsupported boundary.
for (const fp of parts) {
let found = false;
ts.walkPreorder(fp.sf, (node) => {
if (node !== fp.sf && ts.isFunctionLike(node)) return "skip";
if (
ts.isAwaitExpression(node) ||
(ts.isForOfStatement(node) && node.awaitModifier !== undefined)
) {
found = true;
return "stop";
}
return undefined;
});
if (found) this.asyncInitFiles.add(fp.sf);
}
let changed = true;
while (changed) {
changed = false;
for (const fp of parts) {
if (this.asyncInitFiles.has(fp.sf) || !isNodeEsmFile(fp.sf)) continue;
if (orderedImportsOf(this.program, fp.sf).some(({ dep }) => dep !== null && this.asyncInitFiles.has(dep))) {
this.asyncInitFiles.add(fp.sf);
changed = true;
}
}
}
for (const fp of parts) {
if (!this.asyncInitFiles.has(fp.sf)) continue;
const rawTag = this.fileTag.get(fp.sf) ?? "";
const tag = rawTag === "" ? "e." : rawTag.replace(/^%/, "");
const id = `%g.${tag}%initPromise`;
this.modulePromiseOf.set(fp.sf, id);
this.globalsList.push({
id,
name: "%initPromise",
type: { kind: "promise", inner: VOID },
mutable: true,
});
}
const orderIndex = new Map(parts.map((fp, i) => [fp.sf, i] as const));
const partSet = new Set(parts.map((fp) => fp.sf));
const staticDeps = (sf: ts.SourceFile): ts.SourceFile[] =>
orderedImportsOf(this.program, sf)
.map(({ dep }) => dep)
.filter((dep): dep is ts.SourceFile => dep !== null && dep !== sf && partSet.has(dep));
// Tarjan SCCs over the same static graph. The last postorder member is
// a deterministic COMPONENT representative for internal-edge tests
// and global naming. The runtime evaluation root can differ: a cycle
// reached only through import() starts at whichever member is actually
// requested first, not whichever import() site preflight discovered
// first. The shared cycle-promise slot below is filled by the emitted
// spawn wrappers so it records that runtime choice.
let nextIndex = 0;
const indexOf = new Map<ts.SourceFile, number>();
const lowOf = new Map<ts.SourceFile, number>();
const stack: ts.SourceFile[] = [];
const onStack = new Set<ts.SourceFile>();
const visit = (sf: ts.SourceFile): void => {
const at = nextIndex++;
indexOf.set(sf, at);
lowOf.set(sf, at);
stack.push(sf);
onStack.add(sf);
for (const dep of staticDeps(sf)) {
if (!indexOf.has(dep)) {
visit(dep);
lowOf.set(sf, Math.min(lowOf.get(sf)!, lowOf.get(dep)!));
} else if (onStack.has(dep)) {
lowOf.set(sf, Math.min(lowOf.get(sf)!, indexOf.get(dep)!));
}
}
if (lowOf.get(sf) !== indexOf.get(sf)) return;
const component: ts.SourceFile[] = [];
for (;;) {
const member = stack.pop()!;
onStack.delete(member);
component.push(member);
if (member === sf) break;
}
if (component.length < 2 || !component.some((member) => this.asyncInitFiles.has(member))) return;
const root = component.reduce((a, b) => orderIndex.get(a)! > orderIndex.get(b)! ? a : b);
const rawTag = this.fileTag.get(root) ?? "";
const tag = rawTag === "" ? "e." : rawTag.replace(/^%/, "");
const cyclePromiseId = `%g.${tag}%cyclePromise`;
this.globalsList.push({
id: cyclePromiseId,
name: "%cyclePromise",
type: { kind: "promise", inner: VOID },
mutable: true,
});
for (const member of component) {
if (this.asyncInitFiles.has(member)) {
this.asyncCycleRepresentativeOf.set(member, root);
this.asyncCyclePromiseOf.set(member, cyclePromiseId);
}
}
};
for (const fp of parts) if (!indexOf.has(fp.sf)) visit(fp.sf);
}
/** The lowering of a CommonJS `require("./local")` occurrence: a call of
* the required module's run-once %init at exactly this statement's
* position — Node's inline evaluation, with the guard supplying the
* cache-hit behavior for every require after the first. Bare specifiers
* naming an opted-in --npm-static package resolve to that package's
* program entry (the same edge preflight admitted — bundle dists require
* their external dependencies by name). Null for everything else
* (builtins load nothing; the rest kept its preflight fence). */
requireInitStmt(spec: string, node: ts.Node): IrStmt | null {
// Project requires resolve within the program; another bare require can
// be a program-module edge when it names an opted-in --npm-static package
// (one package requiring another — the resolution answered its shipped
// JS, the file is in the module order, and the reads alias its globals).
// Without the guarded %init call at this position
// those globals stay uninitialized: the dep's module body would never
// run.
const dep = resolveImport(this.program, node.getSourceFile(), spec) ??
npmStaticDepSf7(this.program, node.getSourceFile(), spec);
if (!dep || dep.fileName.endsWith(".json")) return null;
if (this.asyncInitFiles.has(dep)) {
this.unsupported(
"SC1090",
node,
`require() of '${spec}' (its ES-module graph uses top-level await; use import() instead)`,
);
}
const initName = this.initNameOf.get(dep);
if (initName === undefined) return null;
const loc = locOf(node);
const init: IrStmt = {
kind: "exprStmt",
expr: { kind: "call", callee: initName, args: [], type: VOID, loc },
loc,
};
const parentModule = this.cjsModuleIdOf.get(node.getSourceFile());
const childModule = this.cjsModuleIdOf.get(dep);
if (parentModule === undefined || childModule === undefined) return init;
return {
kind: "block",
body: [
{
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "module.link",
args: [numLit(parentModule, loc), numLit(childModule, loc)],
type: VOID,
loc,
},
loc,
},
init,
],
loc,
};
}
/** True when this body should lower: everything with no reachable set,
* the marked bodies in an externally-gated pass, and exactly the
* UNMARKED bodies in the coverage remainder. */
wantBody(name: string): boolean {
if (this.reachable === null) return true;
return this.remainder ? !this.reachable.has(name) : this.reachable.has(name);
}
collectNpmImports(parts: FileParts[]): void {
return collectNpmImports(this, parts);
}
collectJsonImports(parts: FileParts[]): void {
return collectJsonImports(this, parts);
}
/** Discover the unchecked-array values that cross static ABI/storage
* boundaries before any function body or module initializer is emitted.
* TypeScript's default indexed-access type is a useful source annotation,
* but it is not a runtime proof; promoting only the affected slots keeps
* the rest of the dense ABI unchanged. */
analyzeRuntimeOptionalArrayReads(parts: FileParts[]): void {
const optionalSymbols = new Set<ts.Symbol>();
const optionalReturns = new Set<ts.Symbol>();
const arithmeticReturns = new Map<ts.Symbol, IrType>();
const optionalParams = new Map<ts.Symbol, Set<number>>();
const optionalFields = new Map<ts.Symbol, Set<string>>();
const dynamicObjectEntryRows = new Set<ts.Symbol>();
const sourceFiles = parts.map((p) => p.sf);
const fnDecls = parts.flatMap((p) => p.fnDecls);
// This fixed-point scan intentionally sees deferred bodies before
// reachability is known. Batch their binding queries through the
// facade's panic fence, then keep lookup free of lowering side effects:
// resolveValueSymbol also flushes deferred diagnostics and applies
// merged-namespace fences, whose authority remains ordinary lowering.
this.checker.prefetchSymbolRoots(sourceFiles);
const symbolOf = (node: ts.Node): ts.Symbol | null => {
const s = this.checker.getSymbolAtLocation(node);
if (!s) return null;
return s.flags & ts.SymbolFlags.Alias ? this.checker.getAliasedSymbol(s) : s;
};
const functionDeclBySymbol = new Map<ts.Symbol, ts.FunctionLikeDeclaration>();
type RuntimeSig = {
params: ParamShape[];
returnType: IrType;
top?: FnSig;
method?: { params: ParamShape[]; ret: IrType };
};
const signatureBySymbol = new Map<ts.Symbol, RuntimeSig>();
for (const [symbol, sig] of this.fnSigsBySymbol) signatureBySymbol.set(symbol, { params: sig.params, returnType: sig.returnType, top: sig });
for (const decl of fnDecls) {
const symbol = declSymbolOf(this, decl);
if (symbol) functionDeclBySymbol.set(symbol, decl);
}
for (const sf of sourceFiles) {
ts.walkPreorder(sf, (node) => {
if (
(ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node)) &&
node.body && node.name && (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name))
) {
const symbol = symbolOf(node.name);
if (symbol) functionDeclBySymbol.set(symbol, node);
}
});
}
for (const info of this.classes.values()) {
for (const { mName, member } of this.classMethodMembers(info)) {
if (!member.name || !(ts.isIdentifier(member.name) || ts.isStringLiteral(member.name))) continue;
const symbol = symbolOf(member.name);
const sig = info.methods.get(mName);
if (symbol && sig) signatureBySymbol.set(symbol, { params: sig.params, returnType: sig.ret, method: sig });
}
}
const peel = (node: ts.Expression): ts.Expression => {
let e = node;
while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isTypeAssertion(e) || ts.isNonNullExpression(e)) {
e = e.expression;
}
return e;
};
const explicitlyNonNull = (node: ts.Expression): boolean => {
let e = node;
while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isTypeAssertion(e) || ts.isNonNullExpression(e)) {
if (ts.isNonNullExpression(e)) return true;
e = e.expression;
}
return false;
};
const isArrayRead = (node: ts.Expression): boolean => {
const e = peel(node);
if (!ts.isElementAccessExpression(e)) return false;
return this.mapTypeOf(this.typeOf(e.expression))?.kind === "array";
};
const isDynamicObjectEntryRead = (node: ts.Expression): boolean => {
const read = peel(node);
if (!ts.isElementAccessExpression(read) || !ts.isIdentifier(read.expression)) return false;
const source = symbolOf(read.expression);
const declaration = source ? this.checker.valueDeclarationOf(source) : undefined;
if (!declaration || !ts.isVariableDeclaration(declaration) || !declaration.initializer) return false;
const init = peel(declaration.initializer);
if (!ts.isCallExpression(init) || !ts.isPropertyAccessExpression(init.expression)) return false;
if (init.expression.name.text !== "entries" || !this.isStdlibGlobal(init.expression.expression, "Object")) return false;
const object = init.arguments[0];
const objectType = object ? this.mapTypeOf(this.typeOf(object)) : null;
return objectType?.kind === "dyn" || objectType?.kind === "jsval";
};
const addUndefined = (t: IrType): IrType => {
if (t.kind === "union") return this.armTag(t.unionId, UNDEFINED_T) >= 0 ? t : this.withUndefinedArmOf(t) ?? t;
if (t.kind === "void" || t.kind === "dyn" || t.kind === "jsval" || t.kind === "generator") return t;
return this.withUndefinedArm(t);
};
const fieldName = (node: ts.PropertyAccessExpression): string => node.name.text;
const mayBeOptional = (node: ts.Expression): boolean => {
// `xs[i]!` is the explicit proven-present form. Its array read keeps
// the established dense bounds trap and must not promote the enclosing
// function/global ABI to `T | undefined`.
if (explicitlyNonNull(node)) return false;
const e = peel(node);
if (isArrayRead(e)) return true;
if (ts.isCallExpression(e) && this.runtimeOptionalReduceTypes.has(e)) return true;
if (
ts.isCallExpression(e) && e.arguments.length === 0 &&
ts.isPropertyAccessExpression(e.expression) &&
e.expression.name.text === "stringify" && this.isStdlibGlobal(e.expression.expression, "JSON")
) return true;
if (
ts.isCallExpression(e) &&
ts.isPropertyAccessExpression(e.expression) &&
(e.expression.name.text === "pop" || e.expression.name.text === "shift") &&
this.mapTypeOf(this.typeOf(e))?.kind === "union" &&
this.armTag((this.mapTypeOf(this.typeOf(e)) as IrType & { kind: "union" }).unionId, UNDEFINED_T) >= 0
) return true;
if (ts.isIdentifier(e)) return optionalSymbols.has(symbolOf(e) ?? ({} as ts.Symbol));
if (ts.isCallExpression(e) && ts.isIdentifier(e.expression)) {
return optionalReturns.has(symbolOf(e.expression) ?? ({} as ts.Symbol));
}
if (ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression)) {
return optionalReturns.has(symbolOf(e.expression.name) ?? ({} as ts.Symbol));
}
if (ts.isConditionalExpression(e)) {
return mayBeOptional(e.whenTrue) || mayBeOptional(e.whenFalse);
}
if (ts.isBinaryExpression(e)) {
const op = e.operatorToken.kind;
if (
op === ts.SyntaxKind.AmpersandAmpersandToken ||
op === ts.SyntaxKind.BarBarToken ||
op === ts.SyntaxKind.QuestionQuestionToken
) return mayBeOptional(e.left) || mayBeOptional(e.right);
}
if (ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.expression)) {
const fields = optionalFields.get(symbolOf(e.expression) ?? ({} as ts.Symbol));
return fields?.has(fieldName(e)) ?? false;
}
return false;
};
const optionalStringArithmeticType = (node: ts.Expression): IrType | null => {
const e = peel(node);
if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.PlusToken) return null;
const stringArrayRead = (part: ts.Expression): boolean => {
const t = this.mapTypeOf(this.typeOf(part));
const p = peel(part);
if (t?.kind !== "string" || !ts.isElementAccessExpression(p)) return false;
const recv = this.mapTypeOf(this.typeOf(p.expression));
return recv?.kind === "array" && recv.elem.kind === "string";
};
const primitive = (part: ts.Expression): boolean => {
const t = this.mapTypeOf(this.typeOf(part));
return t?.kind === "f64" || t?.kind === "string";
};
if (!stringArrayRead(e.left) && !stringArrayRead(e.right)) return null;
if (!(primitive(e.left) || stringArrayRead(e.left)) || !(primitive(e.right) || stringArrayRead(e.right))) return null;
return { kind: "union", unionId: this.unions.intern([F64, STRING]) };
};
const noteField = (decl: ts.VariableDeclaration, name: string): boolean => {
if (!ts.isIdentifier(decl.name)) return false;
const symbol = symbolOf(decl.name);
if (!symbol) return false;
return noteFieldSymbol(symbol, name);
};
const noteFieldSymbol = (symbol: ts.Symbol, name: string): boolean => {
const set = optionalFields.get(symbol) ?? new Set<string>();
const before = set.size;
set.add(name);
optionalFields.set(symbol, set);
return set.size !== before;
};
const scanPattern = (name: ts.BindingName, init: ts.Expression): boolean => {
if (!ts.isArrayBindingPattern(name)) return false;
const sourceType = this.mapTypeOf(this.typeOf(init));
if (sourceType?.kind !== "array") return false;
let changed = false;
name.elements.forEach((el) => {
if (ts.isOmittedExpression(el) || el.name === undefined || el.dotDotDotToken) return;
if (ts.isIdentifier(el.name)) {
const symbol = symbolOf(el.name);
if (symbol && !el.initializer && !optionalSymbols.has(symbol)) {
optionalSymbols.add(symbol);
// Indexed flow facts can survive a mutating method call in the
// checker. The array's element ABI describes every value a
// position may hold after that call, so bind from that type.
const mapped = sourceType.elem;
if (mapped && mapped.kind !== "void" && mapped.kind !== "dyn" && mapped.kind !== "jsval") {
const widened = mapped.kind === "union" ? this.withUndefinedArmOf(mapped) : this.withUndefinedArm(mapped);
if (widened) {
this.runtimeOptionalBindingTypes.set(symbol, widened);
const global = this.globalsBySymbol.get(symbol);
if (global) {
global.type = widened;
this.runtimeOptionalGlobals.add(global);
}
}
}
changed = true;
}
}
});
return changed;
};
const hofCallbackIndices = (name: string, hasInitialValue: boolean): number[] | null => {
if (name === "reduce" || name === "reduceRight") return hasInitialValue ? [1] : [0, 1];
if (
name === "map" || name === "filter" || name === "forEach" ||
name === "find" || name === "findIndex" || name === "findLast" ||
name === "findLastIndex" || name === "some" || name === "every" ||
name === "flatMap"
) return [0];
return null;
};
const bodyReturnsOptional = (fn: ts.FunctionLikeDeclaration): boolean => {
if (!fn.body) return false;
if (!ts.isBlock(fn.body)) return mayBeOptional(fn.body as ts.Expression);
let found = false;
ts.walkPreorder(fn.body, (node) => {
if (node !== fn.body && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node))) return "skip";
if (ts.isReturnStatement(node) && node.expression && mayBeOptional(node.expression)) found = true;
return undefined;
});
return found;
};
const promoteHofCallback = (callback: ts.Expression, parameterIndices: readonly number[], seen = new Set<ts.Symbol>()): boolean => {
let changed = false;
const checkerTypeContainsTypeParameter = (type: ts.Type): boolean => {
if ((type.flags & ts.TypeFlags.TypeParameter) !== 0) return true;
return type.isUnionType() && ts.constituentTypes(type).some(checkerTypeContainsTypeParameter);
};
const promoteNode = (fn: ts.FunctionLikeDeclaration): void => {
for (const parameterIndex of parameterIndices) {
const parameter = fn.parameters[parameterIndex];
if (!parameter) continue;
const symbol = ts.isIdentifier(parameter.name) ? symbolOf(parameter.name) : null;
const checkerType = this.typeOf(parameter.name);
const mapped = this.mapTypeOf(checkerType);
// Generic callbacks are monomorphized by their call-site binder.
// During this prepass the type parameter has no static ABI yet;
// asking irTypeOf to invent one poisons otherwise valid generic
// value/HOF programs. Leave that parameter at its generic
// signature and let the normal instantiation path specialize it.
const current = mapped ??
(checkerTypeContainsTypeParameter(checkerType) ? null : this.irTypeOf(parameter.name));
if (!current) continue;
if (!ts.isIdentifier(parameter.name)) {
const widened = this.runtimeOptionalType(current);
const previous = this.runtimeOptionalPatternTypes.get(parameter.name);
this.runtimeOptionalPatternTypes.set(parameter.name, widened);
if (!previous || !typeEquals(previous, widened)) changed = true;
}
if (symbol) {
const widened = this.runtimeOptionalType(current);
const previous = this.runtimeOptionalBindingTypes.get(symbol);
this.runtimeOptionalBindingTypes.set(symbol, widened);
if (!previous || !typeEquals(previous, widened)) changed = true;
if (!optionalSymbols.has(symbol)) {
optionalSymbols.add(symbol);
changed = true;
}
}
}
const fnType = this.mapTypeOf(this.typeOf(fn));
if (fnType?.kind === "func" && bodyReturnsOptional(fn)) {
const widenedReturn = this.runtimeOptionalType(fnType.ret);
const previousReturn = this.runtimeOptionalFunctionReturnType(fn, fnType.ret);
this.runtimeOptionalFunctionReturns.set(fn, widenedReturn);
if (!typeEquals(previousReturn, widenedReturn)) changed = true;
}
};
if (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback) || ts.isFunctionDeclaration(callback) || ts.isMethodDeclaration(callback)) {
promoteNode(callback);
return changed;
}
const callbackName = ts.isIdentifier(callback)
? callback
: ts.isPropertyAccessExpression(callback)
? callback.name
: null;
if (!callbackName) return false;
const symbol = symbolOf(callbackName);
if (!symbol || seen.has(symbol)) return false;
// Generic function values are pinned and monomorphized by the normal
// contextual-value path when the HOF lowers its callback. Promoting
// their type-parameter body here either asks for an impossible
// monomorphic ABI or poisons valid generic value programs.
if (this.genericFnsBySymbol.has(symbol)) return false;
seen.add(symbol);
const declaration = this.checker.valueDeclarationOf(symbol);
if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) {
if (promoteHofCallback(peel(declaration.initializer), parameterIndices, seen)) changed = true;
}
const sig = signatureBySymbol.get(symbol);
if (sig) {
for (const parameterIndex of parameterIndices) {
if (!sig.params[parameterIndex]) continue;
const before = sig.params[parameterIndex]!.type;
sig.params[parameterIndex]!.type = this.runtimeOptionalType(before);
if (!typeEquals(before, sig.params[parameterIndex]!.type)) changed = true;
}
}
const fn = functionDeclBySymbol.get(symbol);
if (fn) promoteNode(fn);
const valueType = this.runtimeOptionalBindingTypes.get(symbol) ?? this.mapTypeOf(this.typeOf(callback));
if (valueType?.kind === "func") {
const params = valueType.params.slice();
for (const parameterIndex of parameterIndices) {
if (params[parameterIndex]) params[parameterIndex] = this.runtimeOptionalType(params[parameterIndex]!);
}
const promoted: IrType = { ...valueType, params };
const previous = this.runtimeOptionalBindingTypes.get(symbol);
this.runtimeOptionalBindingTypes.set(symbol, promoted);
if (!previous || !typeEquals(previous, promoted)) changed = true;
const global = this.globalsBySymbol.get(symbol);
if (global) global.type = promoted;
// A callback-typed parameter used by a native array HOF changes
// the containing function's own call ABI. Record that edge in the
// already-collected signature as well as on the body binding, so a
// caller can promote the concrete callback value it supplies on a
// later fixed-point pass (`wrapper(xs, predicate)` ->
// `xs.some(predicate)`).
if (declaration && ts.isParameter(declaration)) {
const owner = declaration.parent;
let ownerSymbol: ts.Symbol | null = null;
if (ts.isFunctionDeclaration(owner)) ownerSymbol = declSymbolOf(this, owner) ?? null;
else if (
(ts.isMethodDeclaration(owner) || ts.isGetAccessorDeclaration(owner) || ts.isSetAccessorDeclaration(owner)) &&
owner.name
) ownerSymbol = symbolOf(owner.name);
const ownerSig = ownerSymbol ? signatureBySymbol.get(ownerSymbol) : undefined;
const ownerIndex = ts.isFunctionLike(owner) ? owner.parameters.indexOf(declaration) : -1;
const slot = ownerIndex >= 0 ? ownerSig?.params[ownerIndex] : undefined;
if (slot && !typeEquals(slot.type, promoted)) {
slot.type = promoted;
changed = true;
}
}
}
return changed;
};
const callbackReturnsOptional = (callback: ts.Expression, seen = new Set<ts.Symbol>()): boolean => {
const node = peel(callback);
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) return bodyReturnsOptional(node);
const symbol = symbolOf(ts.isPropertyAccessExpression(node) ? node.name : node);
if (!symbol || seen.has(symbol)) return false;
seen.add(symbol);
const fn = functionDeclBySymbol.get(symbol);
if (fn) return bodyReturnsOptional(fn);
const decl = this.checker.valueDeclarationOf(symbol);
return !!decl && ts.isVariableDeclaration(decl) && !!decl.initializer && callbackReturnsOptional(decl.initializer, seen);
};
const scanFile = (sf: ts.SourceFile): boolean => {
let changed = false;
ts.walkPreorder(sf, (node) => {
if (ts.isVariableDeclaration(node)) {
if (node.initializer) {
if (ts.isIdentifier(node.name) && isDynamicObjectEntryRead(node.initializer)) {
const symbol = symbolOf(node.name);
if (symbol && !dynamicObjectEntryRows.has(symbol)) {
dynamicObjectEntryRows.add(symbol);
changed = true;
}
} else if (ts.isIdentifier(node.name) && mayBeOptional(node.initializer)) {
const symbol = symbolOf(node.name);
if (symbol && !optionalSymbols.has(symbol)) {
optionalSymbols.add(symbol);
changed = true;
}
}
if (ts.isIdentifier(node.name)) {
const valueSymbol = ts.isIdentifier(peel(node.initializer))
? symbolOf(peel(node.initializer))
: ts.isPropertyAccessExpression(peel(node.initializer))
? symbolOf((peel(node.initializer) as ts.PropertyAccessExpression).name)
: null;
const symbol = symbolOf(node.name);
const sourceSig = valueSymbol ? signatureBySymbol.get(valueSymbol) : undefined;
const sourceOptional =
!!sourceSig &&
(optionalReturns.has(valueSymbol!) ||
(sourceSig.returnType.kind === "union" && this.armTag(sourceSig.returnType.unionId, UNDEFINED_T) >= 0) ||
sourceSig.params.some((p) => p.type.kind === "union" && this.armTag(p.type.unionId, UNDEFINED_T) >= 0));
if (symbol && valueSymbol && sourceOptional) {
const aliasNew = !optionalReturns.has(symbol);
if (aliasNew) optionalReturns.add(symbol);
if (aliasNew) changed = true;
const valueType = this.mapTypeOf(this.typeOf(node.name));
if (valueType?.kind === "func" && sourceSig) {
const promotedFn: IrType = {
...valueType,
params: sourceSig.params.map((p) => p.type),
ret: sourceSig.returnType,
};
const previous = this.runtimeOptionalBindingTypes.get(symbol);
if (!previous || !typeEquals(previous, promotedFn)) {
this.runtimeOptionalBindingTypes.set(symbol, promotedFn);
const global = this.globalsBySymbol.get(symbol);
if (global) global.type = promotedFn;
changed = true;
}
}
}
}
if (scanPattern(node.name, node.initializer)) changed = true;
if (ts.isIdentifier(node.name) && ts.isObjectLiteralExpression(peel(node.initializer))) {
const object = peel(node.initializer) as ts.ObjectLiteralExpression;
for (const prop of object.properties) {
if (!ts.isPropertyAssignment(prop) && !ts.isShorthandPropertyAssignment(prop)) continue;
const name = prop.name;
if (!name || !(ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name))) continue;
const value = ts.isPropertyAssignment(prop) ? prop.initializer : prop.name as ts.Expression;
if (mayBeOptional(value) && noteField(node, name.text)) changed = true;
}
}
}
}
if (
ts.isBinaryExpression(node) &&
(node.operatorToken.kind === ts.SyntaxKind.EqualsToken ||
node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionEqualsToken ||
node.operatorToken.kind === ts.SyntaxKind.BarBarEqualsToken ||
node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken) &&
mayBeOptional(node.right)
) {
if (ts.isIdentifier(node.left)) {
const symbol = symbolOf(node.left);
if (symbol && !optionalSymbols.has(symbol)) {
optionalSymbols.add(symbol);
changed = true;
}
} else if (ts.isPropertyAccessExpression(node.left) && ts.isIdentifier(node.left.expression)) {
const symbol = symbolOf(node.left.expression);
if (symbol && noteFieldSymbol(symbol, node.left.name.text)) changed = true;
}
}
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
const callbackIndices = hofCallbackIndices(node.expression.name.text, node.arguments.length >= 2);
const receiverNode = node.expression.expression;
const receiverTs = this.typeOf(receiverNode);
let receiver = this.mapTypeOf(receiverTs);
// An array method after an optional-chain guard only runs on the
// present receiver. The checker includes the chain's short-circuit
// undefined in intermediate call types, so use the same narrowed
// receiver the method lowerer sees when promoting HOF callbacks.
if (
callbackIndices !== null && receiver?.kind === "union" &&
(node.expression.questionDotToken !== undefined || isOptionalChainTail(this, node))
) {
receiver = this.mapTypeOf(this.checker.getNonNullableType(receiverTs));
}
const callback = callbackIndices === null ? undefined : node.arguments[0];
const tuple = receiver?.kind === "record" && this.shapes.get(receiver.shapeId)?.tuple === true;
// A JS/evolving-any array may have acquired a precise FLOW type
// at this call while its actual binding remains a checked-dynamic
// array. Its callback consumes DYN values through runtime method
// dispatch, so widening that callback to the native array value
// ABI makes it impossible to box back into DYN. Judge a simple
// binding by its declaration type, the same storage fact local
// declaration lowering uses.
let nativeArrayReceiver = receiver?.kind === "array" || tuple;
if (nativeArrayReceiver && ts.isIdentifier(receiverNode)) {
const receiverSymbol = symbolOf(receiverNode);
const declaration = receiverSymbol ? this.checker.valueDeclarationOf(receiverSymbol) : undefined;
if (declaration && ts.isVariableDeclaration(declaration)) {
const declared = this.mapTypeOf(this.typeOf(declaration.name));
if (declared?.kind !== "array" && !(declared?.kind === "record" && this.shapes.get(declared.shapeId)?.tuple === true)) {
nativeArrayReceiver = false;
}
}
}
if (nativeArrayReceiver && callback && !ts.isSpreadElement(callback)) {
if (promoteHofCallback(callback, callbackIndices!)) changed = true;
const method = node.expression.name.text;
if ((method === "reduce" || method === "reduceRight") &&
(node.arguments.length < 2 || callbackReturnsOptional(callback))) {
if (promoteHofCallback(callback, [0, 1])) changed = true;
const result = this.runtimeOptionalType(this.irTypeOf(node));
const previous = this.runtimeOptionalReduceTypes.get(node);
if (!previous || !typeEquals(previous, result)) {
this.runtimeOptionalReduceTypes.set(node, result);
changed = true;
}
}
}
}
if (
ts.isCallExpression(node) &&
(ts.isIdentifier(node.expression) || ts.isPropertyAccessExpression(node.expression))
) {
const calleeNode = ts.isIdentifier(node.expression) ? node.expression : node.expression.name;
const symbol = symbolOf(calleeNode);
if (!symbol) return;
const sig = signatureBySymbol.get(symbol);
if (!sig) {
const decl = this.checker.valueDeclarationOf(symbol);
if (decl && ts.isVariableDeclaration(decl) && decl.initializer && ts.isIdentifier(decl.initializer)) {
const source = symbolOf(decl.initializer);
const sourceSig = source ? signatureBySymbol.get(source) : undefined;
if (sourceSig) {
node.arguments.forEach((arg, i) => {
if (ts.isSpreadElement(arg) || !mayBeOptional(arg) || !sourceSig.params[i]) return;
const before = sourceSig.params[i]!.type;
sourceSig.params[i]!.type = this.runtimeOptionalType(before);
if (!typeEquals(before, sourceSig.params[i]!.type)) {
changed = true;
const sourceFn = functionDeclBySymbol.get(source!);
const parameter = sourceFn?.parameters[i];
if (parameter) {
for (const bound of boundIdentifiersOf(parameter.name)) {
const boundSymbol = symbolOf(bound);
if (boundSymbol && !optionalSymbols.has(boundSymbol)) {
optionalSymbols.add(boundSymbol);
changed = true;
}
}
}
}
});
}
}
return;
}
node.arguments.forEach((arg, i) => {
const callbackSlot = sig.params[i]?.type;
if (callbackSlot?.kind === "func" && !ts.isSpreadElement(arg)) {
const optionalCallbackParams = callbackSlot.params.flatMap((type, index) =>
type.kind === "union" && this.armTag(type.unionId, UNDEFINED_T) >= 0 ? [index] : []);
if (optionalCallbackParams.length > 0 && promoteHofCallback(arg, optionalCallbackParams)) {
changed = true;
}
}
if (ts.isSpreadElement(arg) || !mayBeOptional(arg) || !sig.params[i]) return;
const set = optionalParams.get(symbol) ?? new Set<number>();
const before = set.size;
set.add(i);
optionalParams.set(symbol, set);
if (set.size !== before) {
changed = true;
const callee = functionDeclBySymbol.get(symbol);
const parameter = callee?.parameters[i];
if (parameter) {
for (const bound of boundIdentifiersOf(parameter.name)) {
const boundSymbol = symbolOf(bound);
if (boundSymbol && !optionalSymbols.has(boundSymbol)) {
optionalSymbols.add(boundSymbol);
changed = true;
}
}
}
}
});
}
});
return changed;
};
const scanReturns = (symbol: ts.Symbol, decl: ts.FunctionLikeDeclaration): boolean => {
if (!decl.body) return false;
let changed = false;
if (!ts.isBlock(decl.body) && mayBeOptional(decl.body as ts.Expression) && !optionalReturns.has(symbol)) {
optionalReturns.add(symbol);
changed = true;
}
if (!ts.isBlock(decl.body)) {
const arithmetic = optionalStringArithmeticType(decl.body as ts.Expression);
if (arithmetic && !arithmeticReturns.has(symbol)) {
arithmeticReturns.set(symbol, arithmetic);
changed = true;
}
}
ts.walkPreorder(decl.body, (node) => {
if (node !== decl.body && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node))) return "skip";
if (ts.isReturnStatement(node) && node.expression && mayBeOptional(node.expression) && !optionalReturns.has(symbol)) {
optionalReturns.add(symbol);
changed = true;
}
if (ts.isReturnStatement(node) && node.expression) {
const arithmetic = optionalStringArithmeticType(node.expression);
if (arithmetic && !arithmeticReturns.has(symbol)) {
arithmeticReturns.set(symbol, arithmetic);
changed = true;
}
}
return undefined;
});
return changed;
};
let changed = true;
while (changed) {
changed = false;
for (const sf of sourceFiles) if (scanFile(sf)) changed = true;
for (const [symbol, decl] of functionDeclBySymbol) if (scanReturns(symbol, decl)) changed = true;
}
for (const [symbol, sig] of signatureBySymbol) {
const params = optionalParams.get(symbol);
if (params) {
for (const i of params) {
const shape = sig.params[i];
if (shape) shape.type = addUndefined(shape.type);
}
}
if (optionalReturns.has(symbol)) {
const next = sig.returnType.kind === "promise"
? { kind: "promise" as const, inner: addUndefined(sig.returnType.inner) }
: addUndefined(sig.returnType);
sig.returnType = next;
if (sig.top) sig.top.returnType = next;
if (sig.method) sig.method.ret = next;
}
const arithmetic = arithmeticReturns.get(symbol);
if (arithmetic && sig.returnType.kind !== "promise") {
sig.returnType = arithmetic;
if (sig.top) sig.top.returnType = arithmetic;
if (sig.method) sig.method.ret = arithmetic;
const decl = functionDeclBySymbol.get(symbol);
if (decl) this.runtimeOptionalFunctionReturns.set(decl, arithmetic);
}
}
for (const sf of sourceFiles) {
ts.walkPreorder(sf, (node) => {
if (!ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name)) return;
const symbol = symbolOf(node.name);
if (!symbol) return;
const fields = optionalFields.get(symbol);
if (!dynamicObjectEntryRows.has(symbol) && !optionalSymbols.has(symbol) && !fields) return;
const current = this.mapTypeOf(this.typeOf(node.name));
if (!current) return;
let promoted = dynamicObjectEntryRows.has(symbol) ? DYN : current;
if (!dynamicObjectEntryRows.has(symbol) && optionalSymbols.has(symbol)) promoted = addUndefined(promoted);
if (promoted.kind === "func" && node.initializer && ts.isIdentifier(node.initializer)) {
const sourceSig = this.fnSigOf(node.initializer);
if (sourceSig) {
promoted = {
...promoted,
params: sourceSig.params.filter((p) => p.mode !== "dynRest").map((p) => p.type),
ret: sourceSig.returnType,
};
}
}
if (fields && promoted.kind === "record") {
for (const field of fields) {
const source = promoted.kind === "record"
? this.shapes.get(promoted.shapeId)?.fields.find((f) => f.name === field)
: undefined;
if (!source) continue;
promoted = this.runtimeOptionalRecordField(promoted, field, addUndefined(source.type));
}
}
if (!typeEquals(promoted, current)) {
this.runtimeOptionalBindingTypes.set(symbol, promoted);
const global = ts.isIdentifier(node.name) ? this.globalOf(node.name) : this.globalsBySymbol.get(symbol);
if (global) {
global.type = promoted;
this.runtimeOptionalGlobals.add(global);
}
} else if (promoted.kind === "union" && this.armTag(promoted.unionId, UNDEFINED_T) >= 0) {
const global = ts.isIdentifier(node.name) ? this.globalOf(node.name) : this.globalsBySymbol.get(symbol);
if (global) this.runtimeOptionalGlobals.add(global);
}
});
}
}
run(): LowerResult {
const parts = this.splitFiles();
// The coverage remainder deliberately visits every body that reachable
// emit skipped. Those files are already phase-managed, so an ordinary
// checker miss would stay a one-node IPC query instead of falling back
// to the facade's whole-file batch. Coverage has no dead-body boundary
// to preserve: prime each complete file now, reusing the answers the
// reachable pass already memoized and batching only the cold remainder.
if (this.remainder) {
for (const fp of parts) this.checker.prefetchSourceFile(fp.sf);
}
this.collectProgram(parts);
this.analyzeRuntimeOptionalArrayReads(parts);
// Decorated classes analyze AFTER the whole collection pass: a
// decorator's return type may name a subclass declared below the
// class, and reference lowering needs each class's rebindability
// (valueGlobalId) settled before any body lowers.
for (const info of this.classes.values()) analyzeClassDecoration(this, info);
this.prepareModuleInits(parts);
prepareCjsModuleGraph(this, parts);
const functions: IrFunction[] = [];
for (const fp of parts) {
for (const decl of fp.fnDecls) {
// Overload signatures / ambient declarations are type-world (no
// body to lower — and they share the implementation's symbol, so
// counting them as skips would double-count the declaration).
if (!decl.body) continue;
// Generic functions have no body of their own: they are lowered
// per-instantiation, on demand, from the worklist below.
const declSymbol = declSymbolOf(this, decl);
if (declSymbol && this.genericFnsBySymbol.has(declSymbol)) continue;
// Mixin functions likewise: no signature, no body of their own —
// calls instantiate the class inside per site (lower-mixins.ts).
if (this.mixinFnShapes.get(decl)) continue;
const sig = declSymbol ? this.fnSigsBySymbol.get(declSymbol) : undefined;
// A body nothing reaches never lowers: its constructs can't fail
// the build and it leaves no trace in the emitted C.
if (sig && !this.wantBody(sig.name)) continue;
const fn = this.lowerFunction(decl);
if (fn) functions.push(fn);
else if (this.countsSkips()) this.stats.functionsSkipped++;
}
for (const decl of fp.classDecls) {
const info = this.classes.get(this.classNamer(decl));
if (info) functions.push(...this.lowerClassMembers(info));
else if (this.countsSkips()) this.stats.functionsSkipped++;
}
}
// Each file's top-level statements form its run-once init function.
// %main calls only the ENTRY's init: each init's hoisted import header
// runs its dependencies (npm island loads at their import positions),
// inline require statements call theirs mid-body, and the guards make
// revisits cache hits — Node's evaluation order over the WHOLE graph
// falls out of the nesting. The coverage remainder skips them — they
// are reachable by definition, already counted by reachable emit.
if (!this.remainder) {
for (const fp of parts) {
functions.push(this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!));
}
functions.push(this.buildMain());
}
// Class EXPRESSIONS collected while the inits lowered: their members
// lower here, wantBody-gated like declaration members (nested class
// expressions inside these bodies are fenced, so the list is stable).
// Monomorphization worklists: every site above queued the generic
// instances it needs — function instances (calls, pinned values) and
// class instantiations (type references) — and lowering any instance
// body can queue more of EITHER kind (generic functions constructing
// generic classes, generic methods calling generic functions) — the
// index loops run to the joint fixpoint. Same-key recursion re-uses
// its own entry; polymorphic recursion is cut off by
// MAX_GENERIC_INSTANCES.
{
let ec = 0;
let gc = 0;
let gi = 0;
let es = 0;
while (
ec < this.exprClasses.length ||
gc < this.genericClassInstances.length ||
gi < this.instantiationQueue.length ||
es < this.emitSpecQueue.length
) {
while (ec < this.exprClasses.length) {
functions.push(...this.lowerClassMembers(this.exprClasses[ec++]!));
}
while (gc < this.genericClassInstances.length) {
functions.push(...this.lowerClassMembers(this.genericClassInstances[gc++]!));
}
while (gi < this.instantiationQueue.length) {
const { info, inst } = this.instantiationQueue[gi++]!;
// A body-level poison outside the per-statement catches (a
// generic method's this/super fence, a fenced parameter
// default): the diagnostic is recorded — the instance skips
// like a signature-blocked function (lowerFunction's rule).
try {
functions.push(this.lowerGenericInstance(info, inst));
} catch (e) {
if (!(e instanceof PoisonError)) throw e;
}
}
// Emit-override specializations queued by the emit sites above (a
// body can queue more — the super-forward chain — and generic
// instances of its own; the joint fixpoint covers both).
while (es < this.emitSpecQueue.length) {
try {
const fn = lowerEmitOverrideSpec(this, this.emitSpecQueue[es++]!);
if (fn) functions.push(fn);
} catch (e) {
if (!(e instanceof PoisonError)) throw e;
}
}
}
}
// Lambdas lifted while lowering any of the above (plus synthetic
// array-HOF loop functions, which ride the same list), and the
// implicit-any instances lowered eagerly at their first call sites.
functions.push(...this.liftedFns);
functions.push(...this.implicitFns);
if (this.remainder) {
// Deferred collection diagnostics nothing flushed — declarations no
// reference ever made relevant. They belong to the unreached group
// (collection order keeps them deterministic).
for (const [symbol, diags] of this.deferredDiags) {
if (this.alreadyFlushed.has(symbol)) continue;
for (const d of diags) this.pushDiag(d);
}
return {
module: null,
diagnostics: this.diags,
runtimeFences: this.runtimeFences,
stats: this.stats,
...(this.statsByFile.size > 0 ? { statsByFile: this.statsByFile } : {}),
...(this.provenanceElided.length > 0 ? { provenanceElided: this.provenanceElided } : {}),
...(this.npmBuiltins ? { npmBuiltins: this.npmBuiltins } : {}),
...(this.npmLazyTraps ? { npmLazyTraps: this.npmLazyTraps } : {}),
};
}
return this.finishModule(functions);
}
/** Final retention, pruning, and module assembly shared by ordinary emit
* and the retained reachability worklist. */
finishModule(functions: IrFunction[]): LowerResult {
// Globals typed by a class that never REGISTERED (a JS class whose
// collection fenced — Symbol-keyed fields, an unsupported base): the
// declaration statement and every use compiled to runtime fences, but
// the collection-time global still carries the object type, and the
// emitter would name a struct that does not exist — invalid C, the
// compile-C escape family. The storage is dead by construction (the
// initializing assign never lowered; reads cascade to their own
// fences), so drop it — guarded by a reference scan, with the
// validator's registration check as the backstop for anything that
// does slip through with a live reference.
const brokenGlobals = this.globalsList.filter((g) => this.typeNamesUnregisteredClass(g.type));
const brokenLocalFns = functions.filter((fn) =>
fn.locals.some((l) => this.typeNamesUnregisteredClass(l.type)),
);
if (brokenGlobals.length > 0 || brokenLocalFns.length > 0) {
const referencedIn = (root: unknown): Set<string> => {
const referenced = new Set<string>();
const scan = (node: unknown): void => {
if (node === null || typeof node !== "object") return;
if (Array.isArray(node)) {
for (const item of node) scan(item);
return;
}
const rec = node as Record<string, unknown>;
const id = rec["localId"];
if (typeof id === "string") referenced.add(id);
const catchId = rec["catchLocalId"];
if (typeof catchId === "string") referenced.add(catchId);
// Closure captures name their source locals as PLAIN STRINGS
// (captures: string[]) — a captured broken-class local is live.
if (rec["kind"] === "closure" && Array.isArray(rec["captures"])) {
for (const c of rec["captures"]) if (typeof c === "string") referenced.add(c);
}
for (const key of Object.keys(rec)) scan(rec[key]);
};
scan(root);
return referenced;
};
if (brokenGlobals.length > 0) {
const referenced = referencedIn(functions);
for (const g of brokenGlobals) {
if (referenced.has(g.id)) continue; // live reference — validator reports
const i = this.globalsList.indexOf(g);
if (i >= 0) this.globalsList.splice(i, 1);
}
}
// LOCALS left behind the same way (`const countdown = new Countdown(...)`
// inside an init body whose declaration fenced): the emitter declares
// every local at function top, so an unreferenced one typed by the
// unregistered class is the identical invalid-C escape.
for (const fn of brokenLocalFns) {
// Params and captures list their locals by id too — never prune
// those out from under them.
const referenced = referencedIn([fn.body, fn.params, fn.captures ?? []]);
fn.locals = fn.locals.filter(
(l) => referenced.has(l.id) || !this.typeNamesUnregisteredClass(l.type),
);
}
}
// Computed before the module gate: retention resolves every class name
// the emitted program references, flushing deferred class diagnostics
// that a reached type makes relevant.
const artifacts = this.moduleArtifacts(functions);
// Types still naming a class that never REGISTERED after retention's
// flush (JS graphs whose class fences deferred to runtime — the
// sentence-walker's path params, printer tables whose func-typed
// fields spell the fenced class): the emitter would name a struct
// that does not exist — the compile-C escape family. No instance of
// such a class can ever exist (every construction site fenced), so
// the slots are inert by construction: rewrite each to the f64 dummy
// placeholder (boxNewC's uncollected-class stance, applied to unboxed
// slots), uniformly across params/locals/globals/fields/body types so
// every producer and consumer agrees. Programs with no unregistered
// reference are untouched — byte-stability holds.
if (this.diags.length === 0) {
this.sanitizeUnregisteredClassTypes([functions, this.globalsList, artifacts.classes, artifacts.records, artifacts.unions]);
}
const module: IrModule | null =
this.diags.length > 0
? null
: {
irVersion: 13,
sourceFile: this.entry.fileName,
functions,
classes: artifacts.classes,
records: artifacts.records,
unions: artifacts.unions,
globals: this.globalsList,
...(this.npmEmbedded ? { embedded: this.npmEmbedded } : {}),
entry: ENTRY_NAME,
...(this.ffiImports.length > 0 ? { ffiImports: [...this.ffiImports] } : {}),
};
return {
module,
diagnostics: this.diags,
runtimeFences: this.runtimeFences,
stats: this.stats,
...(this.statsByFile.size > 0 ? { statsByFile: this.statsByFile } : {}),
...(this.provenanceElided.length > 0 ? { provenanceElided: this.provenanceElided } : {}),
...(this.npmBuiltins ? { npmBuiltins: this.npmBuiltins } : {}),
...(this.npmLazyTraps ? { npmLazyTraps: this.npmLazyTraps } : {}),
};
}
/** The unregistered-class type sweep (run()'s last step before the
* module assembles): every `{kind:"object"}` TYPE naming a class with
* no registered ClassInfo is rewritten IN PLACE to the f64 dummy.
* classval types are exempt (they emit the class-independent
* `ScrClassObj *` — inert-but-valid storage, the validator's own
* stance), and only type objects rewrite — node-level classNames
* (`new`, upcasts) cannot reach here (their lowerings fence without a
* registered class), so the validator still backstops those. */
sanitizeUnregisteredClassTypes(roots: unknown[]): void {
const isUnregisteredObjectType = (v: unknown): boolean =>
typeof v === "object" && v !== null &&
(v as { kind?: unknown }).kind === "object" &&
typeof (v as { className?: unknown }).className === "string" &&
!this.classes.has((v as { className: string }).className);
const sweep = (node: unknown): void => {
if (node === null || typeof node !== "object") return;
if (Array.isArray(node)) {
node.forEach((item, i) => {
if (isUnregisteredObjectType(item)) node[i] = F64;
else sweep(item);
});
return;
}
const rec = node as Record<string, unknown>;
for (const key of Object.keys(rec)) {
if (key === "loc") continue;
const v = rec[key];
if (isUnregisteredObjectType(v)) rec[key] = F64;
else sweep(v);
}
};
for (const root of roots) sweep(root);
}
/** True when `t` (recursively) names a class instance type with no
* registered ClassInfo — the shape of a JS class whose collection fenced.
* Used by run()'s global pruning; shapes/unions recurse with a seen-set
* (interned ids can nest). */
typeNamesUnregisteredClass(t: IrType, seen: Set<string> = new Set()): boolean {
switch (t.kind) {
case "object":
return !this.classes.has(t.className);
case "array":
case "set":
return this.typeNamesUnregisteredClass(t.elem, seen);
case "map":
return (
this.typeNamesUnregisteredClass(t.key, seen) ||
this.typeNamesUnregisteredClass(t.value, seen)
);
case "promise":
return this.typeNamesUnregisteredClass(t.inner, seen);
case "func":
return (
t.params.some((p) => this.typeNamesUnregisteredClass(p, seen)) ||
this.typeNamesUnregisteredClass(t.ret, seen)
);
case "record": {
if (seen.has(t.shapeId)) return false;
seen.add(t.shapeId);
const shape = this.shapes.get(t.shapeId);
if (!shape) return false;
if (shape.indexValue && this.typeNamesUnregisteredClass(shape.indexValue, seen)) return true;
return shape.fields.some((f) => this.typeNamesUnregisteredClass(f.type, seen));
}
case "union": {
if (seen.has(t.unionId)) return false;
seen.add(t.unionId);
const def = this.unions.get(t.unionId);
return !!def && def.arms.some((a) => this.typeNamesUnregisteredClass(a, seen));
}
default:
return false;
}
}
/** Whether run() counts a signature-blocked declaration in
* stats.functionsSkipped: whole-program passes and the coverage
* remainder do; an externally-gated emit pass leaves the counting to the
* remainder (the declaration was never reached). */
countsSkips(): boolean {
return this.reachable === null || this.remainder;
}
/* ── reachability ─────────────────────────────────────────────────── */
/** Reachable emit: computes the set of body names the program's entry
* reaches and retains each body IR as it lowers. Seeds are the per-file
* init bodies (top-level statements always run); edges fire from
* RESOLUTION sites while a body lowers (noteEdge /
* noteVirtualEdge) — direct calls, closure creation (a taken closure may
* be called indirectly), `new`, super calls, accessor invocations, and
* virtual dispatch. Recording at resolution time (not off the produced
* IR) keeps edges from statements that later poison, so the callee's own
* diagnostics still surface — the collect-everything invariant. Generic
* instances ride the existing monomorphization queue (already
* demand-driven) and lifted lambdas lower inline with their enclosing
* body; both fire edges through the same hooks and are not units
* themselves. */
emitReachable(extraRoots?: readonly string[]): { reachable: Set<string>; result: LowerResult } {
const parts = this.splitFiles();
// Direct lowering callers do not necessarily run program preflight.
// Establish the same managed header/top-level batch here before
// collection, while the production path simply finds warm memos.
this.checker.prefetchSourceFileStructures(parts.map((fp) => fp.sf));
// Signature collection may read the initializer type of a default whose
// body type still admits undefined (for example `value =
// process.env.VALUE`). The expression executes only when reached, but
// that type query is mandatory now; batch hot defaults across ordinary
// declarations before collectProgram visits their signatures.
this.checker.prefetchCollectionTypes(
parts.flatMap((fp) =>
fp.fnDecls
.filter((decl) => decl.body !== undefined && decl.typeParameters === undefined)
.flatMap((decl) =>
decl.parameters.flatMap((param) => param.initializer ? [param.initializer] : []),
),
),
);
// JavaScript class shapes are partly declared by constructor-body
// assignments. Batch those collection-time queries across declarations
// before collectProgram visits them one by one; reached body lowering
// will reuse the same answers later.
this.prefetchClassCollection(parts.flatMap((fp) => fp.classDecls));
this.collectProgram(parts);
this.analyzeRuntimeOptionalArrayReads(parts);
// Decorated classes analyze post-collection here too: the %init seeds
// lower the decoration calls, whose edges (decorator bodies, construct
// thunks) reachable emit must see.
for (const info of this.classes.values()) analyzeClassDecoration(this, info);
this.prepareModuleInits(parts);
prepareCjsModuleGraph(this, parts);
// Every lowerable body, by emitted-function name. The names double as
// retained-function keys and are deterministic by construction
// (qualified declaration names).
const units = new Map<string, {
order: number;
roots: readonly ts.Node[];
lower: () => IrFunction | null;
}>();
const bodyRoots = (...roots: (ts.Node | undefined | null)[]): ts.Node[] =>
roots.filter((root): root is ts.Node => root !== undefined && root !== null);
const functionRoots = (decl: ts.FunctionLikeDeclaration): ts.Node[] => bodyRoots(
...decl.parameters.map((param) => param.initializer),
decl.body,
);
const classCtorRoots = (info: ClassInfo): ts.Node[] => bodyRoots(
...(info.ctor?.parameters ?? []).map((param) => param.initializer),
info.ctor?.body,
...info.fieldOrder.map((field) => field.initializer),
);
const classMemberRoots = (info: ClassInfo): ts.Node[] => [
...classCtorRoots(info),
...[...this.classMethodMembers(info)].flatMap(({ member }) => functionRoots(member)),
...[...(info.staticMethods?.values() ?? [])].flatMap(({ member }) => functionRoots(member)),
];
let unitOrder = 0;
for (const fp of parts) {
for (const decl of fp.fnDecls) {
// Overload signatures share the implementation's symbol (and so
// its FnSig): only the with-body declaration is the unit, or the
// signature's closure would shadow the implementation's.
if (!decl.body) continue;
const declSymbol = declSymbolOf(this, decl);
if (!declSymbol || this.genericFnsBySymbol.has(declSymbol)) continue;
const sig = this.fnSigsBySymbol.get(declSymbol);
if (sig) {
units.set(sig.name, {
order: unitOrder++,
roots: functionRoots(decl),
lower: () => this.lowerFunction(decl),
});
}
}
}
for (const info of this.classes.values()) {
if (info.builtinError) continue; // runtime-provided; nothing lowers
// Generic-class INSTANTIATIONS (and mixin instantiations) are
// demand-driven, not units: their members lower unconditionally in
// the instance drain below — the generic-fn instance rule.
if (info.genericInstance || info.mixinInstance) continue;
const cName = info.def.name;
// A FAMILY has no constructor function and no instance members —
// only its statics are units.
if (!info.generic) {
units.set(`%${cName}.constructor`, {
order: unitOrder++,
roots: classCtorRoots(info),
lower: () => this.lowerClassCtor(info),
});
for (const { mName, member } of this.classMethodMembers(info)) {
units.set(`%${cName}.${mName}`, {
order: unitOrder++,
roots: functionRoots(member),
lower: () => this.lowerClassMethodMember(info, member),
});
}
for (const prop of info.throwingSetters) {
units.set(`%${cName}.set:${prop}`, {
order: unitOrder++,
roots: [],
lower: () => this.throwingSetterFn(info, prop),
});
}
}
for (const [name, entry] of info.staticMethods ?? []) {
units.set(`%${cName}.static:${name}`, {
order: unitOrder++,
roots: functionRoots(entry.member),
lower: () => lowerStaticMethod(this, info, name),
});
}
}
// The old emit pass visited each file's functions and then its classes,
// before every module init. Retained reachability discovers those
// bodies from the inits, but first-seen record metadata still has to
// follow that old order: Object.keys/JSON/inspect observe a shape's
// declaredOrder. Keep output sorting separate — this rank controls only
// that observable metadata.
const metadataPriority = new Map<string, readonly [phase: number, order: number]>();
let declarationMetadataOrder = 0;
const rank = (name: string): void => {
if (units.has(name) && !metadataPriority.has(name)) {
metadataPriority.set(name, [1, declarationMetadataOrder++]);
}
};
for (const fp of parts) {
for (const decl of fp.fnDecls) {
if (!decl.body) continue;
const symbol = declSymbolOf(this, decl);
if (symbol && !this.genericFnsBySymbol.has(symbol)) {
const sig = this.fnSigsBySymbol.get(symbol);
if (sig) rank(sig.name);
}
}
for (const decl of fp.classDecls) {
const info = this.classes.get(this.classNamer(decl));
if (!info) continue;
const cName = info.def.name;
rank(`%${cName}.constructor`);
for (const { mName } of this.classMethodMembers(info)) rank(`%${cName}.${mName}`);
for (const name of info.staticMethods?.keys() ?? []) rank(`%${cName}.static:${name}`);
for (const prop of info.throwingSetters) rank(`%${cName}.set:${prop}`);
}
}
// Defensive fallback for declaration-like units registered outside
// FileParts; they still precede init bodies in the old emit pass.
for (const name of units.keys()) rank(name);
let expressionMetadataOrder = 0;
let instanceMetadataOrder = 0;
const demandOwner = (priority?: readonly [number, number]): GenericDemandOwner => {
const owner: GenericDemandOwner = {
...(priority ? { priority } : {}),
functionDemands: [],
classDemands: [],
};
if (priority) this.genericDemandRoots.push(owner);
return owner;
};
const reachable = new Set<string>();
const queue: string[] = [];
const loweredUnits = new Map<string, IrFunction>();
const initFunctions: IrFunction[] = [];
const instanceFunctions: IrFunction[] = [];
this.onEdge = (name: string): void => {
if (reachable.has(name)) return;
reachable.add(name);
if (units.has(name)) queue.push(name);
};
// Class EXPRESSIONS collect while init bodies lower (below): their
// member units register the moment collection finishes — before any
// edge to them can fire (references require the collected class).
this.onExprClassCollected = (info: ClassInfo): void => {
const cName = info.def.name;
const register = (
name: string,
roots: readonly ts.Node[],
lower: () => IrFunction | null,
): void => {
units.set(name, { order: unitOrder++, roots, lower });
metadataPriority.set(name, [3, expressionMetadataOrder++]);
};
register(`%${cName}.constructor`, classCtorRoots(info), () => this.lowerClassCtor(info));
for (const { mName, member } of this.classMethodMembers(info)) {
register(
`%${cName}.${mName}`,
functionRoots(member),
() => this.lowerClassMethodMember(info, member),
);
}
for (const [name, entry] of info.staticMethods ?? []) {
register(
`%${cName}.static:${name}`,
functionRoots(entry.member),
() => lowerStaticMethod(this, info, name),
);
}
for (const prop of info.throwingSetters) {
register(`%${cName}.set:${prop}`, [], () => this.throwingSetterFn(info, prop));
}
};
// Generic instances queued by the bodies above lower here (an instance
// body fires edges of its own and can queue further instances of
// either kind — function instances and class instantiations drain to
// the joint fixpoint).
let instLowered = 0;
let clsInstLowered = 0;
let specLowered = 0;
const drainInstances = (): void => {
while (
instLowered < this.instantiationQueue.length ||
clsInstLowered < this.genericClassInstances.length ||
specLowered < this.emitSpecQueue.length
) {
while (clsInstLowered < this.genericClassInstances.length) {
const waveEnd = this.genericClassInstances.length;
this.checker.prefetchRoots(
this.genericClassInstances.slice(clsInstLowered, waveEnd).flatMap(classMemberRoots),
);
while (clsInstLowered < waveEnd) {
const info = this.genericClassInstances[clsInstLowered++]!;
const owner = demandOwner();
this.genericClassDemandOwner.set(info, owner);
const ref = this.genericClassDemandPriority.get(info) ?? { rank: [4, instanceMetadataOrder++] };
this.genericClassDemandPriority.set(info, ref);
instanceFunctions.push(...this.withGenericDemandOwner(owner, () =>
this.shapes.withDeclaredOrderPriority(ref, () => this.lowerClassMembers(info))));
}
}
while (instLowered < this.instantiationQueue.length) {
const waveEnd = this.instantiationQueue.length;
this.checker.prefetchRoots(
this.instantiationQueue
.slice(instLowered, waveEnd)
.flatMap(({ info }) => functionRoots(info.decl)),
);
while (instLowered < waveEnd) {
const { info, inst } = this.instantiationQueue[instLowered++]!;
const owner = demandOwner();
this.genericFunctionDemandOwner.set(inst, owner);
const ref = this.genericDemandPriority.get(inst) ?? { rank: [4, instanceMetadataOrder++] };
this.genericDemandPriority.set(inst, ref);
// Body-level poisons skip the instance after retaining the
// diagnostic and every edge fired before poisoning.
try {
instanceFunctions.push(this.withGenericDemandOwner(owner, () =>
this.shapes.withDeclaredOrderPriority(ref, () => this.lowerGenericInstance(info, inst))));
} catch (e) {
if (!(e instanceof PoisonError)) throw e;
}
}
}
// Emit-override specialization bodies fire edges of their own
// (the super-forward chain, closures, generic calls) — lower them
// exactly like generic instances.
while (specLowered < this.emitSpecQueue.length) {
const waveEnd = this.emitSpecQueue.length;
this.checker.prefetchRoots(
this.emitSpecQueue
.slice(specLowered, waveEnd)
.flatMap(({ info }) =>
info.emitOverride ? functionRoots(info.emitOverride.decl) : []),
);
while (specLowered < waveEnd) {
try {
const fn = this.shapes.withDeclaredOrderPriority(
[4, instanceMetadataOrder++],
() => lowerEmitOverrideSpec(this, this.emitSpecQueue[specLowered++]!),
);
if (fn) instanceFunctions.push(fn);
} catch (e) {
if (!(e instanceof PoisonError)) throw e;
}
}
}
}
};
// Module inits are the unconditional root wave. Structure prefetch
// already covered ordinary top-level expressions, but this full-root
// pass also picks up nested/lifted function bodies and the class
// declaration-time code that splitFiles hoists out of topStmts.
this.checker.prefetchRoots([
...parts.flatMap((fp) => fp.topStmts),
...[...this.classes.values()].flatMap((info) => [
...info.staticFields.map((field) => field.initializer),
...(info.staticBlocks ?? []),
...(info.classDecorators?.nodes ?? []),
]),
]);
parts.forEach((fp, index) => {
const priority = [2, index] as const;
const owner = demandOwner(priority);
initFunctions.push(
this.withGenericDemandOwner(
owner,
() => this.shapes.withDeclaredOrderPriority(
priority,
() => this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!),
),
),
);
});
// LIBRARY mode's extra reachability roots (LowerOptions.libRoots): the
// profile-mapped exports are called from outside the graph, so they
// seed the worklist beside the init bodies. Unknown names are inert
// (the export-map resolution reports them as SC4002 later).
for (const root of extraRoots ?? []) this.onEdge?.(root);
const drainUnits = (): void => {
while (queue.length > 0) {
const wave = queue.splice(0);
this.checker.prefetchRoots(wave.flatMap((name) => units.get(name)!.roots));
for (const name of wave) {
// A body-level poison outside the per-statement catches (a fenced
// constructor/method parameter default lowered by declareParams):
// every edge fired before poisoning remains retained; the diagnostic
// stays recorded and the member stays omitted.
try {
const unit = units.get(name)!;
const priority = metadataPriority.get(name) ?? [3, expressionMetadataOrder++];
const owner = demandOwner(priority);
const fn = this.withGenericDemandOwner(
owner,
() => this.shapes.withDeclaredOrderPriority(priority, unit.lower),
);
if (fn) loweredUnits.set(name, fn);
} catch (e) {
if (!(e instanceof PoisonError)) throw e;
}
}
}
};
drainUnits();
this.restoreGenericInstanceOrder();
this.restoreGenericClassInstanceOrder();
// Generic bodies can reach ordinary declarations, whose bodies can in
// turn queue more instances. Continue to the joint fixpoint; the initial
// queue above is the only portion whose discovery order differed from
// historical emit order.
for (;;) {
drainInstances();
if (queue.length === 0) break;
drainUnits();
this.restoreGenericInstanceOrder(instLowered);
this.restoreGenericClassInstanceOrder(clsInstLowered);
}
const orderedUnits = [...loweredUnits]
.sort(([left], [right]) => units.get(left)!.order - units.get(right)!.order)
.map(([, fn]) => fn);
this.settleGenericDemandPriorities();
this.shapes.settleDeclaredOrderPriorities();
for (const finalize of this.shapeOrderMetadataFinalizers) finalize();
for (const finalize of this.shapeOrderHelperFinalizers) finalize();
const functions = [
...orderedUnits,
...initFunctions,
this.buildMain(),
...instanceFunctions,
...this.liftedFns,
...this.implicitFns,
];
this.reachableForArtifacts = reachable;
return { reachable, result: this.finishModule(functions) };
}
/** Reachability hook (see emitReachable): fires when lowering resolves a
* reference to a lowerable body. */
noteEdge(name: string): void {
if (this.onEdge) this.onEdge(name);
}
/** Discovery hook for virtual dispatch: a virtualCall on `info`'s static
* class reaches the nearest declaration at/above it plus every override
* on a STRICT descendant (receivers of sibling branches can't flow into
* this call site; a virtualCall through their own static classes marks
* them). */
noteVirtualEdge(info: ClassInfo, method: string): void {
if (!this.onEdge) return;
const above = this.findMethodOn(info, method);
if (above) this.noteEdge(`%${above.declarer.def.name}.${method}`);
const below = (c: ClassInfo): void => {
for (const s of c.subclasses) {
if (s.methods.has(method)) this.noteEdge(`%${s.def.name}.${method}`);
below(s);
}
};
below(info);
}
moduleArtifacts(functions: IrFunction[]): {
classes: IrClassDef[];
records: IrRecordShape[];
unions: IrUnionDef[];
} {
return moduleArtifacts(this, functions);
}
/* ── diagnostics plumbing ─────────────────────────────────────────── */
/** Converts diagnostics recorded since `diagsBefore` into a runtime
* fence. ICEs stay on the compile-diagnostic path; every other captured
* diagnostic moves to the runtime-fence ledger. Function targets share
* the same capture-free trap-function construction, while statement
* targets return the fence inline. Null means the conversion was not
* eligible (probe mode, no diagnostic/fallback, or an ICE). */
deferToRuntimeFence(
diagsBefore: number,
node: ts.Node,
target: RuntimeFenceStatementTarget,
): IrStmt | null;
deferToRuntimeFence(
diagsBefore: number,
node: ts.Node,
target: RuntimeFenceFunctionTarget,
): IrFunction | null;
deferToRuntimeFence(
diagsBefore: number,
node: ts.Node,
target: RuntimeFenceClosureTarget,
): IrExpr | null;
deferToRuntimeFence(
diagsBefore: number,
node: ts.Node,
target: RuntimeFenceTarget,
): IrStmt | IrFunction | IrExpr | null {
if (
(this.diagSink !== null && target.allowDiagSink !== true) ||
(this.diags.length <= diagsBefore && target.fallback === undefined)
) {
return null;
}
const captured = this.diags.splice(diagsBefore);
if (captured.some((d) => d.code === "SC9001")) {
this.diags.push(...captured);
return null;
}
this.runtimeFences.push(...captured);
const first = captured[0];
const loc = locOf(node);
const code = first?.code ?? target.fallback!.code;
const baseMessage = first?.message ?? target.fallback!.message;
const messageLoc = first?.loc ?? loc;
const source = first
? this.program.getSourceFile(messageLoc.file) ?? node.getSourceFile()
: node.getSourceFile();
const pos = ts.getLineAndCharacterOfPosition(source, messageLoc.start);
const fence: IrStmt = {
kind: "runtimeFence",
code,
message: target.bareMessage
? baseMessage
: `${baseMessage} [${code} at ${messageLoc.file}:${pos.line + 1}]`,
loc,
};
if (target.kind === "statement") return fence;
const params = target.params ?? [];
const name = typeof target.name === "function" ? target.name() : target.name;
const fn: IrFunction = {
name,
params,
returnType: target.returnType,
locals: params.map((p) => ({
id: p.localId,
name: p.name,
type: p.type,
mutable: target.paramsMutable ?? false,
})),
body: [fence],
loc,
};
if (target.async) fn.async = true;
if (target.generator) fn.generator = target.generator;
if (target.kind === "function") return fn;
fn.captures = [];
this.liftedFns.push(fn);
return { kind: "closure", fnName: fn.name, captures: [], type: target.type, loc };
}
/** All diagnostics land here; while a generic instance body is lowering,
* the instantiation context is appended so the user knows which concrete
* types made the (source-anchored) construct fail. */
pushDiag(diag: ScrDiagnostic): void {
const d = this.instantiationContext
? { ...diag, message: `${diag.message} (${this.instantiationContext})` }
: diag;
// Deferred collection: the wrapper decides whether these ever report
// (a reference flushes them; unreferenced declarations stay silent in
// builds and report under coverage's unreached group).
if (this.diagSink) {
this.diagSink.push(d);
return;
}
// One site, one report: some declarations map a type twice (module
// globals pre-register before their initializers lower) — an exact
// duplicate (code + span + message) adds noise, not information.
if (
this.diags.some(
(p) =>
p.code === d.code &&
p.loc.start === d.loc.start &&
p.loc.end === d.loc.end &&
p.message === d.message,
)
) {
return;
}
this.diags.push(d);
}
unsupported(
code: keyof typeof UNSUPPORTED & `SC${number}`,
node: ts.Node,
featureOverride?: string,
hintOverride?: string,
): never {
this.pushDiag(unsupportedDiag(code, locOf(node), featureOverride, hintOverride));
throw new PoisonError();
}
externalHostFence(specifier: string, node: ts.Node, valueUse = true): never {
this.unsupported(
"SC1010",
node,
valueUse
? `values from the '${specifier}' external host module (types supplied by --external-types, but no runtime implementation or scriptc lowering was provided)`
: `the '${specifier}' external host module (types supplied by --external-types, but no runtime implementation or scriptc lowering was provided)`,
`the declaration mapping is analysis-only: coverage continues through project code, while executing ${valueUse ? "this value" : "this module"} requires an embedder integration with explicit runtime semantics`,
);
}
/** The dynamic-family fence for an OPERATION on an `any`-origin
* checked-dynamic value that only the engine can execute (operators,
* iteration, computed member names, ...). Carries SC2011 — the same
* code as the `any` type fence — so the coverage report groups it with
* the dynamic-capable family and the two-tier retry knows the island
* lifts the site. Use exactly when the blocking operand IS dyn-typed
* and its checker type is `any`-flavored; genuine `unknown` keeps the
* SC1100-family fences (tsc constrains what unknown can do, so those
* sites are checker-error territory, not engine territory). */
anyOpFence(feature: string, node: ts.Node): never {
this.pushDiag(anyOpRequiresDynamicDiag(feature, locOf(node)));
throw new PoisonError();
}
/** True when this expression's CHECKER type is `any`-flavored — the
* gate anyOpFence's call sites use to tell `any`-origin dyn values
* (the engine could run the operation) from genuine `unknown` ones. */
anyOrigin(node: ts.Node): boolean {
return (this.typeOf(node).flags & ts.TypeFlags.Any) !== 0;
}
badType(node: ts.Node, type: ts.Type): never {
const widened = this.checker.getBaseTypeOfLiteralType(type);
// Types declared by the ADOPTED @types/node (Buffer, NodeJS.Timeout,
// the undici Response, ...) are supported-surface provenance, not npm
// packages — their values never lower, so the honest blame is the
// SC2020-family fence naming @types/node. Checked FIRST: Buffer has
// an index signature and would otherwise get the misleading use-a-Map
// hint below, and no user-side remedy the other messages suggest
// applies to node-typed values.
const typeSym = widened.getAliasSymbol() ?? widened.getSymbol();
if (this.nodeTypesOnlySymbol(typeSym)) {
this.pushDiag(noLoweringDiag(this.checker.typeToString(type), locOf(node), undefined, true));
throw new PoisonError();
}
// Engine-backed ambient TYPES in a STATIC build report the per-site
// SC2012 rather than the generic supported-types recitation. Native
// fetch values map earlier to checked-dynamic handles and do not reach
// here; constructor objects such as Headers still can.
if (
!this.dynamic &&
typeSym &&
(ISLAND_AMBIENT_TYPES as readonly string[]).includes(typeSym.name) &&
this.isStdlibSymbol(typeSym)
) {
this.pushDiag(requiresDynamicApiDiag(`a value of type '${typeSym.name}'`, locOf(node)));
throw new PoisonError();
}
// ENUM OBJECTS (`typeof e` — a numeric enum's type carries the
// reverse-map index signature, a string enum's just its members):
// shaped like a hybrid record, but the enum identifier has no value
// lowering, so the honest fence names the construct in the same voice
// as the per-site identifier fence — never the index-signature
// recitation.
if (typeSym && (typeSym.flags & ts.SymbolFlags.Enum) !== 0) {
this.pushDiag(
unsupportedDiag(
"SC1090",
locOf(node),
`enum objects as values ('${typeSym.name}' — member reads like '${typeSym.name}.X' compile to constants; the object itself has no runtime representation)`,
),
);
throw new PoisonError();
}
// Would-be records with INDEX SIGNATURES (`Record<string, T>`,
// `{ [k: string]: T }`) get the index-signature fence (SC2006) naming
// the supported key/value domain instead of the generic
// supported-types recitation.
if (
widened.flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection) &&
this.checker.getCallSignatures(widened).length === 0 &&
this.checker.getConstructSignatures(widened).length === 0 &&
!this.checker.isTupleType(widened) &&
!this.checker.isArrayLikeType(widened) &&
this.checker.getIndexInfosOfType(widened).length > 0
) {
// STANDARD-LIBRARY interface/class types that carry index signatures
// (the typed arrays — Int8Array..Float16Array): the honest story is
// the lib fence naming the type (SC2020 — only the supported surface
// compiles), not a use-a-Map hint no user can act on. NOMINAL lib
// declarations only: a lib-declared ALIAS or mapped type
// (`Record<string, object>`) is still a data shape whose own story
// (the value type, the key domain) the fences below tell better.
const ownSym = widened.getSymbol();
if (
ownSym &&
this.checker.declarationsOf(ownSym).some(
(d) =>
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
this.isStdlibFile(d.getSourceFile()),
)
) {
this.pushDiag(noLoweringDiag(this.checker.typeToString(type), locOf(node)));
throw new PoisonError();
}
// The MEASURED island probe, same mechanism as the general dynamic
// reroute below: an index-signature shape the dynamic mapping
// accepts (an `any`-valued signature absorbing into an island
// object, jsval-entangled members) reports the retry-eligible
// SC2011 choice instead of the static recitation.
if (
!this.dynamic &&
!(widened.flags & ts.TypeFlags.Any) &&
mapType(widened, { ...this.typeCtx, dynamic: true }) !== null
) {
this.pushDiag(requiresDynamicTypeDiag(this.checker.typeToString(type), locOf(node)));
throw new PoisonError();
}
this.pushDiag(indexSignatureTypeDiag(this.checker.typeToString(type), locOf(node)));
throw new PoisonError();
}
// Package-declared types in a STATIC build: name the package, not the
// type — the per-package requires-dynamic diagnostic (and the coverage
// report's one-line-per-package attribution). Under --dynamic these
// types map to jsval, so reaching here means the type is genuinely
// unrepresentable (a jsval union arm, a jsval array element) — the
// generic type message tells that story better.
if (!this.dynamic) {
const pkg = this.npmPackageOf(widened);
if (pkg) {
this.pushDiag(requiresDynamicPackageDiag(pkg, locOf(node)));
throw new PoisonError();
}
}
// A type that keeps a GENERIC call signature (`<T>(x: T) => T` slots,
// stored generic functions, higher-order-generic call results): the
// pointed monomorphization message instead of the recitation. Union
// arms count — a `(<T>(x: T) => T) | undefined` slot is the same
// story through its callable arm. After the package check: a
// package-declared generic signature stays the package's story.
{
const parts = widened.isUnionType() ? ts.constituentTypes(widened) : [widened];
if (
parts.some((p) =>
this.checker.getCallSignatures(p).some((s) => s.getTypeParameters().length > 0),
)
) {
this.pushDiag(genericSignatureTypeDiag(this.checker.typeToString(type), locOf(node)));
throw new PoisonError();
}
}
// A type the DYNAMIC mapping accepts (`any[]`, records/functions with
// `any`-typed members, .d.ts-declared shapes whose values are island
// handles): the honest per-site story is the dynamic-family choice, not
// the supported-types recitation — proved by re-running mapType with
// `dynamic: true`, never guessed from the type text. Probing is safe
// here: a diagnostic means this build already failed, so anything the
// probe interns or registers on the way is never emitted. Checked LAST
// so every more specific story above (island ambients, index
// signatures, per-package attribution, generic signatures) keeps its
// own fence class. Bare `any` is excepted: unsupportedTypeDiag's own
// SC2011 arm tells that story with the stronger stay-static remedy
// ('unknown' + a checked cast).
if (
!this.dynamic &&
!(widened.flags & ts.TypeFlags.Any) &&
mapType(widened, { ...this.typeCtx, dynamic: true }) !== null
) {
this.pushDiag(requiresDynamicTypeDiag(this.checker.typeToString(type), locOf(node)));
throw new PoisonError();
}
// STANDARD-LIBRARY nominal provenance, decided once: interface/class
// declarations in the lib's own files (Date, ArrayBuffer, WeakMap, the
// iterator/constructor interfaces). Such types report the SC2020 story
// below — the same one the index-signature-carrying lib types above
// tell — rather than an overload/record claim no user can act on.
const stdlibOwnSym = widened.getSymbol();
const stdlibNominal =
stdlibOwnSym !== undefined &&
this.checker.declarationsOf(stdlibOwnSym).some(
(d) =>
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
this.isStdlibFile(d.getSourceFile()),
);
// OVERLOADED call signatures (SC2007) — after the generic branch, so a
// generic overload set keeps the monomorphization story: a value of a
// multi-signature type has no single compiled signature to hold.
if (!stdlibNominal && this.checker.getCallSignatures(widened).length > 1) {
this.pushDiag(overloadedSignatureTypeDiag(this.checker.typeToString(type), locOf(node)));
throw new PoisonError();
}
// INTERSECTIONS that resolved to no lowering (SC2008). The ones that
// compile never get here: member intersections intern through the
// record path, callable hybrids map to '%call' records, and pinned
// mixin instantiations resolve by chain structure.
if (widened.isIntersectionType()) {
this.pushDiag(intersectionTypeDiag(this.checker.typeToString(type), locOf(node)));
throw new PoisonError();
}
// SUPPORTED shapes over a component outside its slot (SC2009): the
// Map/Set domains, array/tuple elements, union arms, function
// parameters/returns. The container is not the blocker — name the
// component instead of reciting the supported set. Checked BEFORE the
// lib claim so Map/Set/Promise instantiation failures keep their
// component story (describeComponentBlocker always answers for those
// heads).
{
const detail = describeComponentBlocker(widened, this.typeCtx);
if (detail !== null) {
this.pushDiag(componentTypeDiag(this.checker.typeToString(type), detail, locOf(node)));
throw new PoisonError();
}
}
// STANDARD-LIBRARY nominal types with no lowering at all: the SC2020
// story, naming the type — and for the families with a WHY, the same
// pointed reason the identifier/constructor chokepoints teach.
if (stdlibNominal) {
const intlHint =
"Intl formatter values have no representation — the COMPOSED en-US forms lower: " +
'new Intl.NumberFormat("en-US").format(x) and x.toLocaleString("en-US") with default options; ' +
"the rest is ICU locale data the binary does not carry";
const typeHints: Record<string, string | undefined> = {
ArrayBuffer:
"no free-standing ArrayBuffer value exists — typed arrays own their storage " +
"(new Uint8Array(n) allocates; new Uint8Array(new ArrayBuffer(n)) erases the buffer into the view)",
SharedArrayBuffer:
"no shared-memory threads exist in a compiled program — Uint8Array is the byte storage",
NumberFormat: intlHint,
DateTimeFormat: intlHint,
DurationFormat: intlHint,
PluralRules: intlHint,
Collator: intlHint,
ListFormat: intlHint,
RelativeTimeFormat: intlHint,
Segmenter: intlHint,
DisplayNames: intlHint,
TextEncoder:
"TextEncoder instances support encode(string) and encode(); other codec operations require a dedicated lowering",
TextDecoder:
"TextDecoder instances support recognized literal labels with default options and whole-buffer decode(Uint8Array); runtime labels and options remain unsupported",
};
this.pushDiag(
noLoweringDiag(this.checker.typeToString(type), locOf(node), typeHints[stdlibOwnSym?.name ?? ""]),
);
throw new PoisonError();
}
// USER record shapes blocked by ONE member (SC2009's record arm):
// name the member and its type.
{
const detail = describeRecordMemberBlocker(widened, this.typeCtx);
if (detail !== null) {
this.pushDiag(componentTypeDiag(this.checker.typeToString(type), detail, locOf(node)));
throw new PoisonError();
}
}
this.pushDiag(unsupportedTypeDiag(this.checker.typeToString(type), locOf(node)));
throw new PoisonError();
}
/** The lib fence (SC2020): a reached use of standard-library surface
* nothing lowers. Poisons the statement like every other rejection.
* `sym`, when given, picks the wording: surface declared only by the
* adopted @types/node is blamed at @types/node. */
noLowering(surface: string, node: ts.Node, hint?: string, sym?: ts.Symbol | null): never {
this.pushDiag(noLoweringDiag(surface, locOf(node), hint, this.nodeTypesOnlySymbol(sym)));
throw new PoisonError();
}
stdlibMemberFence(access: ts.PropertyAccessExpression): void {
return stdlibMemberFence(this, access);
}
fenceStaticResponseMember(
access: ts.PropertyAccessExpression | ts.ElementAccessExpression,
use: "read" | "call",
): IrExpr | null {
return fenceStaticResponseMember(this, access, use);
}
fenceUnsupportedFetchConstructorMember(
access: ts.PropertyAccessExpression | ts.ElementAccessExpression,
): IrExpr | null {
return fenceUnsupportedFetchConstructorMember(this, access);
}
fenceStaticHeadersMember(
access: ts.PropertyAccessExpression | ts.ElementAccessExpression,
use: "read" | "call",
): IrExpr | null {
return fenceStaticHeadersMember(this, access, use);
}
fenceStaticAbortControllerMemberRead(
access: ts.PropertyAccessExpression | ts.ElementAccessExpression,
): IrExpr | null {
return fenceStaticAbortControllerMemberRead(this, access);
}
fenceStaticHeadersIteration(node: ts.Node): void {
return fenceStaticHeadersIteration(this, node);
}
fenceFetchObjectAssignment(
target: ts.ObjectLiteralExpression,
source: ts.Expression,
): void {
return fenceFetchObjectAssignment(this, target, source);
}
lowerDynamicHeadersIteratorCall(
call: ts.CallExpression,
access: ts.ElementAccessExpression,
): IrExpr | null {
return lowerDynamicHeadersIteratorCall(this, call, access);
}
lowerDynamicHeadersSpread(
node: ts.Expression,
type: IrType & { kind: "array" },
): IrExpr | null {
return lowerDynamicHeadersSpread(this, node, type);
}
fenceStaticReadableStreamMember(
access: ts.PropertyAccessExpression | ts.ElementAccessExpression,
use: "read" | "call",
): IrExpr | null {
return fenceStaticReadableStreamMember(this, access, use);
}
typeOf(node: ts.Node): ts.Type {
// Inside an optional-chain body the guarded receiver is typed by its
// NON-NULLISH type (the chain's tag test proved it), so every
// receiver-kind check downstream sees the narrowed arm.
const narrowed = this.chainNarrowedType.get(node);
if (narrowed) return narrowed;
const t = this.checker.getTypeAtLocation(node);
// ALIASED-TYPEOF narrowing: inside a branch a `type === 'string'`
// test proves (type = typeof val, both never reassigned), references
// to the tested operand answer the proven arm — the var/let alias
// form the checker only narrows for consts. Checked before the
// implicit-param hook: a branch narrow is strictly more specific
// than a call-site binding.
if (this.aliasNarrowTypes.size > 0 && ts.isIdentifier(node)) {
const sym = this.checker.getSymbolAtLocation(node);
const arm = sym !== undefined ? this.aliasNarrowTypes.get(sym) : undefined;
if (arm !== undefined) return arm;
}
// IMPLICIT-ANY instance bodies: an identifier reference to a BOUND
// param answers the call site's concrete type wherever the checker
// still says `any` (there is no `T` for mapType to substitute — the
// binding rides here instead). Where tsc's own flow analysis DID
// narrow the `any` (typeof/instanceof guards), a narrow CONSISTENT
// with the binding wins — it IS the binding, an arm of it, or a
// subclass; a CONTRADICTING narrow is the statically-dead branch of a
// typeof dispatch this instantiation cannot take, and answering the
// bound type there keeps dead branches on honest fences instead of
// lowering the live value under a lying type.
if (this.implicitParamTypes !== null && ts.isIdentifier(node)) {
const sym = this.checker.getSymbolAtLocation(node);
const bound = sym !== undefined ? this.implicitParamTypes.get(sym) : undefined;
if (bound !== undefined && bound !== t) {
if (t.flags & ts.TypeFlags.Any) return bound;
const narrowedIr = this.mapTypeOf(t);
const boundIr = this.mapTypeOf(bound);
if (narrowedIr === null || boundIr === null) return bound;
if (typeEquals(narrowedIr, boundIr)) return t;
// A union binding narrowed to one of its arms (typeof/equality
// guards over string|number bindings) — the narrow is truth.
if (boundIr.kind === "union") {
const arms = this.unions.get(boundIr.unionId)?.arms ?? [];
const nArms =
narrowedIr.kind === "union" ? (this.unions.get(narrowedIr.unionId)?.arms ?? [narrowedIr]) : [narrowedIr];
if (nArms.every((n) => arms.some((a) => typeEquals(a, n)))) return t;
return bound;
}
// An instanceof narrow to a SUBCLASS of the bound class — truth.
if (
boundIr.kind === "object" && narrowedIr.kind === "object" &&
this.isSubclassOf(narrowedIr.className, boundIr.className)
) {
return t;
}
return bound;
}
}
return t;
}
/** mapType with this Lowerer's registries and (while a generic instance
* body lowers) type-parameter bindings threaded through. */
mapTypeOf(t: ts.Type): IrType | null {
const mapped = mapType(t, this.typeCtx);
return mapped?.kind === "func" ? jsArgumentsFunctionType(this, t) ?? mapped : mapped;
}
/** The one position where a contextual UNION must not be adopted over the
* expression's own: the LEFT operand of `&&`, `||`, or `??`. Adopting a
* contextual union is normally safe because tsc proved the value
* assignable to the slot; that proof does not exist here, because tsc
* builds a logical operator's result by DROPPING the left's falsy (or
* nullish) arms. `const s: string | null = (c ? env : undefined) || null`
* contextually types the ternary `string | null` while its value is
* `string | undefined` — adopting that strands the very arm the operator
* exists to answer, throwing where Node yields the default. Such operands
* represent by their own union; the operator's own lowering re-tags on
* the branch where the dropped arms are gone. Narrow by design: only the
* union choice is unsound here. A contextual ARRAY still types the
* element, and an unmappable own type still falls back to the context. */
inLogicalLeftPosition(node: ts.Expression): boolean {
let n: ts.Node = node;
while (n.parent && ts.isParenthesizedExpression(n.parent)) n = n.parent;
const p = n.parent;
if (!p || !ts.isBinaryExpression(p) || p.left !== n) return false;
const k = p.operatorToken.kind;
return (
k === ts.SyntaxKind.AmpersandAmpersandToken ||
k === ts.SyntaxKind.BarBarToken ||
k === ts.SyntaxKind.QuestionQuestionToken
);
}
/** True when a ?. token blocks this lowering — i.e. it is NOT the one an
* active optional-chain lowering is currently handling. Every receiver-
* typed lowering that supports chained receivers guards with this
* instead of a raw questionDotToken check. */
chainBlocked(
...nodes: (ts.CallExpression | ts.PropertyAccessExpression | ts.ElementAccessExpression)[]
): boolean {
return nodes.some((n) => n.questionDotToken !== undefined && !this.chainHandled.has(n));
}
/** formatIrType with this Lowerer's registries (records and unions expand
* to their structure in diagnostics). */
fmt(t: IrType): string {
return formatIrType(t, this.shapes, this.unions);
}
/** Whether an IR value has JavaScript Array identity. Homogeneous arrays
* use the native array representation; non-empty fixed tuples use a
* positional record shape so their slots can keep distinct types, but
* Array.isArray must still answer true for both representations. */
isArrayValueType(t: IrType): boolean {
return t.kind === "array" || (t.kind === "record" && this.shapes.get(t.shapeId)?.tuple === true);
}
/** Runtime tags of every JavaScript-array arm in one union. Kept beside
* isArrayValueType so Array.isArray's predicate and its narrowing bridge
* cannot disagree about fixed tuple arms. */
arrayValueTags(unionId: string): number[] {
const def = this.unions.get(unionId);
return def ? def.arms.flatMap((arm, tag) => (this.isArrayValueType(arm) ? [tag] : [])) : [];
}
/** Checked JSON conversion excludes undefined array slots because JSON
* text cannot preserve that distinction. */
jsonSafe(t: IrType): boolean {
return isJsonSafeType(
t,
(id) => this.shapes.get(id),
(id) => this.unions.get(id),
);
}
jsonStringifySafe(t: IrType): boolean {
return isJsonStringifySafeType(
t,
(id) => this.shapes.get(id),
(id) => this.unions.get(id),
);
}
/** True when a type is a BARE undefined-armed union — the one JSON-unsafe
* shape whose rejections deserve their own wording: Node's stringify of
* bare undefined is not a string at all and JSON text never matches the
* arm, so exactness is unreachable and the fixes (narrow first / null arm
* / make it an optional record FIELD, where drop-and-absent semantics ARE
* Node's) are specific. Record fields don't count: an undefined-armed
* union in field position is JSON-safe (isJsonSafeType), so a record that
* still fails the fence does so for some other reason. */
bareUndefinedArmedUnion(t: IrType): boolean {
return isUndefinedArmedUnion(t, (id) => this.unions.get(id));
}
/** canCrossIslandBoundary with this Lowerer's registries — THE test
* behind every marshal/exit decision (the implicit coercions, jsvalIn,
* the checked island-exit cast). Rejections that follow a false answer
* speak through boundaryIntoIslandMsg / boundaryOutOfIslandMsg. */
boundarySafe(t: IrType): boolean {
return canCrossIslandBoundary(
t,
(id) => this.shapes.get(id),
(id) => this.unions.get(id),
);
}
/** canExitIslandToType with this Lowerer's registries — the EXIT
* direction's slightly wider test (bare undefined-armed unions of
* JSON-safe data arms exit; the engine's undefined takes the undefined
* arm before the JSON round trip). */
boundaryExitSafe(t: IrType): boolean {
return canExitIslandToType(
t,
(id) => this.shapes.get(id),
(id) => this.unions.get(id),
);
}
isIslandExpr(node: ts.Expression): boolean {
return isIslandExpr(this, node);
}
/** True when this node's CHECKER type is proven `any[]`/`unknown[]`,
* directly or through the intersection tsc builds for a readonly tuple
* union (`(Model | readonly [Model, Command]) & any[]`; TS7 distributes
* this over the union arms). These are forms of the `arg is any[]`
* Array.isArray predicate; the VALUE behind them can still be a real
* static array/tuple (maybeNarrow's bridge extracts the union's one
* array-valued arm), so receiver-typed dispatch falls back to the LOWERED
* type under this test. */
checkerAnyArray(node: ts.Expression): boolean {
return this.checkerAnyArrayType(this.typeOf(node));
}
/** Type-level half of checkerAnyArray, for narrowing sites that already
* queried the checker type from a general AST node. */
checkerAnyArrayType(t: ts.Type): boolean {
const isAnyArray = (part: ts.Type): boolean =>
this.checker.isArrayType(part) &&
((this.checker.getTypeArguments(part as ts.TypeReference)[0]?.flags ?? 0) &
(ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0;
const visit = (part: ts.Type): boolean => {
if (isAnyArray(part)) return true;
// 5.9 leaves `(U) & any[]` as one intersection; 7 distributes it
// into `(A & any[]) | (B & any[])`. An intersection is array-proven
// when one constituent is; a union is array-proven only when EVERY
// arm is, so an ordinary `any[] | string` never qualifies.
if ((part.flags & ts.TypeFlags.Intersection) !== 0) {
return ts.constituentTypes(part).some(visit);
}
if (part.isUnionType()) return ts.constituentTypes(part).every(visit);
return false;
};
return visit(t);
}
/** The lowered static array/tuple value behind checkerAnyArray's
* synthetic `any[]` spelling. Array.isArray can leave readonly tuple
* unions as intersections that do not map directly, while maybeNarrow
* still extracts the runtime-proven array arm from the original union.
* Receiver dispatchers use this bridge instead of the synthetic type. */
checkerArrayValue(node: ts.Expression): IrExpr | null {
if (!this.checkerAnyArray(node)) return null;
const value = this.lowerExpr(node);
return this.isArrayValueType(value.type) ? value : null;
}
/** Substitutes a bound type parameter anywhere inside mapType's recursion
* (`T`, `T[]`, `{ v: T }`, `(x: T) => T` all resolve). Inert outside
* generic instantiation. */
readonly typeParamResolver = (t: ts.Type): IrType | null => {
if (!this.typeParamBindings || !(t.flags & ts.TypeFlags.TypeParameter)) return null;
const sym: ts.Symbol | undefined = t.getSymbol();
return (sym && this.typeParamBindings.get(sym)) ?? null;
};
/** The ts-level twin of typeParamResolver: the bound CHECKER type of a
* type parameter in the current instantiation (typeParamTsBindings), for
* the resolutions where mapType's widening already dropped what the body
* needs (indexed accesses over literal-bound keys). Inert outside
* call-keyed generic instantiation. */
readonly typeParamTsResolver = (t: ts.Type): ts.Type | null => {
if (!this.typeParamTsBindings || !(t.flags & ts.TypeFlags.TypeParameter)) return null;
const sym: ts.Symbol | undefined = t.getSymbol();
return (sym && this.typeParamTsBindings.get(sym)) ?? null;
};
irTypeOf(node: ts.Node): IrType {
const t = this.typeOf(node);
// JS inference can leave mutable fields, accessor parameters and
// locals at undefined. They still carry values; void has no storage.
if (isJsSourceFile(node.getSourceFile()) && (t.flags & ts.TypeFlags.Undefined) !== 0) return DYN;
// A never-tainted JS type (neverTaintedJsType) maps — never rides as
// f64 — but must not: pre-empt the mapping so the JS fallback below
// answers instead.
const mapped = neverTaintedJsType(this, node, t) ? null : this.mapTypeOf(t);
if (!mapped) {
const argumentsType = jsArgumentsFunctionType(this, t);
if (argumentsType !== null) return argumentsType;
// The checked-dynamic declaration fallback (dynFallbackType): a
// JAVASCRIPT binding of any inference residue, or a TypeScript
// binding of genuine checker-`any`, becomes the checked-dynamic
// kind instead of a compile fence — 'unknown' with the boundary
// checks coerceToExpected already applies (dynFrom into the slot,
// validated dynCheck out) and per-site fences for operations dyn
// cannot carry. JS arrays keep their array-ness (any[]/never[]
// evolving arrays become unknown[]), so length/push/index still
// lower.
const dyn = dynFallbackType(this, node, t);
if (dyn) return dyn;
this.badType(node, t);
}
return jsOpenObjectType(node, mapped, this.shapes, this.unions);
}
/** Exact-shape enforcement (SC2002). Records are monomorphic structs, so
* everywhere a value flows into a typed slot (call arg, initializer,
* assignment, field, return) the shapes must MATCH — or width-coerce:
* coerceToExpected already ran widthCoerce (the copy-reshape family),
* so what reaches here is the RESIDUE its rules decline, and the
* diagnostic names the first blocking rule (describeRecordWidthBlocker).
* Non-record mismatches are not reachable through tsc-clean programs; the
* validator ICEs on them as the usual backstop. */
requireExactShape(node: ts.Node, actual: IrType, expected: IrType): void {
if (typeEquals(actual, expected)) return;
// dyn mismatches first. A dyn ('unknown') value flowing into a typed
// slot needs a CHECKED cast — the hint points at `as <type>`; tsc
// usually rejects this before we do, so the fence is mostly defensive.
// The reverse — a TYPED value flowing into an 'unknown' slot
// (`const u: unknown = 5`, an unknown-typed param/return) — IS tsc-clean
// and rejected here: a typed value has no dynamic representation
// (constructing a dyn from static values is deliberately out this
// round; only JSON.parse results are dyn).
if (actual.kind === "dyn") {
this.unsupported(
"SC1100",
node,
`passing 'unknown' values where '${this.fmt(expected)}' is expected`,
);
}
if (expected.kind === "dyn") {
// Function values BOX into dyn when their signature crosses
// (canBoxFuncIntoDyn — coerceToExpected already converted those), so
// reaching here with a func means a param/result type outside the
// conversion domains, a generic signature's residue, or an overload
// set — name the shape instead of the generic typed-to-unknown
// wording.
if (actual.kind === "func") {
this.unsupported(
"SC1101",
node,
`passing '${this.fmt(actual)}' function values into 'unknown' slots (a parameter or result type has no dynamic representation — only JSON-safe data, Uint8Array, undefined-armed unions of those, 'unknown', and functions over the same set cross)`,
);
}
this.unsupported("SC1101", node);
}
// jsval mismatches surviving coerceToExpected involve a type with no
// island representation (in) or no validated exit (out).
if (actual.kind === "jsval") {
this.unsupported("SC1090", node, boundaryOutOfIslandMsg(this.fmt(expected)));
}
if (expected.kind === "jsval") {
this.unsupported("SC1090", node, boundaryIntoIslandMsg(this.fmt(actual)));
}
// Class-value mismatches: the pointed stories — a widening whose
// constructor ABIs differ (construction through the slot would
// dispatch a mismatched signature), or a structural flow between
// unrelated classes (nominal identity is the IR's only class
// subtyping, instances and values alike).
if (actual.kind === "classval" && expected.kind === "classval") {
const sub = this.classes.get(actual.className);
const sup = this.classes.get(expected.className);
if (sub && sup && this.isSubclassOf(actual.className, expected.className)) {
this.unsupported(
"SC1090",
node,
`class values whose constructor signatures differ ('${this.fmt(actual)}' into a '${this.fmt(expected)}' slot: construction through the slot completes against the base signature, which '${sub.def.jsName ?? actual.className}' does not share — declare matching constructor parameters)`,
);
}
this.unsupported(
"SC1090",
node,
`structurally-typed class-value flows ('${this.fmt(actual)}' into a '${this.fmt(expected)}' slot: only a class and its subclasses share a slot — extend the base class)`,
);
}
// Union-involving mismatches first: a union flowing into a different
// union's slot (even a superset) would need a runtime re-tag — its own
// diagnostic, not the record one. Arm-into-union coercions were already
// wrapped by coerceToExpected before this check runs.
if (containsUnion(actual) || containsUnion(expected)) {
this.pushDiag(unionMismatchDiag(this.fmt(expected), this.fmt(actual), locOf(node)));
throw new PoisonError();
}
if (containsRecord(actual) || containsRecord(expected)) {
// A record→record pair gets the pointed story: WHICH width rule
// declined (a field that doesn't lift, a required field with no
// source, an index signature that could hold the completed key).
const detail =
actual.kind === "record" && expected.kind === "record"
? (this.describeRecordWidthBlocker(actual.shapeId, expected.shapeId) ?? undefined)
: undefined;
this.pushDiag(recordShapeMismatchDiag(this.fmt(expected), this.fmt(actual), locOf(node), detail));
throw new PoisonError();
}
// Everything else — a plain-kind mismatch like a string flowing into a
// class-instance slot — is tsc-rejected in the lowering world and only
// reaches here through preflight's project-world second chance (e.g. a
// Promise reject called with a non-Error reason, clean under the lib's
// `reason?: any`). The honest fence; silence would hand the emitter a
// reinterpret and the validator an ICE.
this.unsupported(
"SC1090",
node,
`'${this.fmt(actual)}' values where '${this.fmt(expected)}' is expected`,
);
}
/** Unwraps a HYBRID (function-with-properties) record to its callable:
* a record whose shape carries the reserved `%call` func field reads
* that field; anything else returns unchanged. The consumer half of
* type-mapper.ts's chalk-shape mapping — call paths and func-slot coercions
* share it. */
hybridCallUnwrap(expr: IrExpr): IrExpr {
if (expr.type.kind !== "record") return expr;
const shape = this.shapes.get(expr.type.shapeId);
const call = shape?.fields.find((f) => f.name === "%call");
if (!call || call.type.kind !== "func") return expr;
return { kind: "recordGet", obj: expr, shapeId: expr.type.shapeId, field: "%call", type: call.type, loc: expr.loc };
}
/** The tag of the union arm equal to `arm`, or -1 (unknown union / no
* such arm). Arm lists are canonical (typeKey-sorted) and interned, so
* this is THE tag for that (union, arm) pair program-wide — every wrap,
* narrow, and tag test agrees by construction. */
armTag(unionId: string, arm: IrType): number {
const def = this.unions.get(unionId);
return def ? def.arms.findIndex((a) => typeEquals(a, arm)) : -1;
}
/** Implicit union construction. Wherever a value flows into a typed slot
* (initializer, assignment, call argument, return, field write, record
* literal field, ternary arm) whose expected type is a union and the
* value's type is one of its arms, wrap it in a `unionWrap` carrying the
* arm's canonical tag. Same-union values pass through untouched; anything
* else (including a DIFFERENT union) is left for requireExactShape, which
* rejects union mismatches with SC2003. */
coerceToExpected(expr: IrExpr, expected: IrType): IrExpr {
// Island boundary, both directions. IN: any static value flowing into
// an any-typed slot marshals implicitly (tsc allows the assignment;
// the marshal is where its semantics live). OUT: an 'any' value
// flowing into a typed slot compiles to a VALIDATED exit — like every
// dyn→static edge, trust-but-verify: a lying `any` throws a catchable
// TypeError instead of corrupting memory (SEMANTICS.md). Unmarshalable
// and unextractable types fall through to requireExactShape's fences.
if (expected.kind === "jsval" && expr.type.kind !== "jsval") {
// Bare unit literals: the engine's own undefined/null (units have no
// other producers, so dropping the operand loses nothing).
if (isUnitType(expr.type)) {
return { kind: "jsOp", op: expr.type.kind === "undefinedT" ? "undefLit" : "nullLit", args: [], type: JSVAL, loc: expr.loc };
}
// A CHECKED-DYNAMIC (dyn/'unknown') value entering the island (the
// `isJson ? JSON.parse(text) : islandParser(text)` config ternary):
// the dyn tree deep-copies into engine values — data kinds only; a
// dyn carrying a boxed function/handle/promise throws the catchable
// TypeError at runtime (trust-but-verify, like every boundary).
if (expr.type.kind === "dyn") {
return { kind: "jsMarshal", value: expr, type: JSVAL, loc: expr.loc };
}
if (this.boundarySafe(expr.type)) {
return { kind: "jsMarshal", value: expr, type: JSVAL, loc: expr.loc };
}
// Closures cross as host functions when their signature marshals —
// the same shapes jsvalIn admits at call arguments (`const f: any =
// (x: number) => x * 3` is the declaration-slot spelling of the
// package-callback pattern).
if (
expr.type.kind === "func" &&
canMarshalTypedFuncIntoIsland(expr.type, (id) => this.shapes.get(id), (id) => this.unions.get(id))
) {
return { kind: "jsMarshal", value: expr, type: JSVAL, loc: expr.loc };
}
// jsval-BEARING composites (a record holding `any` fields, an array
// of such records) have no JSON marshal but an honest per-field
// island construction — see jsvalLiftExpr.
if (this.jsvalLiftable(expr.type)) {
return this.jsvalLiftExpr(expr, expr.loc);
}
// A RegExp flowing into an 'any' slot: the fresh-engine-RegExp
// rebuild (see jsvalIn's regex rule); computed regex values fall
// through to requireExactShape's fence.
if (expr.type.kind === "regex") {
const re = islandRegexpOf(expr);
if (re) return re;
}
return expr;
}
if (expr.type.kind === "jsval" && expected.kind !== "jsval") {
// The jsval→dyn crossing (the world unification's engine-handle
// kind): an 'any'-typed engine value flowing into an 'unknown'/
// 'object'/JS-residue slot wraps BY REFERENCE as the checked-dynamic tree's island
// kind — engine scalars normalize to native dyn kinds at wrap time,
// typeof/truthiness/String()/=== route to the engine, un-armed dyn
// walks fence loudly, and the value unwraps back identity-preserved
// (scr_jsval_from_dyn). Monotone under evolution/widening: a value
// wraps once at its first dyn edge and stays valid through every
// subsequent dyn slot; narrowing never changes representation.
// This wrap RETIRES the silent-wrong-answer fence-closure box for
// jsval members of dyn object literals (lowerDynObjectLiteral's
// coercion lands here first).
if (expected.kind === "dyn") {
return { kind: "dynFromJsval", value: expr, type: DYN, loc: expr.loc };
}
// An island value flowing into a PROMISE-typed slot (the inferred
// `loadPlugins` return — `Promise.all(...)` lowered engine-side
// against a Promise<any[]> inference): the island→static promise
// bridge — the engine promise settles a fresh static promise (void
// fulfillments drop, `any[]`-declared fulfillments exit
// Array.isArray-gated by reference at the settle, 'any' parks as an
// island handle). Only inner shapes the bridge can deliver take the
// arm; the rest keep the exit fence with the type named.
if (
expected.kind === "promise" &&
(expected.inner.kind === "void" ||
expected.inner.kind === "jsval" ||
(expected.inner.kind === "array" && expected.inner.elem.kind === "jsval"))
) {
return { kind: "jsBridgePromise", value: expr, type: expected, loc: expr.loc };
}
// The exit set: everything round-trippable, plus bare undefined-armed
// unions of JSON-safe data arms (the engine's undefined takes the
// undefined arm before the JSON detour) — canExitIslandToType.
if (this.boundaryExitSafe(expected)) {
return { kind: "jsExit", value: expr, type: expected, loc: expr.loc };
}
return expr;
}
// A TYPED value flowing into an 'unknown' slot (`const u: unknown = 5`,
// an unknown-typed param/return, a dyn-valued index slot): the
// static→dyn conversion — dynFrom, a DEEP COPY (the jsMarshal aliasing
// stance intoIndexValueSlot documents; SEMANTICS.md). Bare
// undefined/null literals store the dyn unit values. Types outside the
// dyn's domain (bytes, classes, Maps, ...) fall through to
// requireExactShape's SC1101 fence.
// A promise flowing into a VOID-promise slot (an inferred
// Promise<never> return holding a `return Promise.reject(value)` the
// dyn arm typed promise<dyn>): awaiting through the slot ignores the
// fulfillment payload (scr_await_void) and rejections flow untyped,
// so the value passes through — one C representation, no adapter.
if (
expected.kind === "promise" &&
expected.inner.kind === "void" &&
expr.type.kind === "promise" &&
expr.type.inner.kind !== "void"
) {
return { kind: "promiseVoidWiden", value: expr, type: expected, loc: expr.loc };
}
if (expected.kind === "dyn" && expr.type.kind !== "dyn") {
// An error-HIERARCHY object (builtin subclass or user `extends
// Error` class) upcasts to the %Error root first — the caughtToDyn
// encoding (scr_dyn_from_error) carries name/message/code and the
// runtime CACHES the identity edge, so `instanceof TypeError` on
// the dyn side still answers exactly (dyn.errInstanceof). Only the
// root spelling was convertible before; the harness passes typed
// errors into untyped helpers constantly.
if (expr.type.kind === "object" && this.errorHierarchyClassOf(expr.type.className)) {
return {
kind: "dynFrom",
value: this.upcastTo(expr, "%Error"),
type: DYN,
loc: expr.loc,
};
}
if (expr.kind === "unitLit" || this.dynConvertible(expr.type)) {
return { kind: "dynFrom", value: expr, type: DYN, loc: expr.loc };
}
return expr;
}
// A dyn ('unknown') ACTUAL flowing into a typed slot the checker
// approved (an assertion function's narrowing, the error-any world's
// forgiven chains): the VALIDATED extraction — dynCheck, the
// checked-cast machinery, applied automatically. Trust-but-verify,
// exactly the island exit's stance: a value that doesn't match the
// slot's type throws a catchable TypeError instead of misreading the
// payload. Only dynCheck's own domain (JSON-representable types,
// undefined-armed unions of those, bytes<u8>, the %Error root)
// converts; everything else keeps requireExactShape's SC1100 fence.
if (expr.type.kind === "dyn" && expected.kind !== "dyn") {
const undefArmedOk =
expected.kind === "union" &&
(this.unions
.get(expected.unionId)
?.arms.every(
(a) =>
a.kind === "undefinedT" ||
this.jsonSafe(a) ||
isDynTypedRefType(a) ||
DYN_HANDLE_KINDS.has(a.kind),
) ??
false);
const bytesOk = expected.kind === "bytes" && expected.elem === "u8";
const errorOk = expected.kind === "object" && expected.className === "%Error";
// Program and runtime class instances can return only from a
// compiler-owned typed-reference capsule. A parsed/plain dyn object
// fails the runtime check; an exact capsule unwraps the original
// instance and preserves identity.
const classOk = isDynTypedRefType(expected);
// ADAPTABLE function targets (the checked-dynamic function
// boundary's OUT direction — `const wrapped: F = mustCall(fn)`):
// check callable-kind, then unwrap an identical boxed signature
// directly or adapt through a per-target shim that dynChecks
// arguments/results per F.
const funcOk =
expected.kind === "func" &&
canAdaptDynFuncTo(expected, (id) => this.shapes.get(id), (id) => this.unions.get(id));
const handleOk = DYN_HANDLE_KINDS.has(expected.kind);
if (canDynCheckTo(expected, (id) => this.shapes.get(id), (id) => this.unions.get(id)) || undefArmedOk || bytesOk || errorOk || classOk || funcOk || handleOk) {
return { kind: "dynCheck", value: expr, type: expected, loc: expr.loc };
}
return expr;
}
// A HYBRID (function-with-properties) record flowing into a plain
// func slot extracts its reserved %call field — the chalk shape's
// "the value IS callable" half (type-mapper.ts's hybrid mapping).
if (expected.kind === "func" && expr.type.kind === "record") {
const un = this.hybridCallUnwrap(expr);
if (un !== expr && typeEquals(un.type, expected)) return un;
}
// A zero-param function whose RETURN is a wider record array than the
// slot's (`getRoutes: () => cachedRoutes` against `() => Narrow[]`):
// the interned width adapter wraps it — each call maps the result
// through the per-element record width copy.
if (
expected.kind === "func" &&
expr.type.kind === "func" &&
!typeEquals(expr.type, expected) &&
expr.type.params.length === 0 &&
expected.params.length === 0
) {
const adapter = this.funcReturnWidthAdapter(expr.type, expected, expr.loc);
if (adapter) {
return { kind: "call", callee: adapter, args: [expr], type: expected, loc: expr.loc };
}
}
// JS func-into-func mismatches ride the checked-dynamic function
// boundary: box the value (dynFrom), adapt to the slot (dynCheck) —
// the thunk delivers JS arity exactly (extras ignored, missing args
// the undefined dyn value), so `return _return` fits _mustCallInner's
// inferred (unknown) => unknown slot even though the wrapper declares
// (). JS files only: TypeScript signatures keep the exact-shape
// fences (a mismatch there is a compile-time story, not a boundary).
if (expected.kind === "func" && expr.type.kind === "func" && !typeEquals(expr.type, expected)) {
const sf = this.program.getSourceFile(expr.loc.file);
if (
sf !== undefined && isJsSourceFile(sf) &&
canConvertToDyn(expr.type, (id) => this.shapes.get(id), (id) => this.unions.get(id)) &&
canAdaptDynFuncTo(expected, (id) => this.shapes.get(id), (id) => this.unions.get(id))
) {
return {
kind: "dynCheck",
value: { kind: "dynFrom", value: expr, type: DYN, loc: expr.loc },
type: expected,
loc: expr.loc,
};
}
}
// A spawnSync-runner value (`defaultRunner` — its inferred return is
// the opaque spawnRes) flowing into a slot whose signature returns
// the structural result record tsc accepted: the interned adapter
// forwards the call and converts the result field-wise.
if (expected.kind === "func" && expr.type.kind === "func" && !typeEquals(expr.type, expected)) {
const adapter = this.spawnResFnAdapter(expr.type, expected, expr.loc);
if (adapter) {
return { kind: "call", callee: adapter, args: [expr], type: expected, loc: expr.loc };
}
}
// The GENERAL function-value adapter (funcCoerceAdapter): a function
// whose signature differs from the slot's only by coercible pieces —
// fewer parameters (JS ignores extras: `load(function () {})` into an
// `(x?: string) => void` slot), parameters/results that wrap into
// union arms, re-tag union-to-union, take a checked narrow, or cross
// the dyn boundary — wraps in a fresh closure applying exactly those
// conversions per call. Runs after the specialized adapters above so
// their pointed shapes keep winning.
if (expected.kind === "func" && expr.type.kind === "func" && !typeEquals(expr.type, expected)) {
const adapter = this.funcCoerceAdapter(expr.type, expected, expr.loc);
if (adapter) {
return { kind: "call", callee: adapter, args: [expr], type: expected, loc: expr.loc };
}
}
// Derived-into-base widening: a legal implicit upcast (prefix layout —
// a pointer reinterpret). Exactness stays required in every other
// direction; there is never an implicit DOWNcast.
if (
expected.kind === "object" &&
expr.type.kind === "object" &&
expr.type.className !== expected.className &&
this.isSubclassOf(expr.type.className, expected.className)
) {
return { kind: "upcast", value: expr, type: expected, loc: expr.loc };
}
// CLASS-VALUE widening (classval:D into a classval:C slot): the same
// pointer with only the static type changing — legal exactly when D
// strictly descends from C AND the two completed constructor ABIs
// agree, the invariant `newValue` completion against C's one
// signature rests on. Mismatches fall through to requireExactShape's
// pointed class-value fences.
if (
expected.kind === "classval" &&
expr.type.kind === "classval" &&
expr.type.className !== expected.className &&
this.isSubclassOf(expr.type.className, expected.className)
) {
const sub = this.classes.get(expr.type.className);
const sup = this.classes.get(expected.className);
// A generic FAMILY as the destination (`new () => Box<any>` slots):
// no `%<family>.constructor` exists for the validator's ABI check
// and no completion target is meaningful — the exact-shape fence
// downstream names the class-value flow instead.
if (sub && sup && !sup.generic && this.ctorAbiEquals(sub, sup)) {
return { kind: "upcast", value: expr, type: expected, loc: expr.loc };
}
}
if (expected.kind !== "union" || typeEquals(expr.type, expected)) {
// A UNION value flowing into one of its own ARMS: the checker
// proved the narrowing (control flow through a destructured
// binding, `d ?? (d = ...)`, a predicate call — tsc typed the SITE
// as the arm; the IR value still carries the declaration's union)
// — the CHECKED extraction, exactly `x!`'s machinery: the proven
// arm's payload comes out, every other arm throws the catchable
// TypeError (divergence 38's lying-assertion stance — sound
// narrowing never reaches them).
if (
expr.type.kind === "union" &&
!isUnitType(expected) &&
expected.kind !== "void" &&
this.armTag(expr.type.unionId, expected) >= 0
) {
const helper = this.narrowedArmHelper(expr.type.unionId, expected, expr.loc);
if (helper) {
return { kind: "call", callee: helper, args: [expr], type: expected, loc: expr.loc };
}
}
if (!typeEquals(expr.type, expected)) {
const w = this.widthCoerce(expr, expected);
if (w) return w;
// A UNIT (null/undefined) flowing into a plain non-nullable slot
// the checker approved (`null!` casts, non-strict assignments):
// the stranded-source stance (divergence 38) without a union in
// sight — the flow compiles to the catchable TypeError, where
// Node lets the impossible value ride until (unless) it is used.
const trap = this.strandedUnitTrap(expr, expected, expr.loc);
if (trap) return trap;
}
return expr;
}
if (expr.type.kind === "union") {
// A DIFFERENT union flowing into this slot: re-tag at runtime when
// every arm maps (unionRetagHelper); anything unmappable falls
// through to requireExactShape's SC2003.
const helper = this.unionRetagHelper(expr.type.unionId, expected.unionId, expr.loc);
if (helper) {
return { kind: "call", callee: helper, args: [expr], type: expected, loc: expr.loc };
}
return expr;
}
if (expr.type.kind === "void") {
// A void CALL RESULT flowing into a union with an undefined arm
// (`var r = foo({})` where foo returns void — tsc's void slots map
// to undefined-armed unions): JS's void value IS undefined, so the
// wrap takes the undefined arm. The backends evaluate the void
// operand for its effects and produce the interned unit instance
// (the unionWrap void-payload rule).
const undefTag = this.armTag(expected.unionId, UNDEFINED_T);
if (undefTag >= 0) {
return { kind: "unionWrap", unionId: expected.unionId, tag: undefTag, value: expr, type: expected, loc: expr.loc };
}
return expr;
}
const tag = this.armTag(expected.unionId, expr.type);
if (tag < 0) {
// A derived class flowing into a union with a base-class arm widens
// first (nearest ancestor arm wins), then wraps like any arm value.
if (expr.type.kind === "object") {
for (let c = this.classes.get(expr.type.className)?.base ?? null; c; c = c.base) {
const baseTag = this.armTag(expected.unionId, { kind: "object", className: c.def.name });
if (baseTag >= 0) {
const widened = this.upcastTo(expr, c.def.name);
return { kind: "unionWrap", unionId: expected.unionId, tag: baseTag, value: widened, type: expected, loc: expr.loc };
}
}
}
// A derived CLASS VALUE against a union with a base classval arm
// (`typeof Base | undefined` slots receiving D): the same nearest-
// ancestor widening, gated by the constructor-ABI rule; a mismatch
// falls through to the union fence.
if (expr.type.kind === "classval") {
const sub = this.classes.get(expr.type.className);
for (let c = sub?.base ?? null; c; c = c.base) {
const baseTag = this.armTag(expected.unionId, { kind: "classval", className: c.def.name });
if (baseTag >= 0) {
if (!sub || !this.ctorAbiEquals(sub, c)) break;
const widened: IrExpr = { kind: "upcast", value: expr, type: { kind: "classval", className: c.def.name }, loc: expr.loc };
return { kind: "unionWrap", unionId: expected.unionId, tag: baseTag, value: widened, type: expected, loc: expr.loc };
}
}
}
// A width-coercible value against a union: coerce into the SINGLE
// width-liftable arm, then wrap like any arm value (widthLiftPlan's
// liftWrap — several candidate arms are ambiguous and decline).
const lift = this.widthLiftPlan(expr.type, expected);
if (lift) return this.applyWidthLift(lift, expr, expected, expr.loc);
// Arms OUTSIDE widthLiftPlan's domain keep the historic per-arm
// widthCoerce probe (first match): index-signature record arms (the
// overflow CAPTURE helper owns their reshapes) and `any[]` arms
// (the island-boundary per-element lift). Bounded to those arm
// shapes so the plan's ambiguity rule for record/array lifts is
// never undone by a first-match fallback.
{
const def = this.unions.get(expected.unionId);
if (def) {
for (let i = 0; i < def.arms.length; i++) {
const arm = def.arms[i]!;
const boundaryArm =
(arm.kind === "record" && this.shapes.get(arm.shapeId)?.indexValue !== undefined) ||
(arm.kind === "array" && arm.elem.kind === "jsval");
if (!boundaryArm) continue;
const w = this.widthCoerce(expr, arm);
if (w) {
return { kind: "unionWrap", unionId: expected.unionId, tag: i, value: w, type: expected, loc: expr.loc };
}
}
}
}
// A checker-approved value the union CANNOT represent: a unit
// (`getV(): Foo | Bar { return null! }`, `null as any as T`), or a
// record/array with NO width-lift candidate at all (`{} as
// InstanceOne | InstanceTwo`). Every one is a LYING assertion —
// tsc accepted the flow only through a cast/assertion our arm list
// proves impossible — so it compiles to the stranded-arm TRAP
// (divergence 38's stance: the catchable TypeError at the flow,
// where Node lets the impossible value ride). AMBIGUOUS width
// candidates stay compile fences: honest code lands there.
{
const trap = this.strandedCoercionTrap(expr, expected, expr.loc);
if (trap) return trap;
}
return expr;
}
return { kind: "unionWrap", unionId: expected.unionId, tag, value: expr, type: expected, loc: expr.loc };
}
/** Copy-based structural WIDTH coercion — a `Full` record flowing into a
* narrower `{ id }` slot, or `Full[]` into `{ id }[]` (the Pick-typed
* display-table pattern): TS's width subtyping is free on erased types,
* but monomorphic structs must RESHAPE, so the value is rebuilt with the
* subset of fields copied — per element, via an interned helper, for
* arrays. A deliberate divergence from JS's aliasing (SEMANTICS.md 35,
* next to the marshal-copy stance): mutations through the narrowed value
* don't reach the original and vice versa. Exactly two flows coerce —
* record→record and record-array→record-array, each target field copied
* from a same-named source field whose type matches exactly or LIFTS
* into the target field's union (see recordWidthHelper); anything
* deeper keeps the exactness fences. Null when the pair isn't
* width-coercible. */
widthCoerce(expr: IrExpr, expected: IrType): IrExpr | null {
if (expected.kind === "record" && expr.type.kind === "record") {
// Index-signature pairs reshape through the overflow CAPTURE helper
// (the `Object.fromEntries(e) as ModelPricing` pattern — declared
// collisions validate at runtime); plain shapes keep the field-copy
// width helper. Each declines the other's shapes.
const helper =
this.recordWidthHelper(expr.type.shapeId, expected.shapeId, expr.loc) ??
lowerRecordOvfCaptureHelper(this, expr.type.shapeId, expected.shapeId, expr.loc);
if (!helper) return null;
return { kind: "call", callee: helper, args: [expr], type: expected, loc: expr.loc };
}
// A CLASS INSTANCE flowing into a record slot (`new Point(0, 0)` into
// `{ x: number; y: number }` — tsc's structural view of classes): the
// same field-projecting copy, each target field read off the instance.
if (expected.kind === "record" && expr.type.kind === "object") {
const helper = this.objRecordWidthHelper(expr.type.className, expected.shapeId, expr.loc);
if (!helper) return null;
return { kind: "call", callee: helper, args: [expr], type: expected, loc: expr.loc };
}
// A RECORD flowing into a class-instance slot (`{x: 0, y: 0}` into
// `A.Point` — the parameter-property data-class pattern): construction
// IS the projection when the constructor is nothing but parameter
// properties (recordToClassPlan's gates).
if (expected.kind === "object" && expr.type.kind === "record") {
const helper = this.recordClassWidthHelper(expr.type.shapeId, expected.className, expr.loc);
if (!helper) return null;
return { kind: "call", callee: helper, args: [expr], type: expected, loc: expr.loc };
}
// A CLASS VALUE flowing into a record slot (`var f: ShapeFactory =
// Shape` — an interface matched by the class's STATIC side): the
// record captures the statics — fields as copies, methods as the
// zero-capture closures `const f = C.m` builds. Direct classRef
// sources only: the projection reads no runtime value, so an effectful
// source expression would lose its evaluation.
if (expected.kind === "record" && expr.type.kind === "classval" && expr.kind === "classRef") {
return this.classStaticsProjection(expr.type.className, expected.shapeId, expr.loc);
}
if (expected.kind === "array" && expr.type.kind === "array" && expected.elem.kind !== "jsval") {
const helper = this.arrayWidthHelper(expr.type, expected, expr.loc);
if (helper) return { kind: "call", callee: helper, args: [expr], type: expected, loc: expr.loc };
// The EMPTY-array lift (widthLiftPlan's emptyArr rule), top-level:
// `cmd.aliases` typed `(null | undefined)[]` (an `aliases: []`
// table) flowing into a `string[]` slot.
const lift = this.widthLiftPlan(expr.type, expected);
if (lift?.how !== "emptyArr") return null;
return this.applyWidthLift(lift, expr, expected, expr.loc);
}
// A TUPLE flowing into an array slot (`const NAMES = [...] as const`
// assigned to a `readonly T[]` — the const-table pattern): TS erases
// the arity for free; the monomorphic tuple REBUILDS as a fresh array,
// each position's value lifted into the element type (the same copy
// stance as every width coercion — later mutations don't alias).
if (expected.kind === "array" && expr.type.kind === "record" && expected.elem.kind !== "jsval") {
const helper = this.tupleArrayWidthHelper(expr.type.shapeId, expected, expr.loc);
if (!helper) return null;
return { kind: "call", callee: helper, args: [expr], type: expected, loc: expr.loc };
}
// An `any[]` slot: any liftable element becomes one island handle per
// element (the messages-array pattern — records holding `any` content).
if (
expected.kind === "array" &&
expected.elem.kind === "jsval" &&
expr.type.kind === "array" &&
expr.type.elem.kind !== "jsval"
) {
const helper = this.arrayToJsvalArrayHelper(expr.type.elem, expr.loc);
if (!helper) return null;
return { kind: "call", callee: helper, args: [expr], type: expected, loc: expr.loc };
}
return null;
}
/** One step of the recursive width-lift relation: how a `src`-typed
* value enters a `dst`-typed slot under the copy-reshape family. The
* pure planning side — nothing interns here, so whole plans validate
* before any helper exists. The cases, in order:
* copy — exact same type (typeEquals), the field/element moves as is
* retag — union into union, every arm mapped (unionRetagMappable —
* identity arms, trap-less; record/array arms may width-lift
* into exactly one destination arm)
* wrap — a non-unit arm value into a union that contains it
* liftWrap — a record/array value into a union with NO identical arm
* but exactly ONE arm it width-lifts into (the findRoute
* rule applied at every level; several candidates are
* ambiguous and decline)
* width — record into a strict-subset record (recordWidthPlan,
* recursively — NESTED width)
* arr — array into array whose element pair lifts (per-element
* copy loop, arrayWidthHelper)
* dynIn — a typed value into an 'unknown' (dyn) slot — the same
* static→dyn deep copy coerceToExpected applies top-level
* upcast — a derived class instance into a base-typed slot (the
* prefix-layout pointer reinterpret, no copy)
* funcAdapt — a function into a slot whose signature differs only by
* CLEAN mechanical conversions (cleanFuncAdaptable); the
* stranded (trap-only) dispositions stay out of the plan
* Null when the pair isn't in the relation — callers keep their fences. */
widthLiftPlan(src: IrType, dst: IrType): WidthLift | null {
if (typeEquals(src, dst)) return { how: "copy" };
// An 'unknown' (dyn) DESTINATION slot: the static→dyn conversion —
// dynFrom, a DEEP COPY (`{ v: 5 }` into `{ v: unknown }`, `number[]`
// into `unknown[]` — tsc's top type over the width family's copies).
if (dst.kind === "dyn" && src.kind !== "dyn" && this.dynConvertible(src)) {
return { how: "dynIn" };
}
if (dst.kind === "union") {
if (src.kind === "union") {
return this.unionRetagMappable(src.unionId, dst.unionId) ? { how: "retag" } : null;
}
// A unit-typed source can't wrap here (unionWrap requires the
// LITERAL unit — and no lowered shape carries a bare unit field).
if (isUnitType(src)) return null;
const tag = this.armTag(dst.unionId, src);
if (tag >= 0) return { how: "wrap", tag };
const def = this.unions.get(dst.unionId);
if (!def) return null;
const candidates: { tag: number; arm: IrType }[] = [];
def.arms.forEach((arm, i) => {
if (isUnitType(arm)) return;
const sameFamily =
(src.kind === "record" && arm.kind === "record") ||
(src.kind === "array" && arm.kind === "array") ||
(src.kind === "object" && arm.kind === "record") ||
(src.kind === "record" && arm.kind === "object");
if (sameFamily && this.widthLiftPlan(src, arm) !== null) candidates.push({ tag: i, arm });
});
if (candidates.length !== 1) return null;
return { how: "liftWrap", tag: candidates[0]!.tag, arm: candidates[0]!.arm };
}
// A UNION source into a slot that is ONE of its arms (a width copy
// whose target field narrowed — the option-table choices shape:
// `value: boolean | string` copying into a `value: string` slot the
// checker approved): the CHECKED extraction — narrowedArmHelper,
// exactly `x!`'s machinery — the proven arm's payload comes out, any
// other arm throws the catchable TypeError (divergence 38's stance).
if (src.kind === "union" && !isUnitType(dst) && dst.kind !== "void" && this.armTag(src.unionId, dst) >= 0) {
return { how: "narrow" };
}
// A DERIVED instance into a BASE-typed slot (`{ p: Q }` copying into
// `{ p: P }`): the same implicit upcast coerceToExpected performs at
// top level — prefix layout, a pointer reinterpret, no copy.
if (
dst.kind === "object" &&
src.kind === "object" &&
this.isSubclassOf(src.className, dst.className)
) {
return { how: "upcast" };
}
// A FUNCTION into a slot whose signature differs only by CLEAN
// mechanical conversions (fewer params — JS ignores extras — and
// coercibleValue pieces): the general function-value adapter, plan-
// gated to the clean subset. The stranded (trap-only) dispositions
// funcCoerceAdapter also builds stay TOP-LEVEL only: a width plan
// never promises a bridge that can only throw.
if (dst.kind === "func" && src.kind === "func" && this.cleanFuncAdaptable(src, dst)) {
return { how: "funcAdapt" };
}
if (dst.kind === "record" && src.kind === "record") {
return this.recordWidthPlan(src.shapeId, dst.shapeId) !== null ? { how: "width" } : null;
}
if (dst.kind === "record" && src.kind === "object") {
return this.objToRecordPlan(src.className, dst.shapeId) !== null ? { how: "objWidth" } : null;
}
if (dst.kind === "object" && src.kind === "record") {
return this.recordToClassPlan(src.shapeId, dst.className) !== null ? { how: "clsWidth" } : null;
}
if (dst.kind === "array" && src.kind === "array") {
if (this.widthLiftPlan(src.elem, dst.elem) !== null) return { how: "arr" };
// The EMPTY-array lift: a unit-only element type (`readonly []`
// mapped as the unit-element array, `(null | undefined)[]`) has no
// per-element conversion into a data element — but the only value
// such a slot honestly holds in the width family is EMPTY, so the
// lift is a fresh empty array of the target type, guarded by a
// runtime non-empty trap (the checked-extraction stance).
if (this.unitOnlyElem(src.elem) && dst.elem.kind !== "jsval" && !this.unitOnlyElem(dst.elem)) {
return { how: "emptyArr" };
}
return null;
}
// A TUPLE flowing into an array FIELD/ELEMENT (`aliases: ["ls"]` into
// an `aliases: string[]` slot): per-position lifts, the top-level
// tuple-into-array coercion applied recursively.
if (dst.kind === "array" && src.kind === "record" && dst.elem.kind !== "jsval") {
const from = this.shapes.get(src.shapeId);
if (from?.tuple && from.fields.every((f) => this.widthLiftPlan(f.type, dst.elem) !== null)) {
return { how: "tupleArr" };
}
return null;
}
return null;
}
/** True for the unit-only element types (`(null | undefined)[]`, the
* `readonly []` mapping): a union whose every arm is a unit. */
unitOnlyElem(t: IrType): boolean {
if (t.kind !== "union") return false;
const def = this.unions.get(t.unionId);
return def !== undefined && def.arms.every((a) => isUnitType(a));
}
/** The build side of widthLiftPlan: the IrExpr converting `value` into
* `dst` under a plan the caller validated. Interns whatever helpers the
* lift needs (planned first, so the interns cannot fail — a failure here
* is a lowerer bug, not a user diagnostic). */
applyWidthLift(lift: WidthLift, value: IrExpr, dst: IrType, loc: SrcLoc): IrExpr {
switch (lift.how) {
case "copy":
return value;
case "wrap": {
if (dst.kind !== "union") throw new InternalCompilerError("lowerer bug: wrap lift against a non-union");
return { kind: "unionWrap", unionId: dst.unionId, tag: lift.tag, value, type: dst, loc };
}
case "retag": {
if (dst.kind !== "union" || value.type.kind !== "union") throw new InternalCompilerError("lowerer bug: retag lift shape");
const retag = this.unionRetagHelper(value.type.unionId, dst.unionId, loc);
if (!retag) throw new InternalCompilerError("lowerer bug: planned retag lift failed to intern");
return { kind: "call", callee: retag, args: [value], type: dst, loc };
}
case "liftWrap": {
if (dst.kind !== "union") throw new InternalCompilerError("lowerer bug: liftWrap lift against a non-union");
const inner = this.widthLiftPlan(value.type, lift.arm);
if (!inner) throw new InternalCompilerError("lowerer bug: planned liftWrap arm stopped lifting");
const lifted = this.applyWidthLift(inner, value, lift.arm, loc);
return { kind: "unionWrap", unionId: dst.unionId, tag: lift.tag, value: lifted, type: dst, loc };
}
case "width": {
if (dst.kind !== "record" || value.type.kind !== "record") throw new InternalCompilerError("lowerer bug: width lift shape");
const helper = this.recordWidthHelper(value.type.shapeId, dst.shapeId, loc);
if (!helper) throw new InternalCompilerError("lowerer bug: planned width lift failed to intern");
return { kind: "call", callee: helper, args: [value], type: dst, loc };
}
case "arr": {
if (dst.kind !== "array" || value.type.kind !== "array") throw new InternalCompilerError("lowerer bug: arr lift shape");
const helper = this.arrayWidthHelper(value.type, dst, loc);
if (!helper) throw new InternalCompilerError("lowerer bug: planned arr lift failed to intern");
return { kind: "call", callee: helper, args: [value], type: dst, loc };
}
case "tupleArr": {
if (dst.kind !== "array" || value.type.kind !== "record") throw new InternalCompilerError("lowerer bug: tupleArr lift shape");
const helper = this.tupleArrayWidthHelper(value.type.shapeId, dst, loc);
if (!helper) throw new InternalCompilerError("lowerer bug: planned tupleArr lift failed to intern");
return { kind: "call", callee: helper, args: [value], type: dst, loc };
}
case "emptyArr": {
if (dst.kind !== "array" || value.type.kind !== "array") throw new InternalCompilerError("lowerer bug: emptyArr lift shape");
const helper = this.emptyArrayLiftHelper(value.type, dst, loc);
return { kind: "call", callee: helper, args: [value], type: dst, loc };
}
case "objWidth": {
if (dst.kind !== "record" || value.type.kind !== "object") throw new InternalCompilerError("lowerer bug: objWidth lift shape");
const helper = this.objRecordWidthHelper(value.type.className, dst.shapeId, loc);
if (!helper) throw new InternalCompilerError("lowerer bug: planned objWidth lift failed to intern");
return { kind: "call", callee: helper, args: [value], type: dst, loc };
}
case "clsWidth": {
if (dst.kind !== "object" || value.type.kind !== "record") throw new InternalCompilerError("lowerer bug: clsWidth lift shape");
const helper = this.recordClassWidthHelper(value.type.shapeId, dst.className, loc);
if (!helper) throw new InternalCompilerError("lowerer bug: planned clsWidth lift failed to intern");
return { kind: "call", callee: helper, args: [value], type: dst, loc };
}
case "narrow": {
if (value.type.kind !== "union") throw new InternalCompilerError("lowerer bug: narrow lift on a non-union");
const helper = this.narrowedArmHelper(value.type.unionId, dst, loc);
if (!helper) throw new InternalCompilerError("lowerer bug: planned narrow lift failed to intern");
return { kind: "call", callee: helper, args: [value], type: dst, loc };
}
case "dynIn": {
if (dst.kind !== "dyn") throw new InternalCompilerError("lowerer bug: dynIn lift against a non-dyn slot");
return { kind: "dynFrom", value, type: DYN, loc };
}
case "upcast": {
if (dst.kind !== "object" || value.type.kind !== "object") throw new InternalCompilerError("lowerer bug: upcast lift shape");
return this.upcastTo(value, dst.className);
}
case "funcAdapt": {
if (dst.kind !== "func" || value.type.kind !== "func") throw new InternalCompilerError("lowerer bug: funcAdapt lift shape");
const adapter = this.funcCoerceAdapter(value.type, dst, loc);
if (!adapter) throw new InternalCompilerError("lowerer bug: planned funcAdapt lift failed to intern");
return { kind: "call", callee: adapter, args: [value], type: dst, loc };
}
default: {
const _exhaustive: never = lift;
void _exhaustive;
throw new InternalCompilerError("unreachable");
}
}
}
/** Interned `%rec.width.<n>(r)` — builds the target shape from a source
* record by copying fields: every target field must exist on the source
* with the EXACT same type, or with a type that LIFTS under
* widthLiftPlan — an arm value wraps (`text: string` into
* `text?: string`), a whole union re-tags (unionRetagMappable), a field
* whose own record/array type needs narrowing reshapes RECURSIVELY
* (nested width — the copy stance applies per level), and a MISSING
* optional-flavored field completes to its undefined arm (the
* literal-completion rule). TUPLES width-coerce too, arity-exact (TS
* permits no other tuple width): per-position lifts, never completion.
* Index-signature SOURCES narrow here like any wider record — declared
* fields copy, the overflow drops with the width (missing target
* fields decline: the overflow could hold them). Index-signature
* TARGETS keep the overflow CAPTURE helper (widthCoerce's other arm).
* Null when the shapes don't relate that way. */
/** The pure planning half of recordWidthHelper — every target field's
* lift, or null when the pair isn't width-coercible. Callers that must
* validate a WHOLE plan before interning anything (the retag helper's
* per-arm width lifts) probe with this. */
recordWidthPlan(fromId: string, toId: string): Map<string, { src: IrType; lift: WidthLift } | { absent: true; utag: number } | { absentDyn: true }> | null {
const from = this.shapes.get(fromId);
const to = this.shapes.get(toId);
// INDEX-SIGNATURE sources narrow like any wider record — the target
// fields copy off the declared struct slots and the overflow drops
// with the rest of the width (divergence 36's stance; the absent-
// completion rule below is the one extra fence). Index-signature
// TARGETS keep the overflow CAPTURE helper (widthCoerce's other arm):
// a fresh hybrid needs keyed writes, not a field-list literal.
if (!from || !to || to.indexValue) return null;
// Tuple↔record pairs never relate; tuple↔tuple only arity-exact.
if (!!from.tuple !== !!to.tuple) return null;
if (from.tuple && from.fields.length !== to.fields.length) return null;
const key = `${fromId}:${toId}`;
// Recursive shapes: an in-progress pair re-entered through its own
// fields answers "assume coercible" — see widthPlanning.
if (this.widthPlanning.has(key)) return new Map();
this.widthPlanning.add(key);
try {
type FieldLift = { src: IrType; lift: WidthLift } | { absent: true; utag: number } | { absentDyn: true };
const plan = new Map<string, FieldLift>();
for (const tf of to.fields) {
const ff = from.fields.find((f) => f.name === tf.name);
if (!ff) {
// A target field MISSING on the source: legal exactly when it is
// optional-flavored (an undefined-armed union) — the unset field
// IS the undefined arm, the same rule literal completion applies
// — or 'unknown' (a dyn slot holds the dyn undefined, exactly
// the absent-property read: the options-record call shape
// against `{ plugins: unknown, ... }`). Never for tuples: a
// completed position would change .length and JSON where Node
// keeps the source arity. Never for INDEX-SIGNATURE sources:
// the overflow may hold this very key at runtime (tsc lets the
// signature satisfy optional target members), so completing to
// undefined would drop a value Node keeps — the pair stays
// fenced.
if (from.tuple || from.indexValue) return null;
if (tf.type.kind === "dyn") {
plan.set(tf.name, { absentDyn: true });
continue;
}
if (tf.type.kind !== "union") return null;
const def = this.unions.get(tf.type.unionId);
const utag = def ? def.arms.findIndex((a) => a.kind === "undefinedT") : -1;
if (utag < 0) return null;
plan.set(tf.name, { absent: true, utag });
continue;
}
const lift = this.widthLiftPlan(ff.type, tf.type);
if (!lift) return null;
plan.set(tf.name, { src: ff.type, lift });
}
return plan;
} finally {
this.widthPlanning.delete(key);
}
}
/** Post-hoc classifier for SC2002's record→record residue: WHY the
* width family (recordWidthPlan and the overflow capture — widthCoerce's
* two record arms) declined this pair — the FIRST blocking rule, named.
* Pure description on the failure path (the site already carries the
* rejection): mirrors the planners' gates, never changes what coerces,
* and answers null when no pointed story applies (the generic message
* stands). */
describeRecordWidthBlocker(fromId: string, toId: string): string | null {
const from = this.shapes.get(fromId);
const to = this.shapes.get(toId);
if (!from || !to) return null;
if (to.indexValue) {
// The overflow CAPTURE's gates (lowerRecordOvfCaptureHelper).
if (from.tuple || to.tuple) return "a tuple cannot reshape into an index-signature record";
const tIv = to.indexValue;
const slotOk = (t: IrType): boolean =>
typeEquals(t, tIv) ||
(tIv.kind === "dyn" && (t.kind === "dyn" || this.dynConvertible(t))) ||
this.widthLiftPlan(t, tIv) !== null;
const consumed = new Set<string>();
for (const tf of to.fields) {
const sf = from.fields.find((f) => f.name === tf.name);
if (sf) {
if (this.widthLiftPlan(sf.type, tf.type) !== null) {
consumed.add(tf.name);
continue;
}
return `field '${tf.name}': '${this.fmt(sf.type)}' does not lift into '${this.fmt(tf.type)}'`;
}
if (tf.type.kind !== "union" || this.armTag(tf.type.unionId, UNDEFINED_T) < 0) {
return `the expected field '${tf.name}' is required and the source has no field to copy into it`;
}
if (tIv.kind === "dyn" ? !this.dynConvertible(tf.type) : !typeEquals(tf.type, tIv)) {
return `the expected field '${tf.name}' ('${this.fmt(tf.type)}') cannot take a runtime key collision from the '${this.fmt(tIv)}' signature slot`;
}
}
for (const ff of from.fields) {
if (consumed.has(ff.name)) continue;
if (!slotOk(ff.type)) {
return `the source field '${ff.name}' ('${this.fmt(ff.type)}') cannot enter the expected '[key: string]: ${this.fmt(tIv)}' slot`;
}
}
if (from.indexValue && !slotOk(from.indexValue)) {
return `the source's '[key: string]: ${this.fmt(from.indexValue)}' slot cannot enter the expected '[key: string]: ${this.fmt(tIv)}' slot`;
}
// The dispatch-writes gate: runtime-keyed writes can collide with a
// declared field whose type is not the slot's.
const dispatchWrites =
from.indexValue !== undefined ||
from.fields.some((ff) => !consumed.has(ff.name) && to.fields.some((f) => f.name === ff.name));
if (dispatchWrites) {
const bad = to.fields.find((f) =>
tIv.kind === "dyn" ? !this.dynConvertible(f.type) : !typeEquals(f.type, tIv),
);
if (bad) {
return `runtime-keyed writes can collide with the expected field '${bad.name}' ('${this.fmt(bad.type)}'), which cannot take a '${this.fmt(tIv)}' slot value`;
}
}
return null;
}
// The field-copy plan's gates (recordWidthPlan).
if (!!from.tuple !== !!to.tuple) return null;
if (from.tuple && from.fields.length !== to.fields.length) {
return `tuple arities differ (${from.fields.length} vs ${to.fields.length}; TS permits no tuple width)`;
}
for (const tf of to.fields) {
const ff = from.fields.find((f) => f.name === tf.name);
if (!ff) {
if (from.tuple) return null;
if (from.indexValue) {
return `'${tf.name}' is not a declared field of the source, and the source's index signature could hold it at runtime (a completed undefined would drop that value)`;
}
if (tf.type.kind === "dyn") continue;
if (tf.type.kind !== "union" || this.armTag(tf.type.unionId, UNDEFINED_T) < 0) {
return `the expected field '${tf.name}' is missing on the source and is not optional`;
}
continue;
}
if (this.widthLiftPlan(ff.type, tf.type) === null) {
return `field '${tf.name}': '${this.fmt(ff.type)}' does not lift into '${this.fmt(tf.type)}'`;
}
}
return null;
}
recordWidthHelper(fromId: string, toId: string, loc: SrcLoc): string | null {
const from = this.shapes.get(fromId);
const to = this.shapes.get(toId);
if (!from || !to) return null;
// Plan every target field BEFORE interning anything (interned helpers
// are part of the emitted program; a later field's failure must not
// orphan one).
const plan = this.recordWidthPlan(fromId, toId);
if (!plan) return null;
const key = `rec:${fromId}:${toId}`;
const existing = this.widthHelpers.get(key);
if (existing) return existing;
const name = `%rec.width.${this.widthHelpers.size}`;
// Interned BEFORE the body builds: a recursive nested-width field
// (self-referential shapes) resolves to this helper itself.
this.widthHelpers.set(key, name);
const fromT: IrType = { kind: "record", shapeId: fromId };
const toT: IrType = { kind: "record", shapeId: toId };
const r: IrExpr = { kind: "varRef", localId: "r.0", type: fromT, loc };
this.liftedFns.push({
name,
params: [{ localId: "r.0", name: "r", type: fromT }],
returnType: toT,
locals: [{ id: "r.0", name: "r", type: fromT, mutable: true }],
body: [
{
kind: "return",
value: {
kind: "recordLit",
fields: to.fields.map((f) => {
const lift = plan.get(f.name)!;
if ("absentDyn" in lift) {
// The unset 'unknown' field: the dyn undefined — exactly
// the absent-property read's answer.
return { name: f.name, value: dynUndefinedExpr(loc) };
}
if ("absent" in lift) {
if (f.type.kind !== "union") throw new InternalCompilerError("lowerer bug: absent lift against a non-union field");
// The unset optional field: build the undefined arm.
return {
name: f.name,
value: {
kind: "unionWrap",
unionId: f.type.unionId,
tag: lift.utag,
value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
type: f.type,
loc,
} satisfies IrExpr,
};
}
const get: IrExpr = { kind: "recordGet", obj: r, shapeId: fromId, field: f.name, type: lift.src, loc };
return { name: f.name, value: this.applyWidthLift(lift.lift, get, f.type, loc) };
}),
type: toT,
loc,
},
loc,
},
],
loc,
});
return name;
}
/** Interned `%arr.width.<n>(a)` — the per-element copy loop over
* widthLiftPlan's element lift: out = []; n = a.length; for (...)
* out.push(lift(a[i])); return out. Record elements reshape
* (recordWidthHelper), union elements wrap or re-tag (`number[]` into
* `(number | undefined)[]`), nested arrays recurse. Null when the
* element pair isn't width-liftable. */
/** Interned `%tup.arr.<n>(t)` — rebuilds a TUPLE as an ARRAY: positions
* read in order, each lifted into the element type under widthLiftPlan.
* Null unless the source shape really is a tuple whose every position
* lifts (records with named fields never relate to arrays). */
tupleArrayWidthHelper(fromId: string, toT: IrType & { kind: "array" }, loc: SrcLoc): string | null {
const from = this.shapes.get(fromId);
if (!from || !from.tuple) return null;
const fields = [...from.fields].sort((a, b) => Number(a.name) - Number(b.name));
const lifts: WidthLift[] = [];
for (const f of fields) {
const lift = this.widthLiftPlan(f.type, toT.elem);
if (!lift) return null;
lifts.push(lift);
}
const key = `tuparr:${fromId}:${typeKey(toT.elem)}`;
const existing = this.widthHelpers.get(key);
if (existing) return existing;
const name = `%tup.arr.${this.widthHelpers.size}`;
this.widthHelpers.set(key, name);
const fromT: IrType = { kind: "record", shapeId: fromId };
const t: IrExpr = { kind: "varRef", localId: "t.0", type: fromT, loc };
this.liftedFns.push({
name,
params: [{ localId: "t.0", name: "t", type: fromT }],
returnType: toT,
locals: [{ id: "t.0", name: "t", type: fromT, mutable: true }],
body: [
{
kind: "return",
value: {
kind: "arrayLit",
elems: fields.map((f, i) =>
this.applyWidthLift(
lifts[i]!,
{ kind: "recordGet", obj: t, shapeId: fromId, field: f.name, type: f.type, loc },
toT.elem,
loc,
),
),
type: toT,
loc,
},
loc,
},
],
loc,
});
return name;
}
/** Interned `%arr.empty.<n>(a)` — the EMPTY-array lift's build side: a
* unit-only-element array reshapes into any data-element array by
* answering a FRESH empty array, after a runtime non-empty trap (a
* genuinely inhabited `(null | undefined)[]` cannot reshape — the
* catchable-TypeError stance every checked extraction takes). */
emptyArrayLiftHelper(fromT: IrType & { kind: "array" }, toT: IrType & { kind: "array" }, loc: SrcLoc): string {
const key = `emptyarr:${typeKey(fromT.elem)}:${typeKey(toT.elem)}`;
const existing = this.widthHelpers.get(key);
if (existing) return existing;
const name = `%arr.empty.${this.widthHelpers.size}`;
this.widthHelpers.set(key, name);
const a: IrExpr = { kind: "varRef", localId: "a.0", type: fromT, loc };
this.liftedFns.push({
name,
params: [{ localId: "a.0", name: "a", type: fromT }],
returnType: toT,
locals: [{ id: "a.0", name: "a", type: fromT, mutable: true }],
body: [
{
kind: "if",
cond: {
kind: "bin",
op: "!==",
left: { kind: "arrIntrinsic", method: "length", receiver: a, args: [], type: F64, loc },
right: { kind: "numLit", value: 0, type: F64, loc },
type: BOOL,
loc,
},
then: [
{
kind: "throw",
value: {
kind: "libCall",
fn: "error.new",
args: [{ kind: "strLit", value: `expected ${this.fmt(toT)} (a non-empty ${this.fmt(fromT)} has no elements the target can hold)`, type: STRING, loc }],
type: { kind: "object", className: "%TypeError" },
loc,
},
loc,
},
],
else_: [],
loc,
},
{ kind: "return", value: { kind: "arrayLit", elems: [], type: toT, loc }, loc },
],
loc,
});
return name;
}
arrayWidthHelper(fromT: IrType & { kind: "array" }, toT: IrType & { kind: "array" }, loc: SrcLoc,): string | null {
const fromElem = fromT.elem;
const toElem = toT.elem;
const elemLift = this.widthLiftPlan(fromElem, toElem);
if (!elemLift || elemLift.how === "copy") return null;
const key = `arr:${typeKey(fromElem)}:${typeKey(toElem)}`;
const existing = this.widthHelpers.get(key);
if (existing) return existing;
const name = `%arr.width.${this.widthHelpers.size}`;
this.widthHelpers.set(key, name);
const arrT: IrType = { kind: "array", elem: fromElem };
const outT: IrType = { kind: "array", elem: toElem };
const f64: IrType = { kind: "f64" };
this.liftedFns.push({
name,
params: [{ localId: "a.0", name: "a", type: arrT }],
returnType: outT,
locals: [
{ id: "a.0", name: "a", type: arrT, mutable: true },
{ id: "out.0", name: "out", type: outT, mutable: false },
{ id: "n.0", name: "n", type: f64, mutable: false },
{ id: "i.0", name: "i", type: f64, mutable: true },
],
body: [
{ kind: "varDecl", localId: "out.0", init: { kind: "arrayLit", elems: [], type: outT, loc }, loc },
{
kind: "varDecl",
localId: "n.0",
init: { kind: "arrIntrinsic", method: "length", receiver: varRef("a.0", arrT, loc), args: [], type: f64, loc },
loc,
},
{
kind: "for",
init: { kind: "varDecl", localId: "i.0", init: numLit(0, loc), loc },
cond: { kind: "bin", op: "<", left: varRef("i.0", f64, loc), right: varRef("n.0", f64, loc), type: BOOL, loc },
update: {
kind: "assign",
localId: "i.0",
value: { kind: "bin", op: "+", left: varRef("i.0", f64, loc), right: numLit(1, loc), type: f64, loc },
loc,
},
body: [
{
kind: "exprStmt",
expr: {
kind: "arrIntrinsic",
method: "push",
receiver: varRef("out.0", outT, loc),
args: [
this.applyWidthLift(
elemLift,
{ kind: "arrayGet", arr: varRef("a.0", arrT, loc), index: varRef("i.0", f64, loc), type: fromElem, loc },
toElem,
loc,
),
],
type: f64,
loc,
},
loc,
},
],
loc,
},
{ kind: "return", value: varRef("out.0", outT, loc), loc },
],
loc,
});
return name;
}
/** The planning half of objRecordWidthHelper — how a CLASS INSTANCE
* projects into a record shape (tsc's structural view of classes makes
* `new Point(0,0)` flow into `{x: number; y: number}` slots). Every
* target field must be a plain instance FIELD on the class (inherited
* included) whose type lifts, or a missing optional-flavored field
* completing to its undefined arm — but never a field the class
* satisfies through a METHOD or accessor (bound method references have
* no lowering; the plan declines instead of projecting a lie). Builtin
* runtime layouts (the Error/EventEmitter/stream chains) decline: their
* fields aren't plain emitted storage. */
objToRecordPlan(className: string, toId: string): Map<string, { src: IrType; lift: WidthLift } | { absent: true; utag: number }> | null {
const info = this.classes.get(className);
const to = this.shapes.get(toId);
if (!info || !to || to.indexValue || to.tuple) return null;
// Reserved slots (%call hybrids, %get:/%set: accessor closures) are
// not projectable storage.
if (to.fields.some((f) => f.name.startsWith("%"))) return null;
for (let c: ClassInfo | null = info; c; c = c.base) {
if (c.builtinError || c.builtinEmitter || c.builtinStream !== undefined || c.def.runtime) return null;
}
const key = `obj:${className}:${toId}`;
if (this.widthPlanning.has(key)) return new Map();
this.widthPlanning.add(key);
try {
const plan = new Map<string, { src: IrType; lift: WidthLift } | { absent: true; utag: number }>();
for (const tf of to.fields) {
// A method/accessor satisfying the checker has no projectable
// value — decline the whole plan, field or not.
if (
findMethodOn(this, info, tf.name) ||
findMethodOn(this, info, `get:${tf.name}`) ||
findGenericMethodOn(this, info, tf.name)
) {
return null;
}
const ft = info.fields.get(tf.name);
if (ft === undefined) {
if (tf.type.kind !== "union") return null;
const def = this.unions.get(tf.type.unionId);
const utag = def ? def.arms.findIndex((a) => a.kind === "undefinedT") : -1;
if (utag < 0) return null;
plan.set(tf.name, { absent: true, utag });
continue;
}
const lift = this.widthLiftPlan(ft, tf.type);
if (!lift) return null;
plan.set(tf.name, { src: ft, lift });
}
return plan;
} finally {
this.widthPlanning.delete(key);
}
}
/** Interned `%obj.width.<n>(o)` — builds a record from a class
* instance's fields under objToRecordPlan: the width-copy stance
* (divergence 305 — a fresh record, mutations don't alias, extra class
* members drop). */
objRecordWidthHelper(className: string, toId: string, loc: SrcLoc): string | null {
const to = this.shapes.get(toId);
if (!to) return null;
const plan = this.objToRecordPlan(className, toId);
if (!plan) return null;
const key = `obj:${className}:${toId}`;
const existing = this.widthHelpers.get(key);
if (existing) return existing;
const name = `%obj.width.${this.widthHelpers.size}`;
this.widthHelpers.set(key, name);
const fromT: IrType = { kind: "object", className };
const toT: IrType = { kind: "record", shapeId: toId };
const o: IrExpr = { kind: "varRef", localId: "o.0", type: fromT, loc };
this.liftedFns.push({
name,
params: [{ localId: "o.0", name: "o", type: fromT }],
returnType: toT,
locals: [{ id: "o.0", name: "o", type: fromT, mutable: true }],
body: [
{
kind: "return",
value: {
kind: "recordLit",
fields: to.fields.map((f) => {
const lift = plan.get(f.name)!;
if ("absent" in lift) {
if (f.type.kind !== "union") throw new InternalCompilerError("lowerer bug: absent lift against a non-union field");
return {
name: f.name,
value: {
kind: "unionWrap",
unionId: f.type.unionId,
tag: lift.utag,
value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
type: f.type,
loc,
} satisfies IrExpr,
};
}
const get: IrExpr = { kind: "fieldGet", obj: o, className, field: f.name, type: lift.src, loc };
return { name: f.name, value: this.applyWidthLift(lift.lift, get, f.type, loc) };
}),
type: toT,
loc,
},
loc,
},
],
loc,
});
return name;
}
/** The planning half of recordClassWidthHelper — how a RECORD enters a
* class-instance slot. Construction IS the projection, so the class
* must be a pure parameter-property data class: its own trivial
* constructor (every parameter a parameter property, empty body), no
* other fields, no methods/accessors anywhere in the chain (a
* fabricated instance must carry no behavior the record lacks), no
* decoration, no base beyond a generic FAMILY ancestor (fieldless and
* methodless by construction). Each constructor parameter takes the
* same-named source field under widthLiftPlan, or — omittable params —
* the absent undefined arm. One entry per constructor parameter, in
* parameter order. */
recordToClassPlan(fromId: string, className: string): ({ field: string; src: IrType; lift: WidthLift } | { absent: true })[] | null {
const from = this.shapes.get(fromId);
const info = this.classes.get(className);
if (!from || !info || from.indexValue || from.tuple) return null;
if (from.fields.some((f) => f.name.startsWith("%"))) return null;
if (!info.decl || info.def.abstract || info.def.runtime || info.generic) return null;
if (info.builtinError || info.builtinEmitter || info.builtinStream !== undefined) return null;
if (info.classDecorators) return null;
if (info.base && !(info.base.generic && !info.base.base)) return null;
for (let c: ClassInfo | null = info; c; c = c.base) {
if (
c.methods.size > 0 ||
(c.genericMethods?.size ?? 0) > 0 ||
(c.symbolFields?.size ?? 0) > 0 ||
c.throwingSetters.length > 0 ||
(c.def.abstractMethods?.length ?? 0) > 0
) {
return null;
}
}
if (!info.ctor || info.ctor.body === undefined || info.ctor.body.statements.length > 0) return null;
const props = info.paramProps ?? [];
if (props.length !== info.ctorParams.length) return null;
// Every layout field must come from a parameter property (no declared
// fields with initializers the projection would silently prefer).
if (info.def.fields.length !== props.length) return null;
const key = `cls:${fromId}:${className}`;
if (this.widthPlanning.has(key)) return [];
this.widthPlanning.add(key);
try {
const plan: ({ field: string; src: IrType; lift: WidthLift } | { absent: true })[] = [];
for (let i = 0; i < props.length; i++) {
const shape = info.ctorParams[i];
if (!shape || (shape.mode !== "required" && shape.mode !== "omittable")) return null;
const name = props[i]!.name;
const ff = from.fields.find((f) => f.name === name);
if (!ff) {
if (shape.mode !== "omittable" || shape.type.kind !== "union") return null;
const def = this.unions.get(shape.type.unionId);
if (!def || !def.arms.some((a) => a.kind === "undefinedT")) return null;
plan.push({ absent: true });
continue;
}
const lift = this.widthLiftPlan(ff.type, shape.type);
if (!lift) return null;
plan.push({ field: name, src: ff.type, lift });
}
return plan;
} finally {
this.widthPlanning.delete(key);
}
}
/** Interned `%cls.width.<n>(r)` — `new C(r.p1, ..., r.pn)` under
* recordToClassPlan: the record's fields become the trivial
* constructor's arguments (divergence 305's copy stance — a fresh
* instance, mutations don't alias, and `instanceof C` answers true
* where Node's plain object answers false). */
recordClassWidthHelper(fromId: string, className: string, loc: SrcLoc): string | null {
const info = this.classes.get(className);
if (!info) return null;
const plan = this.recordToClassPlan(fromId, className);
if (!plan) return null;
const key = `cls:${fromId}:${className}`;
const existing = this.widthHelpers.get(key);
if (existing) return existing;
const name = `%cls.width.${this.widthHelpers.size}`;
this.widthHelpers.set(key, name);
this.noteEdge(`%${className}.constructor`);
const fromT: IrType = { kind: "record", shapeId: fromId };
const toT: IrType = { kind: "object", className };
const r: IrExpr = { kind: "varRef", localId: "r.0", type: fromT, loc };
const args = plan.map((entry, i): IrExpr => {
const shape = info.ctorParams[i]!;
if ("absent" in entry) {
const u = this.wrappedUndefined(shape.type, loc);
if (!u) throw new InternalCompilerError("lowerer bug: planned absent ctor arg has no undefined arm");
return u;
}
const get: IrExpr = { kind: "recordGet", obj: r, shapeId: fromId, field: entry.field, type: entry.src, loc };
return this.applyWidthLift(entry.lift, get, shape.type, loc);
});
this.liftedFns.push({
name,
params: [{ localId: "r.0", name: "r", type: fromT }],
returnType: toT,
locals: [{ id: "r.0", name: "r", type: fromT, mutable: true }],
body: [
{ kind: "return", value: { kind: "new", className, args, type: toT, loc }, loc },
],
loc,
});
return name;
}
/** A CLASS VALUE's statics projected into a record shape (`var f:
* ShapeFactory = Shape`): the record literal capturing static FIELDS as
* copies of their globals and static METHODS as the zero-capture
* closures `const f = C.m` builds (params all required — value-form
* completion rules stay out of coercions). Inherited statics resolve
* like JS's class-object prototype walk. Divergence 305's copy stance:
* later writes to a writable static field don't flow into the record
* (Node aliases the one class object). Null when any target field has
* no projectable static. */
classStaticsProjection(className: string, toId: string, loc: SrcLoc): IrExpr | null {
const info = this.classes.get(className);
const to = this.shapes.get(toId);
if (!info || !to || to.indexValue || to.tuple) return null;
if (to.fields.some((f) => f.name.startsWith("%"))) return null;
if (info.generic || !info.decl) return null;
const fields: { name: string; value: IrExpr }[] = [];
for (const tf of to.fields) {
if (findGenericStaticOn(this, info, tf.name)) return null;
const found = findStaticOn(this, info, tf.name);
if (!found) {
if (tf.type.kind !== "union") return null;
const u = this.wrappedUndefined(tf.type, loc);
if (!u) return null;
fields.push({ name: tf.name, value: u });
continue;
}
if (found.field !== undefined) {
const read: IrExpr = { kind: "varRef", localId: found.field.globalId, type: found.field.type, loc };
const lift = this.widthLiftPlan(found.field.type, tf.type);
if (!lift) return null;
fields.push({ name: tf.name, value: this.applyWidthLift(lift, read, tf.type, loc) });
continue;
}
if (found.method.params.some((p) => p.mode !== "required")) return null;
const funcType: IrType = {
kind: "func",
params: found.method.params.map((p) => p.type),
ret: found.method.ret,
};
const lift = this.widthLiftPlan(funcType, tf.type);
if (!lift) return null;
const fnName = `%${found.declarer.def.name}.static:${tf.name}`;
this.noteEdge(fnName);
const closure: IrExpr = { kind: "closure", fnName, captures: [], type: funcType, loc };
fields.push({ name: tf.name, value: this.applyWidthLift(lift, closure, tf.type, loc) });
}
return { kind: "recordLit", fields, type: { kind: "record", shapeId: toId }, loc };
}
/** Interned `%fn.width.<n>(f)` — the function-RETURN width adapter: a
* zero-param `() => Wide[]` value flowing into a `() => Narrow[]` slot
* (the createProxyServer getRoutes shape) wraps in a fresh closure that
* calls the original and maps the result through the per-element record
* width copy (%arr.width). The adapter is a factory lifted function
* whose param the returned closure captures; each invocation of the
* adapted value builds a FRESH array of narrowed records (the width
* machinery's copy stance — callers see the values, not the identity).
* Null when the return shapes aren't width-coercible; bounded to
* zero-param signatures (the one observed site — widening needs a
* param-forwarding story nothing drives yet). */
funcReturnWidthAdapter(fromT: IrType & { kind: "func" }, toT: IrType & { kind: "func" }, loc: SrcLoc,): string | null {
if (fromT.params.length !== 0 || toT.params.length !== 0) return null;
if (fromT.ret.kind !== "array" || toT.ret.kind !== "array") return null;
const mapper = this.arrayWidthHelper(fromT.ret, toT.ret, loc);
if (!mapper) return null;
const key = `fn:${typeKey(fromT.ret.elem)}:${typeKey(toT.ret.elem)}`;
const existing = this.widthHelpers.get(key);
if (existing) return existing;
const name = `%fn.width.${this.widthHelpers.size}`;
this.widthHelpers.set(key, name);
this.freshClosureAdapters.add(name); // wraps `f` in a new closure per call
const impl = `${name}.impl`;
// The returned closure's body: call the captured original, width-map.
this.liftedFns.push({
name: impl,
params: [],
returnType: toT.ret,
captures: [{ localId: "f.0", name: "f", type: fromT }],
locals: [{ id: "f.0", name: "f", type: fromT, mutable: false, boxed: true }],
body: [
{
kind: "return",
value: {
kind: "call",
callee: mapper,
args: [
{
kind: "callValue",
callee: { kind: "varRef", localId: "f.0", type: fromT, loc },
args: [],
type: fromT.ret,
loc,
},
],
type: toT.ret,
loc,
},
loc,
},
],
loc,
});
// The factory: box the incoming function value, mint the closure.
this.liftedFns.push({
name,
params: [{ localId: "f.0", name: "f", type: fromT }],
returnType: toT,
locals: [{ id: "f.0", name: "f", type: fromT, mutable: false, boxed: true }],
body: [
{
kind: "return",
value: { kind: "closure", fnName: impl, captures: ["f.0"], type: toT, loc },
loc,
},
],
loc,
});
return name;
}
/** Whether a `src`-typed VALUE converts into a `dst` slot through the
* coercions coerceToExpected applies mechanically — the PURE probe
* behind funcCoerceAdapter (nothing interns): exact types, arm wraps
* into unions, whole-union re-tags (unionRetagMappable), checked
* single-arm narrows, void into an undefined-armed union, and the dyn
* boundary in both directions (dynFrom / dynCheck's JSON-safe domain).
* Deliberately EXCLUDES the trap-only stranded conversions — an adapter
* that could only ever throw is a fence, not a bridge. */
coercibleValue(src: IrType, dst: IrType): boolean {
if (typeEquals(src, dst)) return true;
// The island boundary joins the mechanical set: values that MARSHAL
// in (units, the checked-dynamic deep copy, JSON-safe data, liftable
// composites, marshalable closures — coerceToExpected's jsval-IN
// block) and island handles whose exits VALIDATE (boundaryExitSafe) —
// the `defaultFallback(cfg) { return { login, id, scopes } }` shape,
// whose slot returns a package ('any') type.
if (dst.kind === "jsval") {
return (
src.kind !== "jsval" &&
(isUnitType(src) ||
src.kind === "dyn" ||
this.boundarySafe(src) ||
this.jsvalLiftable(src) ||
(src.kind === "func" &&
canMarshalTypedFuncIntoIsland(src, (id) => this.shapes.get(id), (id) => this.unions.get(id))))
);
}
if (src.kind === "jsval") return this.boundaryExitSafe(dst);
if (dst.kind === "dyn") return src.kind !== "dyn" && this.dynConvertible(src);
if (src.kind === "dyn") {
// The checked-dynamic function boundary's OUT direction joins the
// mechanical set: a dyn result landing in an adaptable func slot
// takes dynCheck's per-target shim (coerceToExpected's funcOk rule
// — the production/development function-choice ternary shape).
return (
this.jsonSafe(dst) ||
(dst.kind === "func" && canAdaptDynFuncTo(dst, (id) => this.shapes.get(id), (id) => this.unions.get(id)))
);
}
if (dst.kind === "union") {
if (src.kind === "union") return this.unionRetagMappable(src.unionId, dst.unionId);
if (src.kind === "void") return this.armTag(dst.unionId, UNDEFINED_T) >= 0;
return !isUnitType(src) && this.armTag(dst.unionId, src) >= 0;
}
if (src.kind === "union") {
return !isUnitType(dst) && dst.kind !== "void" && this.armTag(src.unionId, dst) >= 0;
}
return false;
}
/** True when a `src` function value enters a `dst` slot through
* funcCoerceAdapter with NO stranded (trap-only) piece: no rest packs,
* no surplus source params, every slot parameter converts into the
* wrapped function's own type, and the result converts back (a void
* slot drops it; a void result answers the exact JS undefined for
* dyn/jsval slots). The width family's func gate — widthLiftPlan
* bridges only signatures whose every call succeeds by construction. */
cleanFuncAdaptable(src: IrType & { kind: "func" }, dst: IrType & { kind: "func" }): boolean {
if (src.rest === true || dst.rest === true) return false;
if (src.params.length > dst.params.length) return false;
for (let i = 0; i < src.params.length; i++) {
if (!this.coercibleValue(dst.params[i]!, src.params[i]!)) return false;
}
if (dst.ret.kind === "void") return src.ret.kind !== "jsval";
if (this.coercibleValue(src.ret, dst.ret)) return true;
return src.ret.kind === "void" && (dst.ret.kind === "dyn" || dst.ret.kind === "jsval");
}
/** Interned `%fn.adapt.<n>(f)` — the GENERAL function-value adapter: a
* `fromT` function value flowing into a `toT` slot whose pieces differ
* only by coercibleValue conversions. The slot's callers pass toT's
* parameters: the wrapper takes them, converts the first
* fromT.params.length into the wrapped function's own types (surplus
* slot parameters are DROPPED — JS's extra-argument rule), calls it,
* and converts the result back (a void slot drops the result; a void
* result wraps as the slot union's undefined arm). Rest signatures on
* either side decline (the pack shapes don't line up mechanically).
* Null when any piece is outside coercibleValue — the exactness fences
* stay. */
funcCoerceAdapter(fromT: IrType & { kind: "func" }, toT: IrType & { kind: "func" }, loc: SrcLoc): string | null {
if (fromT.rest === true || toT.rest === true) return null;
if (fromT.params.length > toT.params.length) return null;
// Piece dispositions beyond coercibleValue, all CHECKER-APPROVED
// function compatibilities (bivariant method params under the suite's
// non-strict settings, `() => never` throwers displayed as void by
// the type mapping, void functions into unknown/any-returning slots):
// - strandParams: some parameter cannot convert — the assignment
// compiles, INVOKING the slot throws the stranded TypeError (a
// never-called mismatched callback is exact; divergence 38's stance
// extended to calls).
// - voidRet "dyn"/"jsval": calling yields JS's undefined — the exact
// undefined dyn/engine value after the call's effects.
// - voidRet "strand": a void result where the slot promises a typed
// value — the call runs (a `never` thrower never comes back, so the
// trap is unreachable there), then the stranded TypeError.
let strandParams = false;
for (let i = 0; i < fromT.params.length; i++) {
if (!this.coercibleValue(toT.params[i]!, fromT.params[i]!)) strandParams = true;
}
let voidRet: "dyn" | "jsval" | "strand" | null = null;
let strandRet = false;
if (toT.ret.kind !== "void" && !this.coercibleValue(fromT.ret, toT.ret)) {
if (fromT.ret.kind !== "void") {
// A RESULT that cannot convert — the strandParams stance, result
// side (the production/development function-choice ternary: the
// untaken arm's result shape never lands in the slot's): the
// assignment compiles, INVOKING the slot runs the function and
// throws the stranded TypeError where its result would convert.
strandRet = true;
} else {
voidRet = toT.ret.kind === "dyn" ? "dyn" : toT.ret.kind === "jsval" ? "jsval" : "strand";
}
}
if (toT.ret.kind === "void" && fromT.ret.kind === "jsval") return null;
const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`;
const existing = this.retagHelpers.get(key);
if (existing) return existing;
const name = `%fn.adapt.${this.retagHelpers.size}`;
this.retagHelpers.set(key, name);
this.freshClosureAdapters.add(name); // wraps `f` in a new closure per call
const impl = `${name}.impl`;
const params: IrParam[] = toT.params.map((t, i) => ({ localId: `a.${i}`, name: `a${i}`, type: t }));
const strandThrow = (why: string): IrStmt => ({
kind: "throw",
value: {
kind: "libCall",
fn: "error.new",
args: [{ kind: "strLit", value: why, type: STRING, loc }],
type: { kind: "object", className: "%TypeError" },
loc,
},
loc,
});
let body: IrStmt[];
if (strandParams) {
body = [
strandThrow(
`a '${this.fmt(fromT)}' function invoked through a '${this.fmt(toT)}' slot (the parameter types cannot convert — the checker's loose function compatibility admitted the assignment, but the call has no exact lowering)`,
),
];
} else {
const args = fromT.params.map((pt, i) => {
const aRef: IrExpr = { kind: "varRef", localId: `a.${i}`, type: toT.params[i]!, loc };
const converted = this.coerceToExpected(aRef, pt);
if (!typeEquals(converted.type, pt)) throw new InternalCompilerError("lowerer bug: probed fn-adapter param stopped coercing");
return converted;
});
const call: IrExpr = {
kind: "callValue",
callee: { kind: "varRef", localId: "f.0", type: fromT, loc },
args,
type: fromT.ret,
loc,
};
if (toT.ret.kind === "void") {
body = [
{ kind: "exprStmt", expr: call, loc },
{ kind: "return", value: null, loc },
];
} else if (voidRet === "dyn") {
body = [
{ kind: "exprStmt", expr: call, loc },
{ kind: "return", value: dynUndefinedExpr(loc), loc },
];
} else if (voidRet === "jsval") {
body = [
{ kind: "exprStmt", expr: call, loc },
{ kind: "return", value: { kind: "jsOp", op: "undefLit", args: [], type: JSVAL, loc }, loc },
];
} else if (voidRet === "strand") {
body = [
{ kind: "exprStmt", expr: call, loc },
strandThrow(
`a void result where the '${this.fmt(toT)}' slot promises '${this.fmt(toT.ret)}' (a thrower typed 'never' never reaches this; a genuinely void function has no result to hand over)`,
),
];
} else if (strandRet) {
body = [
{ kind: "exprStmt", expr: call, loc },
strandThrow(
`a '${this.fmt(fromT)}' function invoked through a '${this.fmt(toT)}' slot (the result cannot convert to '${this.fmt(toT.ret)}' — the checker's loose function compatibility admitted the assignment, but the call has no exact lowering)`,
),
];
} else {
const result = this.coerceToExpected(call, toT.ret);
if (!typeEquals(result.type, toT.ret)) throw new InternalCompilerError("lowerer bug: probed fn-adapter return stopped coercing");
body = [{ kind: "return", value: result, loc }];
}
}
this.liftedFns.push({
name: impl,
params,
returnType: toT.ret,
captures: [{ localId: "f.0", name: "f", type: fromT }],
locals: [
{ id: "f.0", name: "f", type: fromT, mutable: false, boxed: true },
...toT.params.map((t, i) => ({ id: `a.${i}`, name: `a${i}`, type: t, mutable: false })),
],
body,
loc,
});
this.liftedFns.push({
name,
params: [{ localId: "f.0", name: "f", type: fromT }],
returnType: toT,
locals: [{ id: "f.0", name: "f", type: fromT, mutable: false, boxed: true }],
body: [
{
kind: "return",
value: { kind: "closure", fnName: impl, captures: ["f.0"], type: toT, loc },
loc,
},
],
loc,
});
return name;
}
/** The spawnSync-runner VALUE adapter's plan — a function returning the
* opaque spawnRes flowing into a slot whose signature returns the
* STRUCTURAL result record tsc accepted (`defaultRunner` into a
* `CommandRunner` param: `{ status: number | null; stdout?: string;
* stderr?: string; error?: Error }`). Parameters must agree pairwise;
* each target field must be one of the spawnRes reads (status, stdout,
* stderr, error) at its exact lowered type — string fields optionally
* undefined-armed. Null when the pair isn't this shape. Pure: callers
* probe before interning. */
spawnResFnAdapterPlan(fromT: IrType & { kind: "func" }, toT: IrType & { kind: "func" },): { field: string; build: (r: IrExpr, loc: SrcLoc) => IrExpr }[] | null {
if (!Array.isArray(fromT.params) || !Array.isArray(toT.params)) return null; // defensive: degenerate func types
if (fromT.params.length !== toT.params.length) return null;
if (!fromT.params.every((p, i) => typeEquals(p, toT.params[i]!))) return null;
if (fromT.ret.kind !== "spawnRes" || toT.ret.kind !== "record") return null;
const shape = this.shapes.get(toT.ret.shapeId);
if (!shape || shape.tuple || shape.indexValue) return null;
const statusT: IrType = { kind: "union", unionId: this.unions.intern([F64, { kind: "nullT" }]) };
const errorT: IrType = { kind: "union", unionId: this.unions.intern([{ kind: "object", className: "%Error" }, UNDEFINED_T]) };
const strOptT: IrType = { kind: "union", unionId: this.unions.intern([STRING, UNDEFINED_T]) };
const plan: { field: string; build: (r: IrExpr, loc: SrcLoc) => IrExpr }[] = [];
for (const f of shape.fields) {
if (f.name === "status" && typeEquals(f.type, statusT)) {
plan.push({ field: f.name, build: (r, loc) => ({ kind: "libCall", fn: "spawnRes.status", args: [r], type: statusT, loc }) });
continue;
}
if ((f.name === "stdout" || f.name === "stderr") && (typeEquals(f.type, strOptT) || f.type.kind === "string")) {
const fn = f.name === "stdout" ? ("spawnRes.stdout" as const) : ("spawnRes.stderr" as const);
const strTag = this.armTag(strOptT.kind === "union" ? strOptT.unionId : "", STRING);
plan.push({
field: f.name,
build: (r, loc) => {
const read: IrExpr = { kind: "libCall", fn, args: [r], type: STRING, loc };
return f.type.kind === "string"
? read
: { kind: "unionWrap", unionId: (f.type as IrType & { kind: "union" }).unionId, tag: strTag, value: read, type: f.type, loc };
},
});
continue;
}
if (f.name === "error" && typeEquals(f.type, errorT)) {
plan.push({ field: f.name, build: (r, loc) => ({ kind: "libCall", fn: "spawnRes.error", args: [r], type: errorT, loc }) });
continue;
}
return null;
}
return plan;
}
/** Interned `%fnval.spawnres.<n>(f)` — the runner-value adapter: a
* fresh closure of the TARGET signature forwarding its arguments to the
* captured function and converting the opaque spawnRes result into the
* target's structural record (one eager read per declared field —
* spawnResFnAdapterPlan's set). Divergence caveat: stdout/stderr read
* as the captured text ("" when nothing was captured, e.g. stdio
* "inherit") where Node stores null. */
spawnResFnAdapter(fromT: IrType & { kind: "func" }, toT: IrType & { kind: "func" }, loc: SrcLoc,): string | null {
const plan = this.spawnResFnAdapterPlan(fromT, toT);
if (!plan) return null;
if (toT.ret.kind !== "record") return null;
const key = `fnspawn:${typeKey(fromT)}:${typeKey(toT)}`;
const existing = this.widthHelpers.get(key);
if (existing) return existing;
const name = `%fnval.spawnres.${this.widthHelpers.size}`;
this.widthHelpers.set(key, name);
this.freshClosureAdapters.add(name); // wraps `f` in a new closure per call
const impl = `${name}.impl`;
const params: IrParam[] = toT.params.map((p, i) => ({ localId: `p${i}.0`, name: `p${i}`, type: p }));
const rRef: IrExpr = { kind: "varRef", localId: "r.0", type: fromT.ret, loc };
this.liftedFns.push({
name: impl,
params,
returnType: toT.ret,
captures: [{ localId: "f.0", name: "f", type: fromT }],
locals: [
{ id: "f.0", name: "f", type: fromT, mutable: false, boxed: true },
...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
{ id: "r.0", name: "r", type: fromT.ret, mutable: false },
],
body: [
{
kind: "varDecl",
localId: "r.0",
init: {
kind: "callValue",
callee: { kind: "varRef", localId: "f.0", type: fromT, loc },
args: params.map((p): IrExpr => ({ kind: "varRef", localId: p.localId, type: p.type, loc })),
type: fromT.ret,
loc,
},
loc,
},
{
kind: "return",
value: {
kind: "recordLit",
fields: plan.map((entry) => ({ name: entry.field, value: entry.build(rRef, loc) })),
type: toT.ret,
loc,
},
loc,
},
],
loc,
});
this.liftedFns.push({
name,
params: [{ localId: "f.0", name: "f", type: fromT }],
returnType: toT,
locals: [{ id: "f.0", name: "f", type: fromT, mutable: false, boxed: true }],
body: [
{
kind: "return",
value: { kind: "closure", fnName: impl, captures: ["f.0"], type: toT, loc },
loc,
},
],
loc,
});
return name;
}
/** Interned `%union.retag.<n>(u)` — the runtime re-tag for a value of
* union `fromId` flowing into a slot of union `toId`: a switch on the
* source tag re-wraps the payload under its tag in the destination
* (unionNarrow + unionWrap — the payload pointer moves, no copy, so
* ref-arm identity is preserved across the re-tag). Arms map by
* canonical type (typeEquals): every non-unit source arm must exist in
* the destination, or the pair isn't mappable (null — the caller keeps
* the SC2003 fence). A stranded UNIT arm (undefined/null with no
* destination arm) is different: it means tsc's picture at the site was
* NARROWER than the IR type — control-flow narrowing to a sub-union, or
* a non-null assertion, both of which erase at lowering — so the arm is
* exactly the possibility the checker proved (or the source asserted)
* away. It compiles to a runtime trap case throwing a catchable
* TypeError-shaped string, the lying-cast stance (SEMANTICS.md): sound
* narrowing never reaches it, a lying `!` throws instead of smuggling
* an unrepresentable unit into the destination. */
/** True when unionRetagHelper can bridge the pair — every non-unit
* source arm exists (typeEquals) in the destination, or width-lifts
* into exactly one destination arm (widthLiftPlan — record and array
* arms compose the re-tag with the per-arm reshape). Pure: callers that
* must validate a WHOLE plan before interning anything (recordWidthHelper)
* probe with this so a failed later field never orphans a helper. */
unionRetagMappable(fromId: string, toId: string): boolean {
const from = this.unions.get(fromId);
if (!from || !this.unions.get(toId)) return false;
// The union-pair face of the widthPlanning cycle guard: recursive
// aliases can close their cycle through a union without repeating a
// record pair (`type Json = Json[] | undefined`) — an in-progress
// pair re-entered answers "assume mappable", same greatest-fixed-point
// reading as recordWidthPlan's.
const key = `u:${fromId}:${toId}`;
if (this.widthPlanning.has(key)) return true;
this.widthPlanning.add(key);
try {
const toT: IrType = { kind: "union", unionId: toId };
return from.arms.every((arm) => isUnitType(arm) || this.widthLiftPlan(arm, toT) !== null);
} finally {
this.widthPlanning.delete(key);
}
}
/** A checker-NARROWED union flowing into a different union: `typeof r
* === "string" || Buffer.isBuffer(r)` proves the record arm of r away,
* then `{ data: r }` needs `Buffer | string | Rec` in a `Buffer | string`
* slot. Control-flow narrowing to a sub-union erases at lowering, so the
* IR value still carries the wide union — but the SITE's checker type
* names exactly the arms still possible, and every one of those must
* exist in both unions. The stranded arms compile to trap cases exactly
* like stranded units (divergence 38's trust-the-checker stance): sound
* narrowing never reaches them, a lying cast throws a catchable
* TypeError instead of smuggling an unrepresentable arm. Null when the
* site type isn't a genuine sub-union of the source (the SC2003 fence
* stays). */
narrowedRetagHelper(node: ts.Node, fromId: string, toId: string, loc: SrcLoc): string | null {
const from = this.unions.get(fromId);
if (!from || !this.unions.get(toId)) return null;
const siteT = this.mapTypeOf(this.typeOf(node));
if (!siteT) return null;
const siteArms = siteT.kind === "union" ? this.unions.get(siteT.unionId)?.arms : [siteT];
if (!siteArms || siteArms.length === 0) return null;
const allowed = new Set<number>();
for (const a of siteArms) {
const fi = this.armTag(fromId, a);
if (fi < 0) return null; // not a narrowing of the source union
allowed.add(fi);
}
const trappable = new Set<number>();
from.arms.forEach((_, i) => {
if (!allowed.has(i)) trappable.add(i);
});
if (trappable.size === 0) return null; // nothing stranded: the plain re-tag already declined
return this.unionRetagHelper(fromId, toId, loc, trappable);
}
/** The stranded-UNIT trap for PLAIN (non-union) slots: a null/undefined
* value flowing into a non-nullable typed slot the checker approved —
* `null!` and `null as any as T` casts, and the non-strict world's
* legal `let s: string = null`. The compiled representation has no null
* to carry, so the FLOW throws the catchable stranded TypeError
* (divergence 38's stance: Node lets the impossible value ride until it
* is used; the trap surfaces at the assignment instead). Unit sources
* only — they are pure, so the nullary helper evaluates nothing. */
strandedUnitTrap(expr: IrExpr, expected: IrType, loc: SrcLoc): IrExpr | null {
if (!isUnitType(expr.type)) return null;
if (
expected.kind === "union" || expected.kind === "void" || expected.kind === "dyn" ||
expected.kind === "jsval" || isUnitType(expected)
) {
return null;
}
const what = expr.type.kind === "undefinedT" ? "undefined" : "null";
const key = `strandunit:${typeKey(expected)}:${expr.type.kind}`;
let name = this.retagHelpers.get(key);
if (!name) {
name = `%unit.strand.${this.retagHelpers.size}`;
this.retagHelpers.set(key, name);
this.liftedFns.push({
name,
params: [],
returnType: expected,
locals: [],
body: [
{
kind: "throw",
value: {
kind: "libCall",
fn: "error.new",
args: [
{
kind: "strLit",
value: `${what} is not representable in a '${this.fmt(expected)}' slot (a value narrowed or asserted past the type still held it)`,
type: STRING,
loc,
},
],
type: { kind: "object", className: "%TypeError" },
loc,
},
loc,
},
],
loc,
});
}
return { kind: "call", callee: name, args: [], type: expected, loc };
}
/** The STRANDED-SOURCE trap: a checker-approved value flowing into a
* union that cannot represent it (armTag < 0, no class widening, no
* width lift). Only shapes that PROVE a lying assertion trap: unit
* sources (null/undefined literals smuggled through `null!` / `as any`
* casts), and record/array sources with ZERO same-family width-lift
* candidates among the arms — an AMBIGUOUS lift (several candidates)
* stays a compile fence, because honest code lands there. The interned
* helper evaluates the operand (JS evaluates it too) and throws the
* stranded-arm TypeError verbatim. Null when the shape doesn't prove
* the lie. */
strandedCoercionTrap(expr: IrExpr, expected: IrType & { kind: "union" }, loc: SrcLoc): IrExpr | null {
const def = this.unions.get(expected.unionId);
if (!def) return null;
const src = expr.type;
let what: string;
if (isUnitType(src)) {
what = src.kind === "undefinedT" ? "undefined" : "null";
} else if (src.kind === "f64" || src.kind === "bool" || src.kind === "string") {
// A SCALAR the union has no arm for (`4 as any as X`, a generic
// dummy for an unmappable instantiation): no widening exists at
// all, so the mismatch proves the lie the same way a unit does.
what = `a '${this.fmt(src)}' value`;
} else if (src.kind === "record" || src.kind === "array") {
// Zero width-lift candidates proves no honest mapping was missed.
const candidates = def.arms.filter(
(arm) =>
((src.kind === "record" && arm.kind === "record") || (src.kind === "array" && arm.kind === "array")) &&
this.widthLiftPlan(src, arm) !== null,
);
if (candidates.length !== 0) return null;
what = `a '${this.fmt(src)}' value`;
} else {
return null;
}
// Unit sources have no runtime payload and are pure — the helper is
// nullary (unit-typed ABI params have no representation); ref sources
// pass through so the operand still evaluates, exactly JS.
const takesOperand = !isUnitType(src);
const key = `strand:${expected.unionId}:${typeKey(src)}`;
let name = this.retagHelpers.get(key);
if (!name) {
name = `%union.strand.${this.retagHelpers.size}`;
this.retagHelpers.set(key, name);
const toT: IrType = { kind: "union", unionId: expected.unionId };
this.liftedFns.push({
name,
params: takesOperand ? [{ localId: "v.0", name: "v", type: src }] : [],
returnType: toT,
locals: takesOperand ? [{ id: "v.0", name: "v", type: src, mutable: false }] : [],
body: [
{
kind: "throw",
value: {
kind: "libCall",
fn: "error.new",
args: [
{
kind: "strLit",
value: `${what} is not representable in the target union (a value narrowed or asserted past it still held it)`,
type: STRING,
loc,
},
],
type: { kind: "object", className: "%TypeError" },
loc,
},
loc,
},
],
loc,
});
}
return { kind: "call", callee: name, args: isUnitType(src) ? [] : [expr], type: expected, loc };
}
unionRetagHelper(fromId: string, toId: string, loc: SrcLoc, trappable?: ReadonlySet<number>): string | null {
const from = this.unions.get(fromId);
const to = this.unions.get(toId);
if (!from || !to) return null;
// Per-arm WIDTH LIFTS: a RECORD or ARRAY arm with no identical
// destination arm may width-lift into exactly ONE destination arm
// (widthLiftPlan's liftWrap — the findRoute pattern: `{hostname, port,
// tailscaleUrl?} | undefined` returning as `{hostname, port} |
// undefined`; nested width and per-element array reshapes compose).
// Planned PURELY first so a failing arm never orphans an interned
// width helper; ambiguity (several liftable destination arms) declines
// — no honest single mapping exists. A lifted arm is a COPY
// (divergence 35's stance), unlike the identity-preserving plain
// re-wrap.
const toT: IrType = { kind: "union", unionId: toId };
const lifts = new Map<number, WidthLift & { how: "liftWrap" }>();
from.arms.forEach((arm, i) => {
if (this.armTag(toId, arm) >= 0 || isUnitType(arm) || (trappable?.has(i) ?? false)) return;
const lp = this.widthLiftPlan(arm, toT);
if (lp && lp.how === "liftWrap") lifts.set(i, lp);
});
// `trappable` extends the unit-arm rule to arms the CHECKER proved
// away at the coercion site (narrowedRetagHelper): those may trap too.
const ok = from.arms.every(
(arm, i) => this.armTag(toId, arm) >= 0 || isUnitType(arm) || (trappable?.has(i) ?? false) || lifts.has(i),
);
if (!ok) return null;
const mapping = from.arms.map((arm, i) => {
const identity = this.armTag(toId, arm);
return identity >= 0 ? identity : (lifts.get(i)?.tag ?? -1);
});
const stranded = mapping.flatMap((t, i) => (t < 0 ? [i] : []));
// Lifts are a pure function of the (from, to) pair, so the historic
// key stays sound for them; stranded arms depend on the SITE.
const key = `${fromId}:${toId}:${stranded.join(".")}`;
const existing = this.retagHelpers.get(key);
if (existing) return existing;
const name = `%union.retag.${this.retagHelpers.size}`;
this.retagHelpers.set(key, name);
const fromT: IrType = { kind: "union", unionId: fromId };
const u: IrExpr = { kind: "varRef", localId: "u.0", type: fromT, loc };
const body: IrStmt[] = [];
from.arms.forEach((arm, i) => {
const tag = mapping[i]!;
const cond: IrExpr = { kind: "unionIsTag", unionId: fromId, tag: i, negated: false, value: u, type: BOOL, loc };
let then: IrStmt[];
if (tag < 0) {
const what = isUnitType(arm)
? (arm.kind === "undefinedT" ? "undefined" : "null")
: `a '${this.fmt(arm)}' value`;
then = [
{
kind: "throw",
value: {
kind: "libCall",
fn: "error.new",
args: [
{
kind: "strLit",
value: `${what} is not representable in the target union (a value narrowed or asserted past it still held it)`,
type: STRING,
loc,
},
],
type: { kind: "object", className: "%TypeError" },
loc,
},
loc,
},
];
} else {
const value: IrExpr = isUnitType(arm)
? { kind: "unitLit", unit: arm.kind === "undefinedT" ? "undefined" : "null", type: arm, loc }
: { kind: "unionNarrow", unionId: fromId, tag: i, value: u, type: arm, loc };
const lift = lifts.get(i);
// Width-lifted arm: the narrowed payload reshapes into the
// destination arm and wraps (applyWidthLift — planned above, so
// the interns cannot fail here); identity arms re-wrap the same
// payload pointer.
const wrapped: IrExpr = lift
? this.applyWidthLift(lift, value, toT, loc)
: { kind: "unionWrap", unionId: toId, tag, value, type: toT, loc };
then = [{ kind: "return", value: wrapped, loc }];
}
body.push({ kind: "if", cond, then, else_: null, loc });
});
// Unreachable when tags are exhaustive (they are, by construction);
// satisfies the all-paths-return rule and keeps a corrupted tag loud.
body.push({
kind: "throw",
value: { kind: "strLit", value: "scriptc: internal error: invalid union tag", type: STRING, loc },
loc,
});
this.liftedFns.push({
name,
params: [{ localId: "u.0", name: "u", type: fromT }],
returnType: toT,
locals: [{ id: "u.0", name: "u", type: fromT, mutable: true }],
body,
loc,
});
return name;
}
/** Interned `%union.narrow.<n>(u)` — the CHECKED single-arm extraction
* behind `x!` on union values: the asserted arm's payload comes out
* (+1 for ref arms, like any unionNarrow), and every OTHER arm throws
* the catchable TypeError — divergence 38's lying-assertion stance (an
* unchecked unionNarrow would misread the payload where JS lets the
* impossible value flow on). Null when the target isn't a non-unit arm
* of the union — those uses keep their erasure/fences. */
narrowedArmHelper(fromId: string, target: IrType, loc: SrcLoc): string | null {
const from = this.unions.get(fromId);
if (!from || isUnitType(target)) return null;
const tag = this.armTag(fromId, target);
if (tag < 0) return null;
const key = `${fromId}:${tag}`;
const existing = this.narrowHelpers.get(key);
if (existing) {
this.checkedNarrowHelpers.add(existing);
return existing;
}
const name = `%union.narrow.${this.narrowHelpers.size}`;
this.narrowHelpers.set(key, name);
this.checkedNarrowHelpers.add(name);
const fromT: IrType = { kind: "union", unionId: fromId };
const u: IrExpr = { kind: "varRef", localId: "u.0", type: fromT, loc };
const body: IrStmt[] = [];
from.arms.forEach((arm, i) => {
if (i === tag) return; // the fall-through extraction below
const what = isUnitType(arm)
? (arm.kind === "undefinedT" ? "undefined" : "null")
: `a '${this.fmt(arm)}' value`;
body.push({
kind: "if",
cond: { kind: "unionIsTag", unionId: fromId, tag: i, negated: false, value: u, type: BOOL, loc },
then: [
{
kind: "throw",
value: {
kind: "libCall",
fn: "error.new",
args: [
{
kind: "strLit",
value: `${what} is not representable in the target union (a value narrowed or asserted past it still held it)`,
type: STRING,
loc,
},
],
type: { kind: "object", className: "%TypeError" },
loc,
},
loc,
},
],
else_: null,
loc,
});
});
body.push({
kind: "return",
value: { kind: "unionNarrow", unionId: fromId, tag, value: u, type: target, loc },
loc,
});
this.liftedFns.push({
name,
params: [{ localId: "u.0", name: "u", type: fromT }],
returnType: target,
locals: [{ id: "u.0", name: "u", type: fromT, mutable: true }],
body,
loc,
});
return name;
}
/** The DEFERRED-INIT field read (`stream!: T` assigned past the
* constructor's top level — the slot is `T | undefined`): interned
* `%deferred.read.<n>(u)` extracting the declared type. SCALAR arms
* whose JS-undefined behavior a unit default reproduces read that
* default — bool false (conditions are exact: undefined and false are
* both falsy; only printing/strict-equality could tell) and f64 NaN
* (arithmetic and conditions exact) — while string and REF arms keep
* the checked-extraction TRAP: JS itself TypeErrors the first member
* use of such an undefined, so the catchable TypeError at the read is
* the same failure, named earlier (SEMANTICS.md). */
deferredReadHelper(fromId: string, target: IrType, loc: SrcLoc): string | null {
if (target.kind !== "bool" && target.kind !== "f64") {
return this.narrowedArmHelper(fromId, target, loc);
}
const from = this.unions.get(fromId);
const tag = this.armTag(fromId, target);
const utag = from ? from.arms.findIndex((a) => a.kind === "undefinedT") : -1;
if (!from || tag < 0 || utag < 0) return null;
const key = `deferred:${fromId}:${tag}`;
const existing = this.narrowHelpers.get(key);
if (existing) return existing;
const name = `%deferred.read.${this.narrowHelpers.size}`;
this.narrowHelpers.set(key, name);
const fromT: IrType = { kind: "union", unionId: fromId };
const u: IrExpr = { kind: "varRef", localId: "u.0", type: fromT, loc };
const dflt: IrExpr =
target.kind === "bool"
? { kind: "boolLit", value: false, type: BOOL, loc }
: { kind: "numLit", value: NaN, type: F64, loc };
this.liftedFns.push({
name,
params: [{ localId: "u.0", name: "u", type: fromT }],
returnType: target,
locals: [{ id: "u.0", name: "u", type: fromT, mutable: true }],
body: [
{
kind: "if",
cond: { kind: "unionIsTag", unionId: fromId, tag: utag, negated: false, value: u, type: BOOL, loc },
then: [{ kind: "return", value: dflt, loc }],
else_: null,
loc,
},
{
kind: "return",
value: { kind: "unionNarrow", unionId: fromId, tag, value: u, type: target, loc },
loc,
},
],
loc,
});
return name;
}
/** True when a static value can become ONE island value: jsval itself,
* anything boundary-safe (the deep JSON marshal), a record whose fields
* all can (built as an island OBJECT literal, field by field), or an
* array of such (built as an island ARRAY, element by element). The
* lift beyond boundarySafe exists for jsval-BEARING composites —
* `{ role: string; content: any[] }[]` flowing into an `any[]` slot —
* which have no JSON serialization (a handle isn't JSON) but an honest
* per-field construction. Recursion terminates: recursive shapes are
* rejected at mapping time. */
jsvalLiftable(t: IrType, visiting: Set<string> = new Set()): boolean {
if (t.kind === "jsval") return true;
if (this.boundarySafe(t)) return true;
// Typed arrays and URLs marshal IN without joining the round-trip
// (JSON) set: an engine typed-array copy / an engine URL from href.
if (t.kind === "bytes" || t.kind === "url") return true;
// Checked-dynamic values deep-copy in (scr_jsval_from_dyn — data
// kinds; a boxed function/handle/promise throws at runtime).
if (t.kind === "dyn") return true;
// Marshalable CLOSURES cross as host functions — a record carrying
// methods (the service-registry entry: `{ label, load: () =>
// Promise<any>, defaultFallback: (cfg) => any }`) lifts field by
// field like any other.
if (t.kind === "func") {
return canMarshalTypedFuncIntoIsland(t, (id) => this.shapes.get(id), (id) => this.unions.get(id));
}
if (t.kind === "record") {
const shape = this.shapes.get(t.shapeId);
if (!shape || shape.tuple) return false;
// Recursive shapes reaching here answer FALSE: this branch is the
// per-field island lift (jsval/bytes-bearing composites — the
// JSON-safe ones already answered true through boundarySafe, where
// a cyclic value throws the circular TypeError at the marshal), and
// the lift helpers walk values with no circular guard — fencing the
// TYPE is the honest answer.
if (visiting.has(t.shapeId)) return false;
visiting.add(t.shapeId);
// An INDEX-SIGNATURE record lifts when its value slot does (dyn
// included): declared fields write first, then the overflow keys.
if (shape.indexValue && !this.jsvalLiftable(shape.indexValue, visiting)) return false;
return shape.fields.every((f) => !f.name.startsWith("%") && this.jsvalLiftable(f.type, visiting));
}
if (t.kind === "array") return this.jsvalLiftable(t.elem, visiting);
// A union crossing IN lifts arm by arm (a runtime tag switch — see
// unionToJsvalHelper) when every arm does: unit arms become the
// engine's own undefined/null (which is why bare undefined-armed
// unions lift here despite being JSON-unsafe), the rest lift as
// themselves. Arms never nest unions, so this terminates (recursive
// knots pass through records, guarded above).
if (t.kind === "union") {
const def = this.unions.get(t.unionId);
return !!def && def.arms.every((a) => isUnitType(a) || this.jsvalLiftable(a, visiting));
}
return false;
}
/** A jsval-typed expression carrying `e`'s value into the island —
* jsvalLiftable's constructive side. Primitives and JSON-safe composites
* keep the jsMarshal deep copy; jsval-bearing records and arrays go
* through interned per-type builder helpers (%jsin.*), so the operand is
* always evaluated exactly once (as the helper's argument). */
jsvalLiftExpr(e: IrExpr, loc: SrcLoc): IrExpr {
if (e.type.kind === "jsval") return e;
if (this.boundarySafe(e.type)) {
return { kind: "jsMarshal", value: e, type: JSVAL, loc };
}
if (e.type.kind === "bytes" || e.type.kind === "url") {
return { kind: "jsMarshal", value: e, type: JSVAL, loc };
}
if (e.type.kind === "record") {
const helper = this.recordToJsvalHelper(e.type.shapeId, loc);
return { kind: "call", callee: helper, args: [e], type: JSVAL, loc };
}
if (e.type.kind === "array") {
const helper = this.arrayToJsvalHelper(e.type.elem, loc);
return { kind: "call", callee: helper, args: [e], type: JSVAL, loc };
}
if (e.type.kind === "union") {
const helper = this.unionToJsvalHelper(e.type.unionId, loc);
return { kind: "call", callee: helper, args: [e], type: JSVAL, loc };
}
// Checked-dynamic values and marshalable closures ride jsMarshal
// directly (the checked-dynamic tree deep copy / the host-function wrap).
if (e.type.kind === "dyn" || e.type.kind === "func") {
return { kind: "jsMarshal", value: e, type: JSVAL, loc };
}
throw new InternalCompilerError(`lowerer bug: jsvalLiftExpr of unliftable ${e.type.kind}`);
}
/** Interned `%jsin.union.<n>(u)` — the runtime tag switch marshaling a
* union value INTO the island: unit arms become the engine's own
* undefined/null (JS-exact — `{ instructions: undefined }` crossing in
* has the property present and undefined, exactly what the source
* spells), every other arm narrows and lifts as itself (strings by
* value, JSON-safe composites as deep copies, typed arrays as engine
* typed-array copies, URLs as engine URL instances). Caller must have
* checked jsvalLiftable. */
unionToJsvalHelper(unionId: string, loc: SrcLoc): string {
const key = `union:${unionId}`;
const existing = this.jsinHelpers.get(key);
if (existing) return existing;
const def = this.unions.get(unionId);
if (!def) throw new InternalCompilerError(`lowerer bug: jsval lift of unknown union ${unionId}`);
const name = `%jsin.union.${this.jsinHelpers.size}`;
this.jsinHelpers.set(key, name);
const fromT: IrType = { kind: "union", unionId };
const u: IrExpr = { kind: "varRef", localId: "u.0", type: fromT, loc };
const body: IrStmt[] = [];
def.arms.forEach((arm, i) => {
const cond: IrExpr = { kind: "unionIsTag", unionId, tag: i, negated: false, value: u, type: BOOL, loc };
const value: IrExpr = isUnitType(arm)
? { kind: "jsOp", op: arm.kind === "undefinedT" ? "undefLit" : "nullLit", args: [], type: JSVAL, loc }
: this.jsvalLiftExpr({ kind: "unionNarrow", unionId, tag: i, value: u, type: arm, loc }, loc);
body.push({ kind: "if", cond, then: [{ kind: "return", value, loc }], else_: null, loc });
});
// Unreachable when tags are exhaustive (they are, by construction);
// satisfies the all-paths-return rule and keeps a corrupted tag loud.
body.push({
kind: "throw",
value: { kind: "strLit", value: "scriptc: internal error: invalid union tag", type: STRING, loc },
loc,
});
this.liftedFns.push({
name,
params: [{ localId: "u.0", name: "u", type: fromT }],
returnType: JSVAL,
locals: [{ id: "u.0", name: "u", type: fromT, mutable: true }],
body,
loc,
});
return name;
}
/** Interned `%jsin.rec.<n>(r)` — builds an island OBJECT from a
* jsval-bearing record: marshaled key strings, each field lifted through
* jsvalLiftExpr (jsval fields pass as handles, JSON-safe fields deep-copy,
* nested composites recurse through their own helpers). Caller must have
* checked jsvalLiftable. */
recordToJsvalHelper(shapeId: string, loc: SrcLoc): string {
const key = `rec:${shapeId}`;
const existing = this.jsinHelpers.get(key);
if (existing) return existing;
const shape = this.shapes.get(shapeId);
if (!shape) throw new InternalCompilerError(`lowerer bug: jsval lift of unknown shape ${shapeId}`);
const name = `%jsin.rec.${this.jsinHelpers.size}`;
this.jsinHelpers.set(key, name);
const recT: IrType = { kind: "record", shapeId };
const r: IrExpr = { kind: "varRef", localId: "r.0", type: recT, loc };
const args: IrExpr[] = [];
for (const f of shape.fields) {
args.push({
kind: "jsMarshal",
value: { kind: "strLit", value: f.name, type: STRING, loc },
type: JSVAL,
loc,
});
args.push(
this.jsvalLiftExpr(
{ kind: "recordGet", obj: r, shapeId, field: f.name, type: f.type, loc },
loc,
),
);
}
const lit: IrExpr = { kind: "jsOp", op: "objLit", args, type: JSVAL, loc };
if (!shape.indexValue) {
this.liftedFns.push({
name,
params: [{ localId: "r.0", name: "r", type: recT }],
returnType: JSVAL,
locals: [{ id: "r.0", name: "r", type: recT, mutable: true }],
body: [{ kind: "return", value: lit, loc }],
loc,
});
return name;
}
// An INDEX-SIGNATURE shape: the declared pairs build the object, then
// the overflow map's live keys append in JS own-key order (setIdx —
// runtime keys have no property-name literal).
const iv = shape.indexValue;
const f64: IrType = { kind: "f64" };
const ksT = arrayOf(STRING);
const kRef = varRef("k.0", STRING, loc);
this.liftedFns.push({
name,
params: [{ localId: "r.0", name: "r", type: recT }],
returnType: JSVAL,
locals: [
{ id: "r.0", name: "r", type: recT, mutable: true },
{ id: "out.0", name: "out", type: JSVAL, mutable: false },
{ id: "ks.0", name: "ks", type: ksT, mutable: false },
{ id: "i.0", name: "i", type: f64, mutable: true },
{ id: "k.0", name: "k", type: STRING, mutable: false },
],
body: [
{ kind: "varDecl", localId: "out.0", init: lit, loc },
{ kind: "varDecl", localId: "ks.0", init: { kind: "recordOvfKeys", obj: r, shapeId, type: ksT, loc }, loc },
{
kind: "for",
init: { kind: "varDecl", localId: "i.0", init: numLit(0, loc), loc },
cond: {
kind: "bin",
op: "<",
left: varRef("i.0", f64, loc),
right: { kind: "arrIntrinsic", method: "length", receiver: varRef("ks.0", ksT, loc), args: [], type: f64, loc },
type: BOOL,
loc,
},
update: { kind: "assign", localId: "i.0", value: { kind: "bin", op: "+", left: varRef("i.0", f64, loc), right: numLit(1, loc), type: f64, loc }, loc },
body: [
{ kind: "varDecl", localId: "k.0", init: { kind: "arrayGet", arr: varRef("ks.0", ksT, loc), index: varRef("i.0", f64, loc), type: STRING, loc }, loc },
{
kind: "exprStmt",
expr: {
kind: "jsOp",
op: "setIdx",
args: [
varRef("out.0", JSVAL, loc),
{ kind: "jsMarshal", value: kRef, type: JSVAL, loc },
this.jsvalLiftExpr({ kind: "recordKeyGet", obj: r, shapeId, key: kRef, overflowOnly: true, type: iv, loc }, loc),
],
type: VOID,
loc,
},
loc,
},
],
loc,
},
{ kind: "return", value: varRef("out.0", JSVAL, loc), loc },
],
loc,
});
return name;
}
/** Interned `%jsin.arr.<n>(a)` — builds ONE island ARRAY from a native
* array whose elements lift: out = []; for (...) out[i] = lift(a[i]);
* return out. The index marshals by value like any number. Caller must
* have checked jsvalLiftable of the element. */
arrayToJsvalHelper(elem: IrType, loc: SrcLoc): string {
const key = `arr:${typeKey(elem)}`;
const existing = this.jsinHelpers.get(key);
if (existing) return existing;
const name = `%jsin.arr.${this.jsinHelpers.size}`;
this.jsinHelpers.set(key, name);
const arrT: IrType = { kind: "array", elem };
const f64: IrType = { kind: "f64" };
this.liftedFns.push({
name,
params: [{ localId: "a.0", name: "a", type: arrT }],
returnType: JSVAL,
locals: [
{ id: "a.0", name: "a", type: arrT, mutable: true },
{ id: "out.0", name: "out", type: JSVAL, mutable: false },
{ id: "n.0", name: "n", type: f64, mutable: false },
{ id: "i.0", name: "i", type: f64, mutable: true },
],
body: [
{ kind: "varDecl", localId: "out.0", init: { kind: "jsOp", op: "arrLit", args: [], type: JSVAL, loc }, loc },
{
kind: "varDecl",
localId: "n.0",
init: { kind: "arrIntrinsic", method: "length", receiver: varRef("a.0", arrT, loc), args: [], type: f64, loc },
loc,
},
{
kind: "for",
init: { kind: "varDecl", localId: "i.0", init: numLit(0, loc), loc },
cond: { kind: "bin", op: "<", left: varRef("i.0", f64, loc), right: varRef("n.0", f64, loc), type: BOOL, loc },
update: {
kind: "assign",
localId: "i.0",
value: { kind: "bin", op: "+", left: varRef("i.0", f64, loc), right: numLit(1, loc), type: f64, loc },
loc,
},
body: [
{
kind: "exprStmt",
expr: {
kind: "jsOp",
op: "setIdx",
args: [
varRef("out.0", JSVAL, loc),
{ kind: "jsMarshal", value: varRef("i.0", f64, loc), type: JSVAL, loc },
this.jsvalLiftExpr(
{ kind: "arrayGet", arr: varRef("a.0", arrT, loc), index: varRef("i.0", f64, loc), type: elem, loc },
loc,
),
],
type: { kind: "void" },
loc,
},
loc,
},
],
loc,
},
{ kind: "return", value: varRef("out.0", JSVAL, loc), loc },
],
loc,
});
return name;
}
/** Interned `%jsin.elems.<n>(a)` — a NATIVE array of island handles from
* a native array whose elements lift: the `any[]`-slot coercion (each
* element becomes one island value; the array stays static). Null when
* the element doesn't lift. */
arrayToJsvalArrayHelper(fromElem: IrType, loc: SrcLoc): string | null {
if (fromElem.kind === "jsval" || !this.jsvalLiftable(fromElem)) return null;
const key = `elems:${typeKey(fromElem)}`;
const existing = this.jsinHelpers.get(key);
if (existing) return existing;
const name = `%jsin.elems.${this.jsinHelpers.size}`;
this.jsinHelpers.set(key, name);
const arrT: IrType = { kind: "array", elem: fromElem };
const outT: IrType = { kind: "array", elem: JSVAL };
const f64: IrType = { kind: "f64" };
this.liftedFns.push({
name,
params: [{ localId: "a.0", name: "a", type: arrT }],
returnType: outT,
locals: [
{ id: "a.0", name: "a", type: arrT, mutable: true },
{ id: "out.0", name: "out", type: outT, mutable: false },
{ id: "n.0", name: "n", type: f64, mutable: false },
{ id: "i.0", name: "i", type: f64, mutable: true },
],
body: [
{ kind: "varDecl", localId: "out.0", init: { kind: "arrayLit", elems: [], type: outT, loc }, loc },
{
kind: "varDecl",
localId: "n.0",
init: { kind: "arrIntrinsic", method: "length", receiver: varRef("a.0", arrT, loc), args: [], type: f64, loc },
loc,
},
{
kind: "for",
init: { kind: "varDecl", localId: "i.0", init: numLit(0, loc), loc },
cond: { kind: "bin", op: "<", left: varRef("i.0", f64, loc), right: varRef("n.0", f64, loc), type: BOOL, loc },
update: {
kind: "assign",
localId: "i.0",
value: { kind: "bin", op: "+", left: varRef("i.0", f64, loc), right: numLit(1, loc), type: f64, loc },
loc,
},
body: [
{
kind: "exprStmt",
expr: {
kind: "arrIntrinsic",
method: "push",
receiver: varRef("out.0", outT, loc),
args: [
this.jsvalLiftExpr(
{ kind: "arrayGet", arr: varRef("a.0", arrT, loc), index: varRef("i.0", f64, loc), type: fromElem, loc },
loc,
),
],
type: f64,
loc,
},
loc,
},
],
loc,
},
{ kind: "return", value: varRef("out.0", outT, loc), loc },
],
loc,
});
return name;
}
jsvalIn(e: IrExpr, node: ts.Node): IrExpr {
return jsvalIn(this, e, node);
}
/** THE coercion path for values flowing into a typed slot: union arms
* wrap implicitly (coerceToExpected), then the exact-type fence runs
* (SC2002 for record shapes, SC2003 for unions). Every slot-directed
* lowering goes through here (via lowerExprExpecting) or calls this
* directly when the expression was already lowered. */
coerceInto(node: ts.Node, expr: IrExpr, expected: IrType): IrExpr {
if (expected.kind === "dyn") {
const contextual = this.checker.getContextualType(node);
const parts = contextual?.isUnionType() ? ts.constituentTypes(contextual) : contextual ? [contextual] : [];
const errorOptions = parts.some((part) => {
const sym = part.getAliasSymbol() ?? part.getSymbol();
return sym?.name === "ErrorOptions" && this.isStdlibSymbol(sym);
});
if (errorOptions) {
const actual = this.typeOf(node);
for (const part of actual.isUnionType() ? ts.constituentTypes(actual) : [actual]) {
const cause = this.checker.getPropertyOfType(part, "cause");
if (cause && cause.flags & (ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor)) {
this.unsupported("SC1090", node, "Error options with a cause accessor (use a data property)");
}
}
}
}
let e = this.coerceToExpected(expr, expected);
// An 'any' value PROVABLY null/undefined (the unit literal itself, or
// a read of a binding nothing ever assigns a non-unit value) flowing
// implicitly into a primitive slot: the validated exit refuses units
// unconditionally, so every run would throw the boundary TypeError
// where Node proceeds silently. A failure certain at compile time is
// a fence, not a runtime surprise. Explicit casts keep their runtime
// checked-cast semantics (the cast lowering builds its own jsExit
// before this runs, so `e !== expr` skips them), and union/composite
// targets keep the runtime exit (an undefined-armed union ACCEPTS the
// engine's undefined; composite validation is dynCheck's business).
if (
e !== expr && e.kind === "jsExit" &&
(e.type.kind === "f64" || e.type.kind === "string" || e.type.kind === "bool")
) {
const unit = this.provenUnitAnyOf(node, e.value);
if (unit !== null) {
this.unsupported(
"SC1090",
node,
`an 'any' value that is always ${unit} flowing into a '${this.fmt(expected)}' slot (nothing in the program gives this value another shape, and the island boundary's validated exit refuses ${unit} — every run would throw a TypeError where Node proceeds silently)`,
`give the binding a value other than ${unit} before this use, or keep the slot's type 'any'`,
);
}
}
// A closure that just BOXED into dyn takes its best-effort JS name
// from the source node (identifier reads, named function expressions,
// NamedEvaluation through a variable initializer) — inspect prints
// [Function: name] and call errors spell it, like Node.
if (e.kind === "dynFrom" && e.value.type.kind === "func" && e.fnName === undefined) {
const name = jsFuncNameOf(node);
if (name !== null) e = { ...e, fnName: name };
}
// A union the plain re-tag declined (stranded NON-unit arms): when the
// node's CHECKER type proves those arms away, they trap instead — the
// sub-union narrowing bridge (narrowedRetagHelper).
if (e.type.kind === "union" && expected.kind === "union" && !typeEquals(e.type, expected)) {
const helper = this.narrowedRetagHelper(node, e.type.unionId, expected.unionId, e.loc);
if (helper) {
e = { kind: "call", callee: helper, args: [e], type: expected, loc: e.loc };
}
}
// A JS FUNC value outside the island marshal set flowing into a
// jsval slot (`withPlugins(getSupportInfoWithoutPlugins, 0)` — a
// wrapper built at module init around a function the run may never
// call): the crossing defers to a call-time fence closure instead of
// stopping the statement — jsvalIn's deferral, the implicit-coercion
// spelling.
if (expected.kind === "jsval" && e.type.kind === "func" && !typeEquals(e.type, expected)) {
const diagsBefore = this.diags.length;
try {
this.requireExactShape(node, e.type, expected);
} catch (err) {
const fence = islandFuncValueFence(this, err, diagsBefore, node);
if (fence) return fence;
throw err;
}
return e;
}
this.requireExactShape(node, e.type, expected);
return e;
}
/** The unit an 'any' expression PROVABLY holds on every run, or null
* when no proof exists. Two spellings prove: the lowered value IS the
* engine unit literal (`null as any`, an any-contextual `undefined`),
* or the node is an identifier whose every declaration is a plain,
* non-ambient `var`/`let`/`const` declarator under a variable STATEMENT
* (catch bindings, for-of/for-in cursors, parameters, and imports all
* fail this shape test — each receives values from elsewhere), each
* initializer absent or unit-typed by the checker (a unit TYPE has
* exactly one value, so syntax doesn't matter), and nothing in the
* declaring file ever assigns it — bindingNeverReassigned, the same
* file-scan proof the generic-binding machinery leans on (ESM import
* bindings are read-only, so cross-file writes don't exist). A hoisted
* `var` read before its unit-initialized statement holds undefined —
* also a unit — so the mixed case reports both names. */
provenUnitAnyOf(node: ts.Node, value: IrExpr): string | null {
if (value.kind === "jsOp" && value.args.length === 0) {
if (value.op === "undefLit") return "undefined";
if (value.op === "nullLit") return "null";
}
let n: ts.Node = node;
while (ts.isParenthesizedExpression(n)) n = n.expression;
if (!ts.isIdentifier(n)) return null;
const sym = this.resolveValueSymbol(n);
if (!sym) return null;
const decls = this.checker.declarationsOf(sym);
if (decls.length === 0) return null;
const units = new Set<string>();
let allConst = true;
let firstDecl: ts.VariableDeclaration | null = null;
for (const d of decls) {
if (
!ts.isVariableDeclaration(d) ||
!ts.isIdentifier(d.name) ||
!ts.isVariableDeclarationList(d.parent) ||
!ts.isVariableStatement(d.parent.parent) ||
d.getSourceFile().isDeclarationFile ||
(ts.getCombinedModifierFlags(d) & ts.ModifierFlags.Ambient) !== 0
) {
return null;
}
firstDecl ??= d;
if ((ts.getCombinedNodeFlags(d) & ts.NodeFlags.Const) === 0) allConst = false;
if (d.initializer === undefined) {
units.add("undefined");
} else {
const t = this.typeOf(d.initializer);
if (!isUnitOnlyTsType(t)) return null;
for (const p of t.isUnionType() ? ts.constituentTypes(t) : [t]) {
units.add((p.flags & ts.TypeFlags.Null) !== 0 ? "null" : "undefined");
}
}
// A hoisted `var` with a unit initializer still reads `undefined`
// between module/function entry and its statement.
if ((ts.getCombinedNodeFlags(d) & (ts.NodeFlags.Const | ts.NodeFlags.Let)) === 0) {
units.add("undefined");
}
}
if (!allConst && !bindingNeverReassigned(this, sym, firstDecl!)) return null;
return [...units].sort().join(" or ");
}
/** A key conversion may rebox or upcast a reference, but must never copy
* its referent. Apply the ordinary checked narrowing to absent arms from
* runtime-optional reads; only present reference arms participate here. */
lowerCollectionKey(node: ts.Expression, expected: IrType): IrExpr {
if (expected.kind === "dyn") {
const value = this.lowerExprExpecting(node, expected);
// An adapting promise changes identity, while the Error boundary
// snapshots mutable fields. Neither is a live collection reference.
const copiesReference = (type: IrType): boolean =>
(type.kind === "promise" && type.inner.kind !== "dyn") ||
(type.kind === "object" && (RUNTIME_ERROR_CLASSES.has(type.className) || this.isSubclassOf(type.className, "%Error"))) ||
(type.kind === "union" && (this.unions.get(type.unionId)?.arms.some(copiesReference) ?? false));
if (value.kind === "dynFrom" && copiesReference(value.value.type)) {
this.noLowering("collection slot conversion requiring an Error snapshot or promise adapter", node,
"use a collection with the original reference type; checked collection slots require identity-preserving conversions");
}
// Collections expose the stored reference again. Preserve records,
// arrays and byte views rather than taking the ordinary dyn snapshot.
const hasLiveReference = (type: IrType): boolean =>
type.kind === "record" || type.kind === "array" || type.kind === "bytes" ||
(type.kind === "union" && (this.unions.get(type.unionId)?.arms.some(hasLiveReference) ?? false));
if (value.kind === "dynFrom" && hasLiveReference(value.value.type)) {
return { ...value, liveRef: true };
}
return value;
}
let literal = node;
while (ts.isParenthesizedExpression(literal)) literal = literal.expression;
// Fresh literals have no previous outer identity to preserve. Build them
// for their storage layout, just as ordinary contextual literals do.
if (ts.isObjectLiteralExpression(literal) || ts.isArrayLiteralExpression(literal) || expected.kind === "f64" || expected.kind === "string") {
return this.lowerExprExpecting(node, expected);
}
// Assertions can perform their own conversion before slot coercion.
// Inspect the assertion chain so a width copy cannot hide behind its
// already-converted result type. Union-arm checks only unwrap a box.
let asserted = literal;
while (ts.isAsExpression(asserted) || ts.isTypeAssertion(asserted) || ts.isNonNullExpression(asserted) || ts.isParenthesizedExpression(asserted)) {
if (ts.isAsExpression(asserted) || ts.isTypeAssertion(asserted)) {
const from = this.mapTypeOf(this.typeOf(asserted.expression));
const to = this.mapTypeOf(this.checker.getTypeFromTypeNode(asserted.type));
if (from?.kind === "dyn" || from?.kind === "jsval" || (from?.kind === "record" && to?.kind === "record" && !typeEquals(from, to))) {
this.noLowering("collection key assertion requiring a structural copy or dynamic conversion", asserted,
"use the original reference with the same type as the collection key");
}
}
asserted = asserted.expression;
}
const value = this.lowerExpr(node);
const source = (value.type.kind === "union" ? this.unions.get(value.type.unionId)?.arms ?? [] : [value.type]).filter((type) => !isUnitType(type));
const target = expected.kind === "union" ? this.unions.get(expected.unionId)?.arms ?? [] : [expected];
const preservesIdentity = source.length > 0 && source.every((from) => target.some((to) =>
typeEquals(from, to) || (from.kind === "object" && to.kind === "object" && this.isSubclassOf(from.className, to.className))));
if (!preservesIdentity || value.kind === "dynCheck" || value.kind === "jsExit") {
this.noLowering(`collection key conversion from '${this.fmt(value.type)}' to '${this.fmt(expected)}' requiring a structural copy or dynamic conversion`, node,
"use the same reference type for the key and collection; copying a key would change its identity");
}
return this.coerceInto(node, value, expected);
}
/** Lower for a known destination, then apply ordinary value coercion.
* Fresh arrays and records use the destination layout directly. */
lowerExprExpecting(node: ts.Expression, expected: IrType | undefined): IrExpr {
if (expected?.kind === "array") {
let x: ts.Expression = node;
while (ts.isParenthesizedExpression(x)) x = x.expression;
// Fresh arrays are built for the actual destination, including
// nested literals whose checker context names a wider recursive
// union. Existing arrays still go through ordinary value coercion.
if (ts.isArrayLiteralExpression(x)) {
return this.coerceInto(node, this.lowerArrayLiteral(x, expected), expected);
}
}
if (expected?.kind === "record") {
let x = node;
while (ts.isParenthesizedExpression(x)) x = x.expression;
if (ts.isObjectLiteralExpression(x)) {
return this.coerceInto(node, this.lowerObjectLiteral(x, expected), expected);
}
}
// An OBJECT LITERAL against a checked-dynamic slot in a JS file (the
// getSupportInfo options argument — a dyn-ABI param), or against the
// standard RequestInit/ErrorOptions types in TypeScript: the value's world IS the
// checked-dynamic tree — build the dyn literal directly.
if (expected?.kind === "dyn") {
let x: ts.Expression = node;
while (ts.isParenthesizedExpression(x)) x = x.expression;
const contextual = this.checker.getContextualType(x);
const widened = contextual ? this.checker.getBaseTypeOfLiteralType(contextual) : undefined;
const parts = widened?.isUnionType() ? ts.constituentTypes(widened) : widened ? [widened] : [];
const builtinOption = (name: string): boolean => parts.some((part) => {
const sym = part.getAliasSymbol() ?? part.getSymbol();
return sym?.name === name && this.isStdlibSymbol(sym);
});
if (ts.isObjectLiteralExpression(x)) {
if (isJsSourceFile(x.getSourceFile()) || builtinOption("RequestInit") || builtinOption("ErrorOptions")) {
return lowerDynObjectLiteral(this, x);
}
}
}
// A fresh array can use the unique array-family destination inside a
// union too. In nullable recursive fields the checker can report the
// whole union as context: an identity lift of that context does not
// mean the literal's independently inferred element layout fits.
// Construct the elements for the selected arm before wrapping it.
// Multiple eligible arms retain the ordinary ambiguity rules.
if (expected?.kind === "union") {
let x: ts.Expression = node;
while (ts.isParenthesizedExpression(x)) x = x.expression;
if (ts.isArrayLiteralExpression(x)) {
// Elements beyond bare null/undefined literals can never live in
// a unit-only-element array — a checker type that degraded to one
// (`[]`-flavored inference over a populated literal) carries no
// element information.
const nonUnitElems = x.elements.some(
(el) =>
!ts.isOmittedExpression(el) &&
el.kind !== ts.SyntaxKind.NullKeyword &&
!(ts.isIdentifier(el) && el.text === "undefined"),
);
const def = this.unions.get(expected.unionId);
const arms = (def?.arms ?? []).filter(
(a) =>
(a.kind === "array" && !(nonUnitElems && this.unitOnlyElem(a.elem))) ||
(a.kind === "record" &&
!!this.shapes.get(a.shapeId)?.tuple &&
this.shapes.get(a.shapeId)!.fields.length === x.elements.length &&
!x.elements.some(ts.isSpreadElement)),
);
if (arms.length === 1) {
const arm = arms[0]!;
const built = this.lowerArrayLiteral(x, arm as IrType & { kind: "array" } | (IrType & { kind: "record" }));
return this.coerceInto(node, built, expected);
}
}
}
const e = this.lowerExpr(node);
return expected ? this.coerceInto(node, e, expected) : e;
}
/** A value flowing into an index-signature VALUE slot (an overflow
* literal entry, a dynamic-keyed record write). dyn slots (`unknown`
* signatures — ModelPricing's) take a dyn conversion: dyn values pass
* through, JSON-safe static values convert with dynFrom (a deep copy —
* the jsMarshal aliasing stance), everything else keeps the dyn-boundary
* fence. Typed slots ride the ordinary coercion path (union slots wrap
* arm values, exactness enforced). */
intoIndexValueSlot(value: IrExpr, indexValue: IrType, node: ts.Node): IrExpr {
if (indexValue.kind !== "dyn") return this.coerceInto(node, value, indexValue);
if (value.type.kind === "dyn") return value;
// Bare `undefined`/`null` literals store the dyn unit values (JS keeps
// the key; JSON.stringify drops an undefined-valued one, like Node).
if (value.kind === "unitLit") {
return { kind: "dynFrom", value, type: DYN, loc: value.loc };
}
// An ISLAND ('any') value: the by-reference jsval→dyn wrap — the same
// edge coerceToExpected converts (dyn slots accept engine values).
if (value.type.kind === "jsval") {
return { kind: "dynFromJsval", value, type: DYN, loc: value.loc };
}
if (!this.dynConvertible(value.type)) {
this.unsupported(
"SC1100",
node,
`storing '${this.fmt(value.type)}' values under an 'unknown'-valued index signature (only numbers, strings, booleans, and JSON-safe records/arrays/unions convert)`,
);
}
return { kind: "dynFrom", value, type: DYN, loc: value.loc };
}
/** IR-level `t | undefined` through the shared canonicalizer — the
* declared result type of an index-signature read under
* noUncheckedIndexedAccess. Null when the type cannot take the arm. */
withUndefinedArmOf(t: IrType): IrType | null {
return withUndefinedArmCanonical(t, this.unions);
}
/** True when a static type converts to a dyn value (the dynFrom
* walker's domain): JSON-safe, bytes<u8> (Uint8Array/Buffer — the checked-dynamic tree's
* bytes kind, payload copied; stdin chunks into unknown-typed helpers),
* an undefined-armed union whose other arms are JSON-safe — the
* undefined arm becomes the undefined dyn singleton — or a BOXABLE
* function type (the checked-dynamic function boundary: the closure
* crosses as the checked-dynamic tree's callable kind, identity preserved). */
dynConvertible(t: IrType): boolean {
return canConvertToDyn(t, (id) => this.shapes.get(id), (id) => this.unions.get(id));
}
/** The value of a `return` statement. In an async function `return p`
* where p is a promise flattens (JS: the returned promise's settlement
* becomes the async function's result), so it lowers exactly as
* `return await p` — the awaitExpr parks the fiber and re-throws
* rejections, which IS the flattening. Everything else flows into the
* function's return slot through the usual coercion path. */
/** The value of `return <expr>` against the context's declared return —
* or NULL for a bare return: `return undefined`/`return null` in a
* void-returning function (`{ bar() { return undefined } }`, inferred
* `() => null` shapes whose return maps to void) hands the caller JS's
* undefined, which the void slot drops. Units are pure literals, so
* nothing evaluates; unit-typed non-literals keep the fences. */
lowerReturnValue(node: ts.Expression): IrExpr | null {
const expected = this.ctx.returnType;
const e = this.lowerExpr(node);
if (expected.kind === "void" && e.kind === "unitLit") return null;
if (this.ctx.isAsync && e.type.kind === "promise" && expected.kind !== "promise") {
const awaited: IrExpr = { kind: "awaitExpr", value: e, type: e.type.inner, loc: e.loc };
return this.coerceInto(node, awaited, expected);
}
return this.coerceInto(node, e, expected);
}
/** `return <expr>` lowered as a STATEMENT against the declared return.
* Void-returning contexts get the JS drop: a contextually void-typed
* function may return a value (`fv = function() { return 0; }` into a
* `() => void` slot) — the expression evaluates for its effects, the
* caller never sees a value, so the return goes out bare. Async
* void-inner returns still resolve a returned promise first. */
lowerReturnStmt(node: ts.Expression, loc: SrcLoc): IrStmt {
const expected = this.ctx.returnType;
if (expected.kind === "void") {
let e = this.lowerExpr(node);
if (this.ctx.isAsync && e.type.kind === "promise") {
e = { kind: "awaitExpr", value: e, type: e.type.inner, loc: e.loc };
}
if (e.kind === "unitLit") return { kind: "return", value: null, loc };
if (e.type.kind === "void") return { kind: "return", value: e, loc };
return {
kind: "block",
body: [
{ kind: "exprStmt", expr: e, loc },
{ kind: "return", value: null, loc },
],
loc,
};
}
return { kind: "return", value: this.lowerReturnValue(node), loc };
}
maybeNarrow(expr: IrExpr, node: ts.Node): IrExpr {
return maybeNarrow(this, expr, node);
}
lowerUnitComparison(left: IrExpr,
right: IrExpr,
negated: boolean,
loc: SrcLoc,): IrExpr | null {
return lowerUnitComparison(this, left, right, negated, loc);
}
lowerNullishCoalesce(expr: ts.BinaryExpression, loc: SrcLoc): IrExpr {
return lowerNullishCoalesce(this, expr, loc);
}
lowerOptionalChain(expr: ts.CallExpression | ts.PropertyAccessExpression | ts.ElementAccessExpression,): IrExpr {
return lowerOptionalChain(this, expr);
}
finishOptionalChain(expr: ts.Expression,
id: string,
receiver: IrExpr,
body: IrExpr,
loc: SrcLoc,): IrExpr {
return finishOptionalChain(this, expr, id, receiver, body, loc);
}
/* ── functions ────────────────────────────────────────────────────── */
/** The union without `t`'s undefined arm (unchanged when there is none, or
* when `t` isn't a union). The body-facing type of a defaulted parameter:
* tsc types uses of `x: string | undefined = "hi"` as plain `string` inside
* the body — the default removes exactly the undefined possibility. */
stripUndefinedArm(t: IrType): IrType {
if (t.kind !== "union") return t;
const def = this.unions.get(t.unionId);
if (!def || !def.arms.some((a) => a.kind === "undefinedT")) return t;
const rest = def.arms.filter((a) => a.kind !== "undefinedT");
if (rest.length === 1) return rest[0]!;
// Removing an arm keeps canonical (typeKey-sorted) order.
return { kind: "union", unionId: this.unions.intern(rest) };
}
/** The interned `T | undefined` union over a non-union arm type — the ABI
* type of a defaulted parameter, and the result type of lookups that may
* miss (process.env reads). "undefined" sorts last among all arm typeKeys,
* so the sorted pair is always [t, undefined]. */
withUndefinedArm(t: IrType): IrType {
const arms = [t, UNDEFINED_T].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
return { kind: "union", unionId: this.unions.intern(arms) };
}
paramShape(param: ts.ParameterDeclaration): ParamShape {
return paramShape(this, param);
}
checkDefaultParamBodyType(param: ts.ParameterDeclaration, bodyType: IrType): void {
return checkDefaultParamBodyType(this, param, bodyType);
}
paramShapes(params: readonly ts.ParameterDeclaration[]): ParamShape[] {
return paramShapes(this, params);
}
completeArgs(argNodes: readonly ts.Expression[],
shapes: readonly ParamShape[],
loc: SrcLoc,
blame: ts.Node,): IrExpr[] {
return completeArgs(this, argNodes, shapes, loc, blame);
}
wrappedUndefined(type: IrType, loc: SrcLoc): IrExpr | null {
return wrappedUndefined(this, type, loc);
}
/** The entry value of a binding JS initializes to `undefined` (an
* initializer-less declaration, a hoisted `var` before its statement):
* undefined-armed unions hold the interned undefined arm, and 'any'
* slots hold the ENGINE's undefined — tsc's definite-assignment
* analysis never guards `any` reads, so a jsval slot IS readable before
* any assignment and must never stay a C-level NULL (a validated exit
* or engine op on NULL is memory-unsafe, not a TypeError). Null for
* every other type: tsc rejects their pre-assignment reads. */
unassignedSlotInit(type: IrType, loc: SrcLoc): IrExpr | null {
if (type.kind === "jsval") {
return { kind: "jsOp", op: "undefLit", args: [], type: JSVAL, loc };
}
return this.wrappedUndefined(type, loc);
}
undefinedArgFor(type: IrType, loc: SrcLoc, blame: ts.Node): IrExpr {
return undefinedArgFor(this, type, loc, blame);
}
requireExactArityValue(blame: ts.Node,
contextual: ts.Expression | null,
shapes: readonly ParamShape[],
funcType: IrType,): void {
return requireExactArityValue(this, blame, contextual, shapes, funcType);
}
bodyReturnType(isAsync: boolean, declared: IrType): IrType {
return bodyReturnType(this, isAsync, declared);
}
genBodyReturnType(declared: IrType): IrType {
return declared.kind === "generator" ? declared.retT : declared;
}
declaredReturnType(decl: ts.SignatureDeclaration, blame: ts.Node): IrType {
return declaredReturnType(this, decl, blame);
}
/** Runs one declaration's collection with diagnostics captured: on
* poison, they DEFER under the declaration's symbol instead of failing
* the build — an unreached broken declaration costs nothing; the first
* reference flushes them (flushDeferred). A declaration with no name
* symbol reports eagerly (nothing could ever reference it). */
collectDeferring(symbolOf: () => ts.Symbol | undefined, collect: () => void): ts.Symbol | null {
const sink: ScrDiagnostic[] = [];
this.diagSink = sink;
try {
collect();
return null;
} catch (e) {
this.diagSink = null;
const symbol = (() => {
// symbolOf queries the checker too — a second panic must not
// escape the fence that is handling the first.
try {
return symbolOf() ?? null;
} catch {
return null;
}
})();
// An upstream tsgo panic reached through this declaration's queries
// (the 1e999 JSON-marshal signature crossed collectSignature): the
// declaration poisons under a source-anchored diagnostic, deferred
// like any collection fence — never a crashed CLI.
if (isCheckerPanic(e)) {
const decl = symbol ? this.checker.declarationsOf(symbol)[0] : undefined;
sink.push(checkerPanicDiag(
e.message.split("\n", 1)[0]!,
decl ? locOf(decl) : { file: this.entry.fileName, start: 0, end: 0 },
));
} else if (!(e instanceof PoisonError)) {
throw e;
}
if (!symbol) {
for (const d of sink) this.pushDiag(d);
return null;
}
const list = this.deferredDiags.get(symbol) ?? [];
list.push(...sink);
this.deferredDiags.set(symbol, list);
return symbol;
} finally {
this.diagSink = null;
}
}
/** Pushes a symbol's deferred collection diagnostics: lowering resolved
* a reference to it, so the declaration is part of what the entry runs.
* The reference site then proceeds exactly as before (its own rejection
* may follow) — reached-but-broken declarations report the same set of
* diagnostics the eager collector historically produced. */
flushDeferred(symbol: ts.Symbol): void {
if (this.collecting) return;
const diags = this.deferredDiags.get(symbol);
if (!diags) return;
this.deferredDiags.delete(symbol);
if (this.alreadyFlushed.has(symbol)) return; // the emit pass reported these
this.flushedSymbols.add(symbol);
for (const d of diags) this.pushDiag(d);
}
flushDeferredClass(className: string): void {
const symbol = this.deferredClassByName.get(className);
if (symbol) this.flushDeferred(symbol);
}
collectSignature(decl: ts.FunctionDeclaration): void {
return collectSignature(this, decl);
}
collectSignatureInner(decl: ts.FunctionDeclaration): void {
return collectSignatureInner(this, decl);
}
/* ── generic functions (monomorphization) ─────────────────────────── */
collectGenericSignature(decl: ts.FunctionDeclaration): void {
return collectGenericSignature(this, decl);
}
genericFnOf(ident: ts.Identifier): GenericFnInfo | null {
return genericFnOf(this, ident);
}
lowerGenericCall(expr: ts.CallExpression, info: GenericFnInfo): IrExpr {
return lowerGenericCall(this, expr, info);
}
lowerGenericFnValue(ref: ts.Expression, info: GenericFnInfo): IrExpr {
return lowerGenericFnValue(this, ref, info);
}
inferTypeParamBindings(expr: ts.CallExpression,
info: GenericFnInfo,
rsig: ts.Signature,
tsBindings?: Map<ts.Symbol, ts.Type>,): Map<ts.Symbol, IrType> {
return inferTypeParamBindings(this, expr, info, rsig, tsBindings);
}
lowerGenericInstance(info: GenericFnInfo, inst: GenericInstance): IrFunction {
return lowerGenericInstance(this, info, inst);
}
/* ── classes ──────────────────────────────────────────────────────── */
/** Checker work class SHAPE collection performs inside otherwise
* deferred JavaScript bodies. Constructor assignments declare fields,
* so collection asks for each `this.x` symbol and RHS type even when the
* constructor is unreachable. Keep that mandatory work batched without
* sweeping unrelated dead method bodies. */
private prefetchClassCollection(decls: readonly ts.ClassLikeDeclaration[]): void {
const defaultTypeNodes: ts.Node[] = [];
const typeNodes: ts.Node[] = [];
const symbolRoots: ts.Node[] = [];
for (const decl of decls) {
for (const member of decl.members) {
if (
(ts.isConstructorDeclaration(member) ||
ts.isMethodDeclaration(member) ||
ts.isGetAccessor(member) ||
ts.isSetAccessor(member)) &&
(!ts.isMethodDeclaration(member) || member.typeParameters === undefined)
) {
for (const param of member.parameters) {
if (param.initializer) defaultTypeNodes.push(param.initializer);
}
}
}
if (!isJsSourceFile(decl.getSourceFile())) continue;
for (const member of decl.members) {
if (ts.isConstructorDeclaration(member)) {
for (const param of member.parameters) {
if (param.initializer) typeNodes.push(param.initializer);
}
for (const stmt of member.body?.statements ?? []) {
if (!ts.isExpressionStatement(stmt) || !ts.isBinaryExpression(stmt.expression)) continue;
if (stmt.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken) continue;
const lhs = stmt.expression.left;
if (
(ts.isPropertyAccessExpression(lhs) || ts.isElementAccessExpression(lhs)) &&
lhs.expression.kind === ts.SyntaxKind.ThisKeyword
) {
symbolRoots.push(lhs);
// Field inference normally uses the symbol's type, but its
// panic/undefined fallback asks for the assignment node too.
typeNodes.push(lhs, stmt.expression.right);
}
}
continue;
}
if (
ts.isMethodDeclaration(member) ||
ts.isGetAccessor(member) ||
ts.isSetAccessor(member)
) {
for (const param of member.parameters) {
if (param.initializer) typeNodes.push(param.initializer);
}
// npm-static implicit-any classification resolves body parameter
// references while collecting the method's shape.
if (implicitMonoFile(decl.getSourceFile()) && member.body) {
symbolRoots.push(member.body);
}
}
}
}
if (defaultTypeNodes.length > 0) {
this.checker.prefetchCollectionTypes(defaultTypeNodes);
}
if (typeNodes.length > 0 || symbolRoots.length > 0) {
this.checker.prefetchClassCollection(typeNodes, symbolRoots);
}
}
collectClassShape(decl: ts.ClassDeclaration): void {
return collectClassShape(this, decl);
}
collectClassShapeInner(decl: ts.ClassLikeDeclaration, jsNameOverride?: string,
inst?: { family: ClassInfo; name: string; bindings: Map<ts.Symbol, IrType>; typeArgsText: string; ordinal: number },
mixin?: { base: ClassInfo; name: string; call: ts.CallExpression; bindings: Map<ts.Symbol, IrType>; context: string; ordinal: number },): void {
// Late class expressions and mixin/generic instances do not participate
// in emitReachable's initial declaration wave. Prime their mandatory
// shape queries at the collection boundary; initial declarations are
// already warm and this is memo-free.
this.prefetchClassCollection([decl]);
return collectClassShapeInner(this, decl, jsNameOverride, inst, mixin);
}
lowerClassExpressionInfo(expr: ts.ClassExpression): ClassInfo {
return lowerClassExpressionInfo(this, expr);
}
lowerClassExpression(expr: ts.ClassExpression): IrExpr {
return lowerClassExpression(this, expr);
}
/* ── the class graph (single inheritance) ─────────────────────────── */
findMethodOn(info: ClassInfo | null,
name: string,): { declarer: ClassInfo; sig: { params: ParamShape[]; ret: IrType; abstract?: true; async?: true; gen?: NonNullable<IrFunction["generator"]> } } | null {
return findMethodOn(this, info, name);
}
isSubclassOf(sub: string, sup: string): boolean {
return isSubclassOf(this, sub, sup);
}
inHierarchy(info: ClassInfo): boolean {
return inHierarchy(this, info);
}
overrideBelow(info: ClassInfo, name: string): boolean {
return overrideBelow(this, info, name);
}
upcastTo(expr: IrExpr, className: string): IrExpr {
return upcastTo(this, expr, className);
}
/** True when `className` is the %Error root or any class inside its
* hierarchy (builtin kinds and user `extends Error` subclasses). */
errorHierarchyClassOf(className: string): boolean {
if (className === "%Error" || RUNTIME_ERROR_CLASSES.has(className)) return true;
for (let c = this.classes.get(className)?.base ?? null; c; c = c.base) {
if (c.def.name === "%Error") return true;
}
return false;
}
classValueRef(info: ClassInfo, blame: ts.Node): IrExpr {
return classValueRef(this, info, blame);
}
/** Class EXPRESSIONS collected this run, in first-encounter order: the
* emit pass lowers their members after the init bodies (declaration
* members ride fp.classDecls; expressions register only when their
* containing statement lowers). */
readonly exprClasses: ClassInfo[] = [];
readonly exprClassInfoByNode = new Map<ts.ClassExpression, ClassInfo>();
/** Class expressions whose collection is IN FLIGHT — the reentrancy
* guard for heritage-demanded collection (lowerClassExpressionInfo). */
readonly collectingExprClasses = new Set<ts.ClassExpression>();
/** Static-init statements of class expressions inside the statement
* currently lowering — lowerFileInit drains the buffer immediately
* BEFORE that statement (JS's order for the supported whole-initializer
* positions). */
readonly pendingClassExprInits: IrStmt[] = [];
/** Discovery hook: registers a just-collected expression class's member
* bodies as worklist units (the units map is otherwise built before
* lowering starts). Null in the emit pass. */
onExprClassCollected: ((info: ClassInfo) => void) | null = null;
/** Mixin functions (`(Base: T) => class extends Base {…}`) by their
* function-like node: recognized shape, or null for checked
* non-qualifiers (lower-mixins.ts). */
readonly mixinFnShapes = new Map<ts.Node, MixinFnShape | null>();
/** Mixin instantiations by CALL SITE (one class per once-evaluated call
* — the class-expression identity rule); null marks a poisoned
* instantiation so re-demands fence instead of half-collecting. */
readonly mixinInstanceByCall = new Map<ts.CallExpression, ClassInfo | null>();
/** Mixin calls whose instantiation is IN FLIGHT — the cyclic-extends
* backstop (the collectingExprClasses rule). */
readonly mixinCollectingCalls = new Set<ts.CallExpression>();
/** Per mixin-class-node demand count: only the FIRST instantiation
* counts statements toward coverage (the generic-instance rule). */
readonly mixinOrdinals = new Map<ts.ClassLikeDeclaration, number>();
/** The mixin instantiation whose source is CURRENTLY collecting or
* lowering: mapType resolves the inner class node's own instance type
* (`this` inside members, self-referential fields) to THIS
* instantiation — the shared AST means the checker keeps answering the
* one class node for every instantiation, like generic bindings. */
mixinTypeContext: { classNode: ts.ClassLikeDeclaration; className: string } | null = null;
/** PINNED mixin instantiations (const-binding / heritage call sites) by
* their class node — the intersection resolver's candidate sets
* (mixinIntersectionInstanceType). */
readonly mixinInstancesByClassNode = new Map<ts.ClassLikeDeclaration, ClassInfo[]>();
mixinCallClassInfoOf(call: ts.CallExpression): ClassInfo | null {
return mixinCallClassInfoOf(this, call);
}
/** Generic classes (monomorphization by flow): declaration → the family's
* instance table. Filled by collectClassShapeInner's family mode;
* consulted by mapType's genericClassInstance hook. */
readonly genericClassByDecl = new Map<ts.ClassLikeDeclaration, GenericClassInfo>();
/** Instantiations in demand order — the member-lowering worklist run()'s
* monomorphization fixpoint drains (an instantiation's methods can
* demand further instances of either kind). */
readonly genericClassInstances: ClassInfo[] = [];
/** Discovery hook: a generic-class instantiation collected mid-lowering
* (instantiations are demand-driven, not units — their members lower in
* the instance drain). Null everywhere today; reserved for symmetry with
* onExprClassCollected should instantiations ever need eager
* registration. */
onLateClassCollected: ((info: ClassInfo) => void) | null = null;
genericClassInstanceType(decl: ts.ClassLikeDeclaration, ref: ts.Type): IrType | null {
return genericClassInstanceType(this, decl, ref);
}
findStaticOn(info: ClassInfo | null, name: string): ReturnType<typeof findStaticOn> {
return findStaticOn(this, info, name);
}
staticShadowBelow(info: ClassInfo, name: string): boolean {
return staticShadowBelow(this, info, name);
}
ctorAbiEquals(sub: ClassInfo, sup: ClassInfo): boolean {
return ctorAbiEquals(this, sub, sup);
}
exactClassOfReceiver(expr: ts.Expression): ClassInfo | null {
return exactClassOfReceiver(this, expr);
}
lowerClassMembers(info: ClassInfo): IrFunction[] {
return lowerClassMembers(this, info);
}
lowerStaticFieldInits(info: ClassInfo): IrStmt[] {
return lowerStaticFieldInits(this, info);
}
/** The method-like members (methods and accessors) of a class that have
* lowerable bodies, with their collected method-map names. */
*classMethodMembers(
info: ClassInfo,
): Generator<{ mName: string; member: ts.MethodDeclaration | ts.AccessorDeclaration }> {
if (!info.decl) return; // builtin error classes: runtime-provided bodies
for (const member of info.decl.members) {
const fnLike =
ts.isMethodDeclaration(member) || ts.isGetAccessor(member) || ts.isSetAccessor(member)
? member
: null;
if (!fnLike) continue;
// STATIC methods lower separately (`%C.static:m` via staticMethods;
// accessors stay fenced per site) — and a static member SHARING an
// instance member's name would match the instance entry in
// info.methods below and lower a second body under the same %C.name
// (the duplicate-function ICE, signature 10).
const mods = ts.canHaveModifiers(fnLike) ? ts.getModifiers(fnLike) : undefined;
if (mods?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword)) continue;
// Computed method names resolve exactly like collection did
// (classMemberNameOf — folded keys and the sym:iterator slot);
// unresolvable ones never collected, so they skip here too.
const baseName = ts.isMethodDeclaration(fnLike)
? classMemberNameOf(this, fnLike.name)
: ts.isIdentifier(fnLike.name) || ts.isPrivateIdentifier(fnLike.name)
? fnLike.name.text
: null;
if (baseName === null) continue;
const mName = ts.isMethodDeclaration(fnLike) ? baseName : `${ts.isGetAccessor(fnLike) ? "get" : "set"}:${baseName}`;
if (!info.methods.get(mName) || !fnLike.body) continue;
yield { mName, member: fnLike };
}
}
lowerClassCtor(info: ClassInfo): IrFunction {
return lowerClassCtor(this, info);
}
lowerClassMethodMember(info: ClassInfo,
fnLike: ts.MethodDeclaration | ts.AccessorDeclaration,): IrFunction | null {
return lowerClassMethodMember(this, info, fnLike);
}
throwingSetterFn(info: ClassInfo, prop: string): IrFunction {
return throwingSetterFn(this, info, prop);
}
fieldInitStmts(info: ClassInfo, thisLocal: IrLocal): IrStmt[] {
return fieldInitStmts(this, info, thisLocal);
}
lowerDerivedCtorBody(info: ClassInfo, thisLocal: IrLocal, forward?: IrExpr[]): IrStmt[] {
return lowerDerivedCtorBody(this, info, thisLocal, forward);
}
superCallStmt(info: ClassInfo,
thisLocal: IrLocal,
args: IrExpr[],
loc: SrcLoc,): IrStmt {
return superCallStmt(this, info, thisLocal, args, loc);
}
/** Declares the `this` param local, registered under the THIS_BINDING
* sentinel so lexical-this capture in arrows uses the normal machinery. */
declareThis(type: IrType): IrLocal {
const ctx = this.ctx;
const local: IrLocal = { id: "this.0", name: "this", type, mutable: false };
ctx.locals.push(local);
ctx.scopes[ctx.scopes.length - 1]!.set(THIS_BINDING, local);
return local;
}
lowerFunction(decl: ts.FunctionDeclaration): IrFunction | null {
return lowerFunction(this, decl);
}
collectGlobals(sf: ts.SourceFile, topStmts: ts.Statement[]): void {
return collectGlobals(this, sf, topStmts);
}
lowerFileInit(sf: ts.SourceFile, stmts: ts.Statement[], name: string): IrFunction {
return lowerFileInit(this, sf, stmts, name);
}
lowerDefaultExport(stmt: ts.ExportAssignment): IrStmt | null {
return lowerDefaultExport(this, stmt);
}
buildMain(): IrFunction {
return buildMain(this);
}
/* ── scoping and captures ─────────────────────────────────────────── */
declareLocal(nameNode: ts.Node, name: string, type: IrType, mutable: boolean): IrLocal {
const ctx = this.ctx;
const count = ctx.localCounters.get(name) ?? 0;
ctx.localCounters.set(name, count + 1);
const local: IrLocal = { id: `${name}.${count}`, name, type, mutable, source: bindingSource(nameNode) };
ctx.locals.push(local);
const symbol = this.checker.getSymbolAtLocation(nameNode);
if (symbol) ctx.scopes[ctx.scopes.length - 1]!.set(symbol, local);
return local;
}
/** A function-scope local bound to NO ts.Symbol — the hidden ABI slot of a
* defaulted parameter (the parameter's symbol binds to the separately-
* declared body local; nothing in the source can name this one). */
declareHiddenLocal(name: string, type: IrType): IrLocal {
const ctx = this.ctx;
const count = ctx.localCounters.get(name) ?? 0;
ctx.localCounters.set(name, count + 1);
const local: IrLocal = { id: `${name}.${count}`, name, type, mutable: false };
ctx.locals.push(local);
return local;
}
/** Declares a callee's parameter locals from its ParamShapes and builds
* the DEFAULT-PARAM PROLOGUE. Required/optional/rest params bind their
* symbol directly (one local of the ABI type). A defaulted param `x: T = e`
* gets TWO locals: the hidden ABI slot (the incoming `T | undefined`
* union) and the body local `x` of plain T, initialized by
*
* const x = <in> is undefined-arm ? e : narrow(<in>)
*
* — a lazily-branched ternary, so the default expression evaluates exactly
* when the argument was omitted or undefined (JS's call-time rule), in the
* callee scope, left-to-right across params (prologue order), and may
* reference earlier params (their body locals are already bound) and
* `this` in methods (param 0, declared before any of these). tsc rejects
* self- and forward-references inside initializers. Must be called before
* lowering the body; the returned prologue statements go first. */
declareParams(
rawDecls: readonly ts.ParameterDeclaration[],
shapes: readonly ParamShape[],
): { params: IrParam[]; prologue: IrStmt[] } {
// `this` parameters are type-world (paramShapes skipped them; callers
// never pass them) — skip here too so decls stay shape-aligned.
const decls = rawDecls.filter((p) => !isThisParameter(p));
const params: IrParam[] = [];
const prologue: IrStmt[] = [];
decls.forEach((decl, i) => {
const shape = shapes[i]!;
if (ts.isArrayBindingPattern(decl.name) || ts.isObjectBindingPattern(decl.name)) {
// Pattern parameter: one hidden ABI slot carries the source value;
// the prologue binds each name exactly like a destructuring
// declaration reading from it (the same lowerBindingPattern —
// pattern fences included). Bound names are mutable, like any
// parameter in JS.
const loc = locOf(decl);
const slot = this.declareHiddenLocal("%param", shape.type);
params.push({ localId: slot.id, name: "%param", type: shape.type });
let srcType = shape.type;
let srcRef = (): IrExpr => ({ kind: "varRef", localId: slot.id, type: shape.type, loc });
if (shape.mode === "omittable" && shape.bodyType && decl.initializer) {
// A WHOLE-PATTERN default (`({ x } = { x: 1 })`): pick the
// default exactly when the argument was omitted or undefined
// (the ABI union's undefined arm — JS's call-time rule, the
// identifier-param prologue's ternary), then destructure the
// picked value.
const abi = shape.type;
if (abi.kind === "dyn" || abi.kind === "jsval") {
// A DYNAMIC-TIER pattern source (`function f({} = a)` with
// `a: any` — jsval for island values, dyn for the checked-
// dynamic dyn): the slot holds its tier's undefined directly,
// so the default test is the runtime undefined test — then
// the pattern destructures the picked value.
const dflt = this.lowerExprExpecting(decl.initializer, abi);
const src = this.declareHiddenLocal("%psrc", abi);
const inRef = (): IrExpr => ({ kind: "varRef", localId: slot.id, type: abi, loc });
const isUndef: IrExpr =
abi.kind === "jsval"
? { kind: "jsOp", op: "eq", args: [inRef(), { kind: "jsOp", op: "undefLit", args: [], type: JSVAL, loc }], type: BOOL, loc }
: { kind: "dynTest", test: "undefined", value: inRef(), type: BOOL, loc };
prologue.push({
kind: "varDecl",
localId: src.id,
init: { kind: "ternary", cond: isUndef, then: dflt, else_: inRef(), type: abi, loc },
loc,
});
const pickedT = abi;
srcType = pickedT;
srcRef = () => ({ kind: "varRef", localId: src.id, type: pickedT, loc });
// An empty object pattern still rejects null after applying
// the default. With no property reads, nothing else performs
// RequireObjectCoercible; use V8's default-expression spelling.
if (abi.kind === "dyn" && ts.isObjectBindingPattern(decl.name) && decl.name.elements.length === 0) {
prologue.push({
kind: "exprStmt",
expr: { kind: "dynDestrCheck", value: srcRef(), spelling: "(intermediate value)(intermediate value)(intermediate value)", type: DYN, loc },
loc,
});
}
this.lowerBindingPattern(decl.name, srcRef, srcType, true, prologue);
return;
}
if (abi.kind !== "union") this.unsupported("SC1090", decl, "this parameter form"); // defensive
const undefTag = this.armTag(abi.unionId, UNDEFINED_T);
if (undefTag < 0) this.unsupported("SC1090", decl, "this parameter form"); // defensive
const isUndef: IrExpr = {
kind: "unionIsTag", unionId: abi.unionId, tag: undefTag, negated: false,
value: { kind: "varRef", localId: slot.id, type: abi, loc }, type: BOOL, loc,
};
let present: IrExpr | null = null;
if (typeEquals(shape.bodyType, abi)) {
present = { kind: "varRef", localId: slot.id, type: abi, loc };
} else if (shape.bodyType.kind === "union") {
const retag = this.unionRetagHelper(abi.unionId, shape.bodyType.unionId, loc);
if (retag) present = { kind: "call", callee: retag, args: [{ kind: "varRef", localId: slot.id, type: abi, loc }], type: shape.bodyType, loc };
} else {
const tag = this.armTag(abi.unionId, shape.bodyType);
if (tag >= 0) {
present = { kind: "unionNarrow", unionId: abi.unionId, tag, value: { kind: "varRef", localId: slot.id, type: abi, loc }, type: shape.bodyType, loc };
}
}
if (!present) this.unsupported("SC1090", decl, "this parameter form"); // defensive: abi = bodyType + undefined by construction
const dflt = this.lowerExprExpecting(decl.initializer, shape.bodyType);
const src = this.declareHiddenLocal("%psrc", shape.bodyType);
prologue.push({
kind: "varDecl",
localId: src.id,
init: { kind: "ternary", cond: isUndef, then: dflt, else_: present, type: shape.bodyType, loc },
loc,
});
const pickedT = shape.bodyType;
srcType = pickedT;
srcRef = () => ({ kind: "varRef", localId: src.id, type: pickedT, loc });
}
this.lowerBindingPattern(decl.name, srcRef, srcType, true, prologue);
return;
}
const name = (decl.name as ts.Identifier).text;
if (shape.mode === "omittable" && shape.bodyType && decl.initializer) {
const abi = shape.type;
if (abi.kind === "dyn" || abi.kind === "jsval") {
// A DYNAMIC-TIER defaulted param (`function f(x = a)` with
// `a: any` — jsval for island values, dyn for the checked-
// dynamic dyn): the slot holds its tier's undefined directly —
// the body local picks the default on the runtime test.
const loc = locOf(decl);
const slot = this.declareHiddenLocal(name, abi);
params.push({ localId: slot.id, name, type: abi });
const inRef = (): IrExpr => ({ kind: "varRef", localId: slot.id, type: abi, loc });
const isUndef: IrExpr =
abi.kind === "jsval"
? { kind: "jsOp", op: "eq", args: [inRef(), { kind: "jsOp", op: "undefLit", args: [], type: JSVAL, loc }], type: BOOL, loc }
: { kind: "dynTest", test: "undefined", value: inRef(), type: BOOL, loc };
const dflt = this.lowerExprExpecting(decl.initializer, abi);
const body = this.declareLocal(decl.name, name, abi, true);
prologue.push({
kind: "varDecl",
localId: body.id,
init: { kind: "ternary", cond: isUndef, then: dflt, else_: inRef(), type: abi, loc },
loc,
});
return;
}
if (abi.kind !== "union") this.unsupported("SC1090", decl, "this parameter form"); // defensive
const undefTag = this.armTag(abi.unionId, UNDEFINED_T);
if (undefTag < 0) this.unsupported("SC1090", decl, "this parameter form"); // defensive
const loc = locOf(decl);
const slot = this.declareHiddenLocal(name, abi);
params.push({ localId: slot.id, name, type: abi });
const inRef = (): IrExpr => ({ kind: "varRef", localId: slot.id, type: abi, loc });
if (typeEquals(shape.bodyType, abi)) {
// The default may ITSELF be undefined (`x = process.env.FOO`):
// the body keeps the full `T | undefined` union (tsc's type),
// so a present argument passes through unchanged and an omitted
// one takes the default AS IS — no narrow on either branch.
const dflt = this.lowerExprExpecting(decl.initializer, abi);
const body = this.declareLocal(decl.name, name, abi, true);
prologue.push({
kind: "varDecl",
localId: body.id,
init: {
kind: "ternary",
cond: { kind: "unionIsTag", unionId: abi.unionId, tag: undefTag, negated: false, value: inRef(), type: BOOL, loc },
then: dflt,
else_: inRef(),
type: abi,
loc,
},
loc,
});
return;
}
if (shape.bodyType.kind === "union") {
// UNION body type: a present argument re-tags from the ABI union
// (body arms + undefined) back into the body union through the
// interned retag helper — the stranded undefined arm's trap case
// is unreachable from this else-branch (the ternary just tested
// it), and every other arm maps by identity.
const retag = this.unionRetagHelper(abi.unionId, shape.bodyType.unionId, loc);
if (!retag) this.unsupported("SC1090", decl, "this parameter form"); // defensive
const dflt = this.lowerExprExpecting(decl.initializer, shape.bodyType);
const body = this.declareLocal(decl.name, name, shape.bodyType, true);
prologue.push({
kind: "varDecl",
localId: body.id,
init: {
kind: "ternary",
cond: { kind: "unionIsTag", unionId: abi.unionId, tag: undefTag, negated: false, value: inRef(), type: BOOL, loc },
then: dflt,
else_: { kind: "call", callee: retag, args: [inRef()], type: shape.bodyType, loc },
type: shape.bodyType,
loc,
},
loc,
});
return;
}
const valueTag = this.armTag(abi.unionId, shape.bodyType);
if (valueTag < 0) this.unsupported("SC1090", decl, "this parameter form"); // defensive
// The default lowers BEFORE the body local binds, so a same-named
// outer binding referenced in it can never resolve to the fresh
// local (tsc separately rejects `x = x`).
const dflt = this.lowerExprExpecting(decl.initializer, shape.bodyType);
const body = this.declareLocal(decl.name, name, shape.bodyType, true);
prologue.push({
kind: "varDecl",
localId: body.id,
init: {
kind: "ternary",
cond: { kind: "unionIsTag", unionId: abi.unionId, tag: undefTag, negated: false, value: inRef(), type: BOOL, loc },
then: dflt,
else_: { kind: "unionNarrow", unionId: abi.unionId, tag: valueTag, value: inRef(), type: shape.bodyType, loc },
type: shape.bodyType,
loc,
},
loc,
});
return;
}
const local = this.declareLocal(decl.name, name, shape.type, true);
params.push({ localId: local.id, name, type: local.type });
if (
shape.mode === "required" &&
shape.type.kind === "union" &&
this.armTag(shape.type.unionId, UNDEFINED_T) >= 0 &&
typeEquals(this.stripUndefinedArm(shape.type), this.mapTypeOf(this.typeOf(decl.name)) ?? VOID)
) {
const root = this.runtimeOptionalRootOf(local);
this.runtimeOptionalLocals.add(root);
this.runtimeOptionalStorageLocals.add(root);
}
});
return { params, prologue };
}
/** The binding for `symbol` inside context `ctx` — a scoped local or an
* already-threaded capture entry. */
bindingIn(ctx: FnCtx, symbol: ts.Symbol): IrLocal | null {
for (let i = ctx.scopes.length - 1; i >= 0; i--) {
const local = ctx.scopes[i]!.get(symbol);
if (local) return local;
}
return ctx.captureBySymbol.get(symbol) ?? null;
}
/** Resolves an identifier to a local of the CURRENT function, creating
* capture entries (and boxing the origin binding) when the name lives in
* an enclosing function. Self-references of a named lambda are NOT
* resolved here — callers check `isSelfReference` first. */
resolveLocal(ident: ts.Identifier): IrLocal | null {
let symbol = this.checker.getSymbolAtLocation(ident);
// Shorthand names read their VALUE binding (see resolveValueSymbol).
if (ident.parent && ts.isShorthandPropertyAssignment(ident.parent) && ident.parent.name === ident) {
symbol = this.checker.getShorthandAssignmentValueSymbol(ident.parent) ?? symbol;
}
if (!symbol) return null;
const direct = this.resolveKey(symbol, ident);
if (direct) return direct;
// A PARAMETER PROPERTY declares two symbols: references resolve to the
// PARAMETER symbol, while the declaration's name binds the PROPERTY
// symbol — which is what declareParams registered the local under.
// On a miss, normalize to the declaration's key and retry (ordinary
// bindings never reach this — their two sides intern to one symbol).
const vd = this.checker.valueDeclarationOf(symbol);
if (
vd && ts.isParameter(vd) && ts.isIdentifier(vd.name) &&
vd.modifiers?.some(
(m) =>
m.kind === ts.SyntaxKind.PublicKeyword ||
m.kind === ts.SyntaxKind.PrivateKeyword ||
m.kind === ts.SyntaxKind.ProtectedKeyword ||
m.kind === ts.SyntaxKind.ReadonlyKeyword ||
m.kind === ts.SyntaxKind.OverrideKeyword,
)
) {
const propSym = this.checker.getSymbolAtLocation(vd.name);
if (propSym && propSym !== symbol) return this.resolveKey(propSym, ident);
}
return null;
}
/** Lexical `this` — the enclosing method's this-param, possibly captured
* through arrows (function expressions/declarations reset `this` in JS;
* their bodies never see an enclosing method's binding). */
resolveThis(): IrLocal | null {
return this.resolveKey(THIS_BINDING);
}
/** READ-ONLY twin of resolveLocal for PROBES (isIslandExpr): answers
* the nearest binding entry without boxing, threading, or predeclaring.
* resolveKey mutates capture state as a side effect, and a speculative
* island-ness query through a context that takes no captures (a plain
* declared function between the origin and the reference) was an ICE —
* the REAL lowering path still resolves (and diagnoses) the reference
* itself. */
peekLocal(ident: ts.Identifier): IrLocal | null {
let symbol = this.checker.getSymbolAtLocation(ident);
if (ident.parent && ts.isShorthandPropertyAssignment(ident.parent) && ident.parent.name === ident) {
symbol = this.checker.getShorthandAssignmentValueSymbol(ident.parent) ?? symbol;
}
if (!symbol) return null;
for (let depth = this.fnStack.length - 1; depth >= 0; depth--) {
const hit = this.bindingIn(this.fnStack[depth]!, symbol);
if (hit) return hit;
}
return null;
}
resolveKey(symbol: ts.Symbol, blame?: ts.Node): IrLocal | null {
const direct = this.bindingIn(this.ctx, symbol);
if (direct) return direct;
// Search enclosing functions, innermost first.
for (let depth = this.fnStack.length - 2; depth >= 0; depth--) {
const origin = this.bindingIn(this.fnStack[depth]!, symbol);
if (!origin) continue;
// dyn captures ride an UNTRACED obj-box (scr_dyn_retain_v/release_v
// — boxNewC): the mustCall wrapper closing over its implicit-any
// `fn` param. A dyn tree is pure data except the function kind,
// whose closure edge the collector never sees — cycles through a
// captured dyn are uncollectable (leak, never dangle: trial
// deletion treats untraced edges as external roots). SEMANTICS.md.
// jsval captures are fine: the box is an obj-box carrying the
// island handle's own retain/release (scr_jsval_*_v), untraced like
// every jsval container position — engine-side back-references are
// the island's documented collection stance, not the box's.
if (origin.type.kind === "caught") {
// A catch binding never escapes its catch (KEEP NARROW): narrow it
// into a typed local and capture THAT.
this.unsupported(
"SC1090",
blame ?? this.checker.declarationsOf(symbol)[0] ?? this.entry,
"closures capturing catch bindings (narrow into a typed local first)",
);
}
// The binding escapes into a nested function: it must live in a box,
// shared by everyone (that's what makes mutation visible everywhere).
origin.boxed = true;
// Thread a capture through every function between origin and here.
let parentEntry = origin;
for (let j = depth + 1; j < this.fnStack.length; j++) {
const ctx = this.fnStack[j]!;
let entry = ctx.captureBySymbol.get(symbol);
if (!entry) {
// A context that takes NO captures (a plain declared function —
// monomorphized/implicit instances lower this way) cannot carry
// the binding through: the shape is a module binding whose only
// storage is the init function's LOCAL (a typed-but-unmappable
// const — the file-scope `new Map()` ledger idiom) read from
// inside a nested instance. Fence it — in JS the statement
// defers to its runtime trap like every collection failure;
// asserting here was an ICE on ordinary npm-static JS.
if (ctx.captures === null) {
this.unsupported(
"SC1090",
blame ?? this.checker.declarationsOf(symbol)[0] ?? this.entry,
`the binding '${origin.name}' captured through a plain nested function (the declaration has no static storage a capture can thread — bind the value through a typed const, or read it in the declaring scope)`,
);
}
const count = ctx.localCounters.get(origin.name) ?? 0;
ctx.localCounters.set(origin.name, count + 1);
entry = {
id: `${origin.name}.${count}`,
name: origin.name,
type: origin.type,
mutable: origin.mutable,
boxed: true,
...(origin.source ? { source: origin.source } : {}),
// TDZ travels with the binding: reads through ANY capture of a
// forward-captured const must trap while the box is empty.
...(origin.tdz ? { tdz: true as const } : {}),
};
ctx.locals.push(entry);
ctx.captureBySymbol.set(symbol, entry);
ctx.captures.push({ localId: entry.id, name: entry.name, type: entry.type });
ctx.captureSources.push(parentEntry.id);
const runtimeOptionalRoot = this.runtimeOptionalRootOf(parentEntry);
if (this.runtimeOptionalStorageLocals.has(runtimeOptionalRoot)) {
this.runtimeOptionalRoots.set(entry, runtimeOptionalRoot);
}
}
parentEntry = entry;
}
return parentEntry;
}
// Nothing declared yet anywhere on the stack: the hoisted-handler shape
// — a function declared BEFORE a const it captures (`const cleanup =
// () => onSigInt; ...; const onSigInt = ...`). Pre-declare the const as
// a TDZ box at its scope's entry and resolve again (the recursion finds
// it in the origin frame and threads captures normally).
if (blame && predeclareForwardCapture(this, symbol)) {
return this.resolveKey(symbol, blame);
}
// The FUNCTION-DECLARATION twin: JS hoists a nested `function f() {}`
// to scope entry, so a reference lexically ABOVE the declaration in the
// same function (`http.createServer(handler).listen(0, cb)` with
// `function handler(req, res)` below — the suite's standard layout) is
// a live binding, never a TDZ read. Lower the declaration eagerly at
// the reference and resolve again; the statement loop skips the source
// statement when it arrives.
if (blame && predeclareForwardFnDecl(this, symbol)) {
return this.resolveKey(symbol, blame);
}
// The `var` twin: a reference above the `var` statement (a direct read
// tsc allowed because the type carries undefined, or a nested function
// capturing the binding early). JS reads undefined there — never a TDZ
// error — so only undefined-armed types predeclare; the rest land on
// rejectUnresolvedSymbol's named fence.
if (blame && predeclareForwardVar(this, symbol)) {
return this.resolveKey(symbol, blame);
}
return null;
}
isSelfReference(ident: ts.Identifier): boolean {
const ctx = this.ctx;
if (!ctx.selfSymbol) return false;
// The self binding can be shadowed by a scoped local of the same symbol?
// No — a shadow is a different symbol; symbol identity is exact.
return this.checker.getSymbolAtLocation(ident) === ctx.selfSymbol;
}
/* ── statements ───────────────────────────────────────────────────── */
lowerStmts(stmts: readonly ts.Statement[]): IrStmt[] {
return lowerStmts(this, stmts);
}
noteBlockedBindings(stmt: ts.Statement): void {
return noteBlockedBindings(this, stmt);
}
isBlockedBinding(symbol: ts.Symbol | null): boolean {
return isBlockedBinding(this, symbol);
}
/** The cascade rejection: an honest "inherits its declaration's blocker"
* diagnostic (SC2004) when the symbol is a known-blocked binding, the
* caller's own fallback otherwise. */
rejectUnresolved(ident: ts.Identifier, fallback: string): never {
this.rejectUnresolvedSymbol(this.resolveValueSymbol(ident), ident.text, ident, fallback);
}
rejectUnresolvedSymbol(
symbol: ts.Symbol | null,
name: string,
node: ts.Node,
fallback: string,
): never {
if (this.isBlockedBinding(symbol)) {
this.pushDiag(blockedBindingUseDiag(name, locOf(node)));
throw new PoisonError();
}
// A binding whose TYPE keeps a generic call signature and whose
// declaration carries no initializer (`declare const o4: undefined |
// (<T>(f: (a: T) => T) => T)` — the optional-chained ambient shape):
// there is no function body to monomorphize, so no use can ever pin a
// concrete signature — name the shape instead of the generic
// binding-form text.
if (symbol) {
const d = this.checker.valueDeclarationOf(symbol);
if (d && ts.isVariableDeclaration(d) && d.initializer === undefined) {
const t = this.checker.getTypeOfSymbol(symbol);
const parts = t.isUnionType() ? ts.constituentTypes(t) : [t];
if (parts.some((p) => this.checker.getCallSignatures(p).some((s) => s.getTypeParameters().length > 0))) {
this.unsupported(
"SC1030",
node,
`the generic-signature binding '${name}' (its type keeps type parameters and the declaration has no initializer — no function body exists to monomorphize, so nothing can pin a concrete signature)`,
);
}
}
}
// An unresolved reference to a LATER `var` whose predeclare was
// refused: the reads Node would serve before the declaration's
// assignment are `undefined`, and this binding's type has no slot for
// that value — name the shape instead of the generic no-lowering text.
if (symbol) {
const d = this.checker.valueDeclarationOf(symbol);
if (d && ts.isVariableDeclaration(d) && (ts.getCombinedNodeFlags(d) & ts.NodeFlags.BlockScoped) === 0) {
this.unsupported(
"SC1030",
node,
`the reference to '${name}' above its 'var' declaration (a read there would be 'undefined', which the binding's type cannot hold — annotate it '| undefined' or move the declaration up)`,
);
}
}
this.unsupported("SC1090", node, fallback);
}
lowerScopedBlock(stmt: ts.Statement): IrStmt[] {
return lowerScopedBlock(this, stmt);
}
/** Lowers inside a jump-target marker. `labels` carries the construct's
* source label names so labeled jumps resolve against them. */
inCtl<T>(kind: "loop" | "switch" | "block", fn: () => T, labels?: string[]): T {
this.ctx.ctl.push(labels !== undefined && labels.length > 0 ? { kind, labels } : { kind });
try {
return fn();
} finally {
this.ctx.ctl.pop();
}
}
/** The label names a `lbl:` chain put on the statement currently being
* lowered — set by lowerLabeled around lowering the labeled construct,
* consumed exactly once by the construct's own lowering (takeLabels).
* A lowering that never consumes them signals lowerLabeled to fence:
* silently dropping a label would compile `break lbl` wrong. */
pendingLabels: string[] | null = null;
takeLabels(): string[] | undefined {
const labels = this.pendingLabels;
this.pendingLabels = null;
return labels ?? undefined;
}
lowerStmt(stmt: ts.Statement): IrStmt | IrStmt[] | null {
return lowerStmt(this, stmt);
}
lowerVarStatement(stmt: ts.VariableStatement): IrStmt[] {
return lowerVarStatement(this, stmt);
}
lowerDestructuringDecl(decl: ts.VariableDeclaration, isLet: boolean): IrStmt[] {
return lowerDestructuringDecl(this, decl, isLet);
}
lowerDestructuringAssignParts(target: ts.ObjectLiteralExpression | ts.ArrayLiteralExpression, rhs: ts.Expression, loc: SrcLoc): { stmts: IrStmt[]; value: IrExpr } {
return lowerDestructuringAssignParts(this, target, rhs, loc);
}
lowerBindingPattern(pattern: ts.ArrayBindingPattern | ts.ObjectBindingPattern,
srcRef: () => IrExpr,
srcType: IrType,
isLet: boolean,
out: IrStmt[],
dynSpell?: string,
allowDynObject = false,): void {
if (ts.isObjectBindingPattern(pattern)) {
fenceFetchObjectBinding(this, pattern);
// parseArgs's declaration family deliberately maps to dyn. Infer the
// scoped bridge at the binding site as well as at initializer-backed
// declarations, covering parameter and for-of element patterns.
allowDynObject ||= srcType.kind === "dyn" &&
isParseArgsDynCheckerType(this, this.typeOf(pattern));
}
// An ISLAND source (`const { readFileSync } = await import("fs")` —
// a namespace handle, or any 'any'-typed object): each bound name is
// an engine property read, mirroring the island property-read rule —
// a member the .d.ts declares as a primitive exits eagerly to the
// static type; everything else (including members whose declared
// types have no static mapping, like @types/node's function types)
// stays a HANDLE, and its use sites dispatch to engine ops. Patterns
// the element-wise walk cannot spell — ARRAY patterns (the iterator
// protocol), empty patterns (the coercion checks), holes, rest,
// defaults — run the REAL pattern in a synthesized engine function
// instead (lowerJsvalBindingPattern).
const elementWise =
srcType.kind === "jsval" &&
ts.isObjectBindingPattern(pattern) &&
pattern.elements.length > 0 &&
pattern.elements.every(
(el) =>
!el.dotDotDotToken &&
!el.initializer &&
el.name !== undefined &&
ts.isIdentifier(el.propertyName ?? el.name),
);
if (srcType.kind === "jsval" && !elementWise) {
if (lowerJsvalBindingPattern(this, pattern, srcRef, isLet, out)) return;
// No engine form (computed keys, untransportable defaults): fall
// through to the static fences below.
}
if (srcType.kind === "jsval" && ts.isObjectBindingPattern(pattern)) {
for (const el of pattern.elements) {
this.checkBindingElement(el);
// 7's BindingElement declares name optional (array elisions are
// nameless there); object-pattern elements always carry one.
if (el.name === undefined) continue;
const prop = el.propertyName ?? el.name;
if (!ts.isIdentifier(prop)) {
this.unsupported("SC1031", el, "destructuring with computed or non-identifier keys");
}
const loc = locOf(el);
const read: IrExpr = {
kind: "jsOp", op: "getProp", name: prop.text, args: [srcRef()], type: JSVAL, loc,
};
if (!ts.isIdentifier(el.name)) {
// A nested pattern reads through its own handle temp.
const tmp = this.declareHiddenLocal("%destr", JSVAL);
out.push({ kind: "varDecl", localId: tmp.id, init: read, loc });
this.lowerBindingPattern(
el.name,
() => ({ kind: "varRef", localId: tmp.id, type: JSVAL, loc }),
JSVAL, isLet, out,
);
continue;
}
const declared = this.mapTypeOf(this.typeOf(el.name));
const primitive =
declared &&
(declared.kind === "f64" || declared.kind === "bool" || declared.kind === "string");
const value: IrExpr = primitive
? { kind: "jsExit", value: read, type: declared, loc }
: read;
const symbol = this.checker.getSymbolAtLocation(el.name);
const g = symbol ? this.globalsBySymbol.get(symbol) : undefined;
if (g) {
out.push({ kind: "assign", localId: g.id, value: this.coerceInto(el.name, value, g.type), loc });
continue;
}
const local = this.declareLocal(el.name, el.name.text, value.type, isLet);
out.push({ kind: "varDecl", localId: local.id, init: value, loc });
}
return;
}
return lowerBindingPattern(this, pattern, srcRef, srcType, isLet, out, dynSpell, allowDynObject);
}
checkBindingElement(el: ts.BindingElement, allowDefault = false): void {
return checkBindingElement(this, el, allowDefault);
}
bindPatternTarget(name: ts.BindingName,
value: IrExpr,
isLet: boolean,
out: IrStmt[],
allowDynObject = false,): void {
return bindPatternTarget(this, name, value, isLet, out, allowDynObject);
}
lowerVarDeclList(list: ts.VariableDeclarationList): IrStmt | null {
return lowerVarDeclList(this, list);
}
lowerVarDecl(decl: ts.VariableDeclaration, isLet: boolean): IrStmt | null {
// ISLAND-HANDLE rescue (--dynamic): `const factory = (await
// import("./x.mjs")).default` / `const buf = islandReadFileSync(p)` —
// a binding whose DECLARED type either has no static mapping or has
// one no island value can EXIT to (bytes, functions, promises), but
// whose initializer is an island value. The declared type is a .d.ts
// surface over an engine value; the binding stays a HANDLE (jsval
// local) and typed use sites go through engine ops and validated
// exits like any island value. Exit-CAPABLE declared types (numbers,
// strings, JSON-safe composites, their undefined-armed unions) keep
// the standard path and its validated-exit machinery; so does
// everything non-island — including badType here when the
// initializer turns out not to be island-typed, which is exactly
// what the standard path would have reported.
// Only reference-shaped initializers are candidates (awaits, calls,
// member reads, identifiers, casts/parens over those): they are the
// island producers, and re-lowering one on the fall-through emits
// nothing twice — a lambda or literal initializer would (each
// lowering mints a fresh %fn), and is never an island value anyway.
const islandCandidate = (e: ts.Expression): boolean => {
let cur = e;
while (
ts.isParenthesizedExpression(cur) ||
ts.isAsExpression(cur) ||
ts.isTypeAssertion(cur) ||
ts.isNonNullExpression(cur)
) {
cur = cur.expression;
}
if (
!ts.isAwaitExpression(cur) &&
!ts.isCallExpression(cur) &&
!ts.isPropertyAccessExpression(cur) &&
!ts.isElementAccessExpression(cur) &&
!ts.isIdentifier(cur)
) {
return false;
}
// A lambda ANYWHERE inside (a call argument) would emit its %fn
// twice across the fall-through's re-lowering — skip those.
let lambda = false;
const scan = (n: ts.Node): void => {
if (lambda) return;
if (ts.isArrowFunction(n) || ts.isFunctionExpression(n)) {
lambda = true;
return;
}
ts.forEachChild(n, scan);
};
scan(cur);
return !lambda;
};
if (
this.dynamic &&
ts.isIdentifier(decl.name) &&
decl.initializer !== undefined &&
// `var` stays out: the rescue's block-positioned jsval local can't
// model the function-scoped hoisted binding (a redeclaration or an
// out-of-block read would split the variable in two) — vars take the
// standard path and its own fences.
(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.BlockScoped) !== 0 &&
islandCandidate(decl.initializer) &&
// createRequire's plumbing decls are COMPILE-TIME erasures (the
// require binding and its builtin-namespace bindings) — never
// island values; the standard path's skips own them.
!createRequireBindingDecl(this, decl.name, decl.initializer) &&
!createRequireNamespaceDecl(this, decl.name, decl.initializer)
) {
const mapped = this.mapTypeOf(this.typeOf(decl.name));
const handleOnly =
mapped === null ||
(mapped.kind !== "jsval" &&
mapped.kind !== "void" &&
!canExitIslandToType(
mapped,
(id) => this.shapes.get(id),
(id) => this.unions.get(id),
));
const symbol = this.checker.getSymbolAtLocation(decl.name);
if (handleOnly && (!symbol || !this.globalsBySymbol.has(symbol))) {
const init = this.lowerExpr(decl.initializer);
if (init.type.kind === "jsval") {
const local = this.declareLocal(decl.name, decl.name.text, JSVAL, isLet);
return { kind: "varDecl", localId: local.id, init, loc: locOf(decl) };
}
// The CHECKED-DYNAMIC twin of the handle rescue (the runtime-world
// local rule): an unmappable declared type over a dyn initializer
// (`const first = plugins[0]` — the checker spells 'string |
// object' while the read is a dyn keyed read) keeps the binding
// dyn; typed use sites ride validated extractions and the routed
// engine ops, exactly the JSON.parse-binding story.
if (mapped === null && init.type.kind === "dyn") {
const local = this.declareLocal(decl.name, decl.name.text, DYN, isLet);
return { kind: "varDecl", localId: local.id, init, loc: locOf(decl) };
}
if (mapped === null) this.badType(decl.name, this.typeOf(decl.name));
// A mappable declared type with a non-island initializer: the
// standard path owns it (the initializer lowered clean; re-running
// it re-produces the same IR with no duplicate diagnostics).
}
}
return lowerVarDecl(this, decl, isLet);
}
lowerSwitch(stmt: ts.SwitchStatement): IrStmt {
return lowerSwitch(this, stmt);
}
lowerTry(stmt: ts.TryStatement): IrStmt {
return lowerTry(this, stmt);
}
lowerExprStatement(expr: ts.Expression): IrStmt {
return lowerExprStatement(this, expr);
}
lowerForOf(stmt: ts.ForOfStatement): IrStmt {
const optional = this.runtimeOptionalIdentifierValue(stmt.expression);
if (optional?.present.kind !== "string") return lowerForOf(this, stmt);
const helper = this.narrowedArmHelper(optional.unionId, optional.present, locOf(stmt.expression));
if (!helper) return lowerForOf(this, stmt);
return this.withExpressionOverride(
stmt.expression,
{ kind: "call", callee: helper, args: [optional.value], type: optional.present, loc: locOf(stmt.expression) },
() => lowerForOf(this, stmt),
);
}
lowerForStatement(stmt: ts.ForStatement): IrStmt {
return lowerForStatement(this, stmt);
}
lowerCondition(expr: ts.Expression): IrExpr {
return lowerCondition(this, expr);
}
ensureBool(e: IrExpr, node: ts.Expression): IrExpr {
return ensureBool(this, e, node);
}
requireTruthyUnion(unionId: string, node: ts.Expression): void {
return requireTruthyUnion(this, unionId, node);
}
eqComparableUnion(unionId: string): boolean {
return eqComparableUnion(this, unionId);
}
/* ── expressions ──────────────────────────────────────────────────── */
lowerExpr(expr: ts.Expression): IrExpr {
return lowerExpr(this, expr);
}
lowerIntrinsicProperty(expr: ts.PropertyAccessExpression): IrExpr | null {
// The builtin-spoke property extensions (Stats.mtimeMs,
// SpawnSyncReturns.signal) claim their reads before the intrinsic
// fallback's member fences fire for them.
return lowerBuiltinExtraProperty(this, expr) ?? lowerIntrinsicProperty(this, expr);
}
/** True for the STANDARD LIBRARY's source files: the shipped ambient
* .d.ts files (core + overrides + fallback), a lib.*.d.ts bundled with the typescript
* package (asked via program.isSourceFileDefaultLibrary, never by path
* matching), or the ADOPTED @types/node surface standing in for the
* fallback (see loadProgram — the lowering tables recognize the same
* members by name + this provenance, and everything else those files
* declare hits the SC2020-family fence). The file half of every
* supported-surface provenance check. */
readonly isStdlibFile = (sf: ts.SourceFile): boolean =>
sf.fileName === this.ambient ||
sf.fileName === this.overridesAmbient ||
sf.fileName === this.fallbackAmbient ||
this.program.isSourceFileDefaultLibrary(sf) ||
(sf.isDeclarationFile && isNodeTypesPath(sf.fileName));
nodeTypesOnlySymbol(sym: ts.Symbol | null | undefined): boolean {
return nodeTypesOnlySymbol(this, sym);
}
/** True for an npm package's shipped declaration files — under
* node_modules but NOT the standard library (typescript's own lib files
* live under node_modules too), or inside a registered workspace-linked
* package (a node_modules symlink whose realpath'd files carry no
* node_modules segment — workspace-registry.ts). The provenance half of the npm
* typing rule (package types are island handles) and of the per-package
* requires-dynamic attribution. */
readonly isNpmFile = (sf: ts.SourceFile): boolean =>
sf.isDeclarationFile &&
(sf.fileName.includes("/node_modules/") || workspacePackageOfPath(sf.fileName) !== null) &&
!this.isStdlibFile(sf);
npmPackageOf(type: ts.Type): string | null {
return npmPackageOf(this, type);
}
npmMemberFence(access: ts.PropertyAccessExpression): void {
return npmMemberFence(this, access);
}
npmPackageOfSymbol(sym: ts.Symbol | undefined): string | null {
return npmPackageOfSymbol(this, sym);
}
isStdlibMember(access: ts.PropertyAccessExpression): boolean {
return isStdlibMember(this, access);
}
isStdlibSymbol(symbol: ts.Symbol | undefined): boolean {
return isStdlibSymbol(this, symbol);
}
isStdlibGlobal(expr: ts.Expression, name: string): boolean {
return isStdlibGlobal(this, expr, name);
}
stdlibGlobalMember(access: ts.PropertyAccessExpression, name: string): string | null {
return stdlibGlobalMember(this, access, name);
}
lowerArrayLiteral(expr: ts.ArrayLiteralExpression, expected?: (IrType & { kind: "array" }) | (IrType & { kind: "record" })): IrExpr {
return lowerArrayLiteral(this, expr, expected);
}
lowerObjectLiteral(expr: ts.ObjectLiteralExpression, expected?: IrType & { kind: "record" }): IrExpr {
return lowerObjectLiteral(this, expr, expected);
}
lowerShorthandValue(prop: ts.ShorthandPropertyAssignment): IrExpr {
return lowerShorthandValue(this, prop);
}
rejectThisInObjectMethod(node: ts.Node): void {
return rejectThisInObjectMethod(this, node);
}
lowerElementAccess(expr: ts.ElementAccessExpression): IrExpr {
if (
ts.isPropertyAccessExpression(expr.expression) &&
ts.isIdentifier(expr.expression.expression) &&
expr.expression.expression.text === "require" &&
expr.expression.name.text === "cache"
) {
return lowerElementAccess(this, expr);
}
const receiver = this.runtimeOptionalIdentifierValue(expr.expression);
const receiverValue = receiver && (receiver.present.kind === "array" || receiver.present.kind === "record")
? (() => {
const helper = this.narrowedArmHelper(receiver.unionId, receiver.present, locOf(expr.expression));
return helper ? { kind: "call" as const, callee: helper, args: [receiver.value], type: receiver.present, loc: locOf(expr.expression) } : null;
})()
: null;
if (receiverValue && receiver?.present.kind === "record" && ts.isNumericLiteral(expr.argumentExpression)) {
const shape = this.shapes.get(receiver.present.shapeId);
const index = expr.argumentExpression.text;
const field = shape?.tuple ? shape.fields.find((candidate) => candidate.name === String(Number(index))) : undefined;
if (field) {
const read: IrExpr = {
kind: "recordGet",
obj: receiverValue,
shapeId: receiver.present.shapeId,
field: field.name,
type: field.type,
loc: locOf(expr),
};
if (
expr.parent && ts.isTypeOfExpression(expr.parent) &&
(field.type.kind === "f64" || field.type.kind === "bool" || field.type.kind === "string")
) {
return { kind: "dynFrom", value: read, type: DYN, loc: locOf(expr) };
}
return read;
}
}
const key = this.runtimeOptionalIdentifierValue(expr.argumentExpression);
const keyValue = key?.present.kind === "string" ? this.ensureString(key.value, expr.argumentExpression) : null;
const lower = (): IrExpr => lowerElementAccess(this, expr);
const withKey = (): IrExpr => keyValue ? this.withExpressionOverride(expr.argumentExpression, keyValue, lower) : lower();
return receiverValue ? this.withExpressionOverride(expr.expression, receiverValue, withKey) : withKey();
}
lowerRecordKeyRead(
expr: ts.ElementAccessExpression,
shapeId: string,
shape: IrRecordShape,
includeUndefined = false,
): IrExpr {
return lowerRecordKeyRead(this, expr, shapeId, shape, includeUndefined);
}
lowerElementWrite(expr: ts.BinaryExpression): IrStmt {
return lowerElementWrite(this, expr);
}
ensureString(e: IrExpr, node: ts.Node): IrExpr {
return ensureString(this, e, node);
}
lowerTemplate(expr: ts.TemplateExpression): IrExpr {
return lowerTemplate(this, expr);
}
lowerAsExpression(expr: ts.AsExpression | ts.TypeAssertion): IrExpr {
// ISLAND value cast to a PROMISE type (`factory(opts) as Promise<Mod>`
// — the Node-typed async-API shape): promises never have a validated
// exit, so instead of refusing the build the cast DEFERS the failure
// to runtime — island.castFail evaluates the value (its side effects
// are real) and throws a catchable TypeError naming the target, so
// typed-but-never-executed code (a wasm decode path behind a
// rejecting import) still compiles and a reached cast fails loudly at
// the exact site. Claimed only when the source is island-typed by the
// checker (jsval, or a promise whose inner is a handle); a source
// that lowers to a real static promise keeps erasure — the standard
// path's rule for non-island inners. Static builds keep their
// per-site diagnostics.
if (this.dynamic) {
const srcMapped = this.mapTypeOf(this.typeOf(expr.expression));
const island =
srcMapped?.kind === "jsval" ||
(srcMapped?.kind === "promise" && srcMapped.inner.kind === "jsval");
if (island) {
const targetTs = this.checker.getTypeFromTypeNode(expr.type);
const target = this.mapTypeOf(targetTs);
if (target?.kind === "promise") {
const inner = this.lowerExpr(expr.expression);
if (inner.type.kind !== "jsval") return inner; // static promise: erasure
const loc = locOf(expr);
const name: IrExpr = {
kind: "strLit", value: this.fmt(target), type: STRING, loc,
};
return { kind: "libCall", fn: "island.castFail", args: [inner, name], type: target, loc };
}
}
}
return lowerAsExpression(this, expr);
}
lowerPrefixUnary(expr: ts.PrefixUnaryExpression): IrExpr {
return lowerPrefixUnary(this, expr);
}
lowerBinary(expr: ts.BinaryExpression): IrExpr {
return lowerBinary(this, expr);
}
lowerCaughtTypeofTest(expr: ts.BinaryExpression, loc: SrcLoc): IrExpr | null {
return lowerCaughtTypeofTest(this, expr, loc);
}
caughtRead(node: ts.Identifier, local: IrLocal, loc: SrcLoc): IrExpr {
return caughtRead(this, node, local, loc);
}
caughtLocalOf(node: ts.Expression): IrLocal | null {
return caughtLocalOf(this, node);
}
caughtToString(node: ts.Expression): IrExpr | null {
return caughtToString(this, node);
}
lowerInstanceOf(expr: ts.BinaryExpression, loc: SrcLoc): IrExpr {
const lower = (): IrExpr => this.withRuntimeOptionalClassValue(expr.right, () => lowerInstanceOf(this, expr, loc));
if (!ts.isIdentifier(expr.right) || this.mapTypeOf(this.typeOf(expr.right))?.kind !== "classval") {
return lower();
}
const optionalLeft = this.runtimeOptionalIdentifierValue(expr.left);
const left = optionalLeft?.value ?? (ts.isElementAccessExpression(expr.left) ? this.lowerExpr(expr.left) : null);
if (!left) return lower();
if (left.type.kind !== "union" || this.armTag(left.type.unionId, UNDEFINED_T) < 0) return lower();
const present = this.stripUndefinedArm(left.type);
if (present.kind !== "object") return lower();
const undefinedTag = this.armTag(left.type.unionId, UNDEFINED_T);
const valueTag = this.armTag(left.type.unionId, present);
if (undefinedTag < 0 || valueTag < 0) return lower();
const stable = this.declareHiddenLocal("%instanceofLeft", left.type);
const value = varRef(stable.id, left.type, locOf(expr.left));
const optionalRight = this.runtimeOptionalIdentifierValue(expr.right);
let right: IrExpr;
if (optionalRight?.present.kind === "classval") {
const helper = this.narrowedArmHelper(optionalRight.unionId, optionalRight.present, locOf(expr.right));
if (!helper) return lower();
right = { kind: "call", callee: helper, args: [optionalRight.value], type: optionalRight.present, loc: locOf(expr.right) };
} else {
right = this.lowerExpr(expr.right);
}
if (right.type.kind !== "classval") return lower();
const stableRight = this.declareHiddenLocal("%instanceofRight", right.type);
const rightValue = varRef(stableRight.id, right.type, locOf(expr.right));
const body = this.withExpressionOverride(expr.left, {
kind: "unionNarrow", unionId: left.type.unionId, tag: valueTag, value, type: present, loc: locOf(expr.left),
}, () => this.withExpressionOverride(expr.right, rightValue, () => lowerInstanceOf(this, expr, loc)));
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: stable.id, init: left, loc: locOf(expr.left) },
{ kind: "varDecl", localId: stableRight.id, init: right, loc: locOf(expr.right) },
],
result: {
kind: "ternary",
cond: { kind: "unionIsTag", unionId: left.type.unionId, tag: undefinedTag, negated: false, value, type: BOOL, loc },
then: { kind: "boolLit", value: false, type: BOOL, loc },
else_: body,
type: BOOL,
loc,
},
type: BOOL,
loc,
};
}
private withRuntimeOptionalClassValue<T>(node: ts.Expression, lower: () => T): T {
if (this.mapTypeOf(this.typeOf(node))?.kind !== "classval") return lower();
const optional = this.runtimeOptionalIdentifierValue(node);
if (optional?.present.kind !== "classval") return lower();
const helper = this.narrowedArmHelper(optional.unionId, optional.present, locOf(node));
if (!helper) return lower();
return this.withExpressionOverride(
node,
{ kind: "call", callee: helper, args: [optional.value], type: optional.present, loc: locOf(node) },
lower,
);
}
private withExpressionOverride<T>(node: ts.Expression, value: IrExpr, lower: () => T): T {
const previous = this.chainRecvByNode.get(node);
this.chainRecvByNode.set(node, value);
try {
return lower();
} finally {
if (previous) this.chainRecvByNode.set(node, previous);
else this.chainRecvByNode.delete(node);
}
}
lowerCall(expr: ts.CallExpression): IrExpr {
// Ambient fetch (native in static builds, island-backed under
// --dynamic) and dynamic import() claim their calls before the general
// dispatch. The general identifier-call paths have no lowering for a
// promise-returning ambient global, and `import` is a keyword callee no
// identifier path matches.
return (
lowerFfiCall(this, expr) ??
lowerFetchCall(this, expr) ??
lowerStaticFetchCompanionCall(this, expr) ??
lowerStaticAbortControllerCall(this, expr) ??
lowerStaticAbortSignalListenerCall(this, expr) ??
lowerStaticReadableStreamCancelCall(this, expr) ??
lowerStaticReadableStreamControllerCall(this, expr) ??
lowerStaticReadableStreamReaderCall(this, expr) ??
lowerStaticResponseCall(this, expr) ??
lowerFetchElementMethodCall(this, expr) ??
lowerDynamicImportCall(this, expr) ??
lowerCall(this, expr)
);
}
isTopLevelFnSymbol(ident: ts.Identifier): boolean {
return isTopLevelFnSymbol(this, ident);
}
lowerNestedFunctionDecl(stmt: ts.FunctionDeclaration): IrStmt {
return lowerNestedFunctionDecl(this, stmt);
}
lambdaSignature(node: ts.ArrowFunction | ts.FunctionExpression | ts.FunctionDeclaration | ts.MethodDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration,): { shapes: ParamShape[]; funcType: IrType & { kind: "func" } } {
return lambdaSignature(this, node);
}
lowerLambda(node: ts.ArrowFunction | ts.FunctionExpression | ts.FunctionDeclaration | ts.MethodDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration,): IrExpr {
return lowerLambda(this, node);
}
lowerArrayMethodCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerArrayMethodCall(this, call, access);
}
lowerMapMethodCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerMapMethodCall(this, call, access);
}
lowerMapForEachCall(call: ts.CallExpression,
receiver: IrExpr,
mapT: IrType & { kind: "map" },): IrExpr {
return lowerMapForEachCall(this, call, receiver, mapT);
}
buildMapForEachFn(name: string,
mapT: IrType & { kind: "map" },
arity: number,
fnRet: IrType,
loc: SrcLoc,): IrFunction {
return buildMapForEachFn(this, name, mapT, arity, fnRet, loc);
}
lowerSetMethodCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerSetMethodCall(this, call, access);
}
lowerSetForEachCall(call: ts.CallExpression,
receiver: IrExpr,
setT: IrType & { kind: "set" },): IrExpr {
return lowerSetForEachCall(this, call, receiver, setT);
}
buildSetForEachFn(name: string,
setT: IrType & { kind: "set" },
arity: number,
fnRet: IrType,
loc: SrcLoc,): IrFunction {
return buildSetForEachFn(this, name, setT, arity, fnRet, loc);
}
lowerRegexLiteral(expr: ts.RegularExpressionLiteral): IrExpr {
return lowerRegexLiteral(this, expr);
}
lowerRegexMethodCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerRegexMethodCall(this, call, access);
}
lowerStringMethodCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerStringMethodCall(this, call, access);
}
lowerBytesNew(expr: ts.NewExpression, symbol: ts.Symbol | null | undefined): IrExpr | null {
return lowerBytesNew(this, expr, symbol);
}
lowerBytesMethodCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerBytesMethodCall(this, call, access);
}
lowerBufferStaticCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerBufferStaticCall(this, call, access);
}
fieldTarget(access: ts.PropertyAccessExpression): FieldTarget | null {
return fieldTarget(this, access);
}
uniqueSymbolKeyOf(key: ts.Expression): { sym: ts.Symbol; fieldName: string } | null {
return uniqueSymbolKeyOf(this, key);
}
foldedStringKeyOf(expr: ts.Expression): string | null {
return foldedStringKeyOf(this, expr);
}
accessorCall(className: string,
member: string,
obj: IrExpr,
extraArgs: IrExpr[],
ret: IrType,
loc: SrcLoc,): IrExpr {
return accessorCall(this, className, member, obj, extraArgs, ret, loc);
}
classIteratorOf(t: IrType): ClassIteratorInfo | null {
return classIteratorOf(this, t);
}
classIteratorOpenCall(cit: ClassIteratorInfo, recv: IrExpr, loc: SrcLoc): IrExpr {
return classIteratorOpenCall(this, cit, recv, loc);
}
classIteratorNextCall(cit: ClassIteratorInfo, itRef: IrExpr, loc: SrcLoc): IrExpr {
return classIteratorNextCall(this, cit, itRef, loc);
}
classIteratorDrainCall(src: IrExpr, loc: SrcLoc, elemT?: IrType): IrExpr | null {
return classIteratorDrainCall(this, src, loc, elemT);
}
classIteratorRestDrainCall(cit: ClassIteratorInfo, itVal: IrExpr, loc: SrcLoc): IrExpr {
return classIteratorRestDrainCall(this, cit, itVal, loc);
}
fieldGetExpr(target: FieldTarget, loc: SrcLoc, blame: ts.Node): IrExpr {
return fieldGetExpr(this, target, loc, blame);
}
fieldSetStmt(target: FieldTarget, value: IrExpr, loc: SrcLoc, blame: ts.Node): IrStmt {
return fieldSetStmt(this, target, value, loc, blame);
}
lowerFieldCompound(access: ts.PropertyAccessExpression | ts.ElementAccessExpression,
op: CompoundOp,
rhsNode: ts.Expression | null,
loc: SrcLoc,): IrStmt {
return lowerFieldCompound(this, access, op, rhsNode, loc);
}
errorConstructorArgs(args: readonly ts.Expression[], loc: SrcLoc, blame: ts.Node): IrExpr[] {
return errorConstructorArgs(this, args, loc, blame);
}
inheritsBuiltinErrorCtor(info: ClassInfo): boolean {
return inheritsBuiltinErrorCtor(this, info);
}
inheritsBuiltinEmitterCtor(info: ClassInfo): boolean {
return inheritsBuiltinEmitterCtor(this, info);
}
lowerNew(expr: ts.NewExpression): IrExpr {
// `new Uint8Array(handle)` (and the other typed-array ctors) over an
// ISLAND argument: the construction is an ENGINE operation — the
// engine's own constructor over the engine's own value (an Emscripten
// factory's `wasmBinary: new Uint8Array(buf)` where buf came off an
// island readFileSync) — and the instance stays a handle. The static
// bytes ctor cannot claim it (bytes never cross the boundary), so
// this preempts only when the single argument is island-typed;
// every static form keeps the standard path.
if (
this.dynamic &&
ts.isIdentifier(expr.expression) &&
expr.arguments?.length === 1 &&
!ts.isSpreadElement(expr.arguments[0]!) &&
/^(Uint8|Uint8Clamped|Int8|Uint16|Int16|Uint32|Int32|Float32|Float64|BigInt64|BigUint64)Array$/.test(
expr.expression.text,
) &&
this.isStdlibSymbol(this.resolveValueSymbol(expr.expression) ?? undefined) &&
this.isIslandExpr(expr.arguments[0]!)
) {
const loc = locOf(expr);
const ctor: IrExpr = {
kind: "jsOp", op: "globalGet", name: expr.expression.text, args: [], type: JSVAL, loc,
};
const arg = this.lowerExpr(expr.arguments[0]!);
return { kind: "jsOp", op: "construct", args: [ctor, arg], type: JSVAL, loc };
}
const builtin = lowerAbortControllerNew(this, expr) ?? lowerResponseNew(this, expr) ?? lowerStaticReadableStreamNew(this, expr);
return builtin ?? this.withRuntimeOptionalClassValue(expr.expression, () => lowerNew(this, expr));
}
lowerFieldRead(expr: ts.PropertyAccessExpression): IrExpr | null {
// `C.x` static reads first (the receiver is a CLASS, not an instance
// — the instance field path below could never claim it), then static
// access through class VALUES (classval-typed bindings).
return lowerStaticFieldRead(this, expr) ?? lowerClassValueProperty(this, expr) ?? lowerFieldRead(this, expr);
}
lowerUnionProperty(expr: ts.PropertyAccessExpression): IrExpr | null {
return lowerUnionProperty(this, expr);
}
lowerRecordFieldCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerRecordFieldCall(this, call, access);
}
lowerObjectMethodCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerObjectMethodCall(this, call, access);
}
lowerSuperMethodCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr {
return lowerSuperMethodCall(this, call, access);
}
superThisRef(access: ts.PropertyAccessExpression): { thisRef: IrExpr; base: ClassInfo } {
return superThisRef(this, access);
}
lowerSuperAccessorRead(access: ts.PropertyAccessExpression): IrExpr {
return lowerSuperAccessorRead(this, access);
}
lowerSuperAccessorWrite(access: ts.PropertyAccessExpression,
rhs: ts.Expression,
loc: SrcLoc,): IrStmt {
return lowerSuperAccessorWrite(this, access, rhs, loc);
}
/* ── comptime (compile-time evaluation) ───────────────────────────── */
lowerComptime(expr: ts.CallExpression): IrExpr {
return lowerComptime(this, expr);
}
comptimeBakeable(t: IrType): boolean {
return comptimeBakeable(this, t);
}
rejectComptimeCaptures(cb: ts.ArrowFunction | ts.FunctionExpression): void {
return rejectComptimeCaptures(this, cb);
}
comptimeValueToIr(value: unknown,
expected: IrType,
path: string,
blame: ts.Node,): IrExpr {
return comptimeValueToIr(this, value, expected, path, blame);
}
isConsoleLog(call: ts.CallExpression): boolean {
return isConsoleLog(this, call);
}
consoleCallMember(call: ts.CallExpression): "log" | "info" | "debug" | "error" | "warn" | null {
return consoleCallMember(this, call);
}
/* ── standard library (process + node:fs) ─────────────────────────── */
builtinImportOf(ident: ts.Identifier): { module: string; member: string } | null {
return builtinImportOf(this, ident);
}
/** The namespace-import twin of builtinImportOf's provenance rule:
* resolves an expression to the supported builtin MODULE whose members
* it exposes — an identifier declared by `import * as ns from "node:fs"`
* (through the symbol, so shadowing locals never match), or a nested
* member access that IS a module in its own right (`fs.promises` — the
* same object as node:fs/promises, Node's rule). Null otherwise. */
builtinNamespaceModuleOf(expr: ts.Expression): string | null {
const stored = ts.isIdentifier(expr)
? (this.peekLocal(expr)?.type ?? this.globalOf(expr)?.type)
: undefined;
const mapped = stored ?? this.mapTypeOf(this.typeOf(expr));
if (mapped?.kind === "moduleNs" && mapped.moduleId.startsWith("builtin:")) {
return mapped.moduleId.slice("builtin:".length);
}
if (ts.isIdentifier(expr)) {
const symbol = this.checker.getSymbolAtLocation(expr);
const decl = symbol ? this.checker.declarationsOf(symbol)[0] : undefined;
if (!decl) return null;
// The CommonJS twin: `const fs = require("fs")` binds the same
// namespace surface as `import * as fs from "node:fs"` — and the
// createRequire spelling (`const fs = require("node:fs")` through a
// createRequire binding, the fallback-typed cast idiom stripped)
// binds it too.
if (ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name) && decl.initializer) {
const spec = requireSpecOf(decl.initializer);
if (spec !== null) return canonicalBuiltinModule(spec);
const init = stripTypeCasts(decl.initializer);
if (ts.isCallExpression(init)) {
const cr = createRequireSpecOf(this, init);
if (cr !== null && cr.spec !== null) return canonicalBuiltinModule(cr.spec);
}
return null;
}
// A DESTRUCTURED sub-namespace binding — `const { promises } =
// fs` / `= require('fs')`: the member is itself a supported module
// ("fs/promises"), so the binding carries that module's namespace
// surface. canonicalBuiltinModule gates the composition (an
// ordinary destructured FUNCTION binding composes to an unknown
// name and answers null — builtinImportOf owns those).
if (ts.isBindingElement(decl) && ts.isObjectBindingPattern(decl.parent) &&
ts.isVariableDeclaration(decl.parent.parent) && decl.parent.parent.initializer !== undefined &&
decl.propertyName === undefined && decl.initializer === undefined) {
const name = decl.name;
if (name === undefined || !ts.isIdentifier(name)) return null;
const init = decl.parent.parent.initializer;
const spec = requireSpecOf(init);
const outer = spec !== null
? canonicalBuiltinModule(spec)
: ts.isIdentifier(init) ? this.builtinNamespaceModuleOf(init) : null;
if (outer !== null) return canonicalBuiltinModule(`${outer}/${name.text}`);
return null;
}
}
// The INLINE CommonJS spelling: `require("cluster").isPrimary` — the
// call expression IS the module namespace (Node evaluates the member
// off the module object; a supported module's members key the same
// tables as any namespace binding).
{
const spec = requireSpecOf(expr);
if (spec !== null) return canonicalBuiltinModule(spec);
}
// The createRequire spelling of the same inline form:
// `require("node:path").join(...)` through a createRequire binding
// (the fallback-typed cast idiom strips: `(require("x") as T).m`).
{
const inner = stripTypeCasts(expr);
if (ts.isCallExpression(inner)) {
const cr = createRequireSpecOf(this, inner);
if (cr !== null && cr.spec !== null) return canonicalBuiltinModule(cr.spec);
}
}
if (ts.isIdentifier(expr)) {
const symbol = this.checker.getSymbolAtLocation(expr);
const decl = symbol ? this.checker.declarationsOf(symbol)[0] : undefined;
if (!decl) return null;
// The DEFAULT-import twin: Node's default export of a CJS builtin
// IS the module object, so `import path from "node:path"` exposes
// exactly the namespace form's member surface for EVERY supported
// builtin (preflight admits the spelling in JS sources, plus the
// callable module objects — assert, events, test — everywhere).
if (ts.isImportClause(decl) && decl.name) {
const importDecl = decl.parent;
if (ts.isImportDeclaration(importDecl) && ts.isStringLiteral(importDecl.moduleSpecifier)) {
return canonicalBuiltinModule(importDecl.moduleSpecifier.text);
}
}
if (!ts.isNamespaceImport(decl)) return null;
const importDecl = decl.parent.parent;
if (!ts.isImportDeclaration(importDecl) || !ts.isStringLiteral(importDecl.moduleSpecifier)) return null;
return canonicalBuiltinModule(importDecl.moduleSpecifier.text);
}
if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken && ts.isIdentifier(expr.expression)) {
const outer = this.builtinNamespaceModuleOf(expr.expression);
if (outer === null) return null;
return canonicalBuiltinModule(`${outer}/${expr.name.text}`);
}
return null;
}
/** A member access on a supported builtin namespace import —
* `fs.readFileSync`, `path.sep`, `fs.promises.readFile` — as the same
* { module, member } shape builtinImportOf gives named imports, so both
* import forms key the same lowering tables. Null for everything else. */
builtinMemberOf(access: ts.PropertyAccessExpression): { module: string; member: string } | null {
if (access.questionDotToken) return null;
const module = this.builtinNamespaceModuleOf(access.expression);
if (module !== null) return { module, member: access.name.text };
// The RE-EXPORT FACADE's namespace spelling: `import * as assert from
// "./facade.js"` over `export { ok } from "node:assert"` (the formatter idiom's
// universal/assert). The member's symbol is the facade's
// ExportSpecifier, and builtinImportOf's alias chase answers the
// builtin's own module/member — the same tables as a direct import;
// ordinary property symbols are not aliases and never match.
return ts.isIdentifier(access.name) ? builtinImportOf(this, access.name) : null;
}
/** `ns.member(...)` on a builtin namespace import: exactly the named-
* import dispatch — the module tables for lowered members, the module-
* qualified per-member fence for the rest. Null for non-namespace
* callees (the call chain keeps trying). */
lowerNamespaceBuiltinCall(call: ts.CallExpression, access: ts.PropertyAccessExpression): IrExpr | null {
const bi = this.builtinMemberOf(access);
if (!bi) return null;
// child_process.execFile's callback forms are special-cased rather
// than table-backed, but namespace imports share the named-import
// implementation and runtime callback adapter.
if (bi.module === "child_process" && bi.member === "execFile") {
return this.lowerExecFileCall(call, locOf(access));
}
// The timers spoke: `timers.setTimeout(...)` through a namespace or
// require binding IS the global (Node's timers module re-exports
// them) — the shared member lowering serves both spellings.
if (bi.module === "timers") {
const timersServed = lowerTimersMemberCall(this, call, bi.member, locOf(access));
if (timersServed) return timersServed;
}
// The assert spoke owns node:assert wholesale (every call shape is
// special-cased — optional messages, per-type comparisons, synthesized
// deep-equality helpers).
const assertServed = this.lowerAssertModuleCall(call, bi, locOf(access));
if (assertServed) return assertServed;
// The node:test spoke owns its module the same way (`test.skip(...)`
// through the default import is a namespace-member call here).
const testServed = this.lowerNodeTestModuleCall(call, bi, locOf(access));
if (testServed) return testServed;
// The util spoke owns inspect/format the same way (per-type
// synthesized traversal helpers, compile-time format strings).
const utilServed = this.lowerUtilModuleCall(call, bi, locOf(access));
if (utilServed) return utilServed;
// The dgram/dns spoke owns those modules for namespace imports too
// (`import * as dns from "node:dns"` — portless's form): every call
// shape is special-cased there, so it never rides the param tables.
const dgramServed = this.lowerDgramDnsModuleCall(call, bi, locOf(access));
if (dgramServed) return dgramServed;
// The server-surface spoke owns net and http wholesale — the same
// dispatch the named-import path takes (`net.createServer(...)` via
// `import * as net` is portless's own spelling).
const served = this.lowerNetModuleCall(call, bi, locOf(access));
if (served) return served;
// The stream spoke owns finished/pipeline the same way.
const streamServed = lowerStreamModuleCall(this, call, bi, locOf(access));
if (streamServed) return streamServed;
// fs._toUnixTimestamp — off the param tables (an underscore-stable
// internal), served by its own spoke before the table fence.
const fsTs = this.lowerFsToUnixTimestampCall(call, bi, locOf(access));
if (fsTs) return fsTs;
// The fs validation-ladder spoke (checked-dynamic lane): misuse of
// implemented-namespace members throws Node's typed errors instead
// of meeting the table fence.
const fsLadder = this.lowerFsLadderCall(call, bi, locOf(access));
if (fsLadder) return fsLadder;
// The crypto introspection statics (getFips and the name lists) bake
// at the call site — no runtime entry exists to table.
const cryptoServed = this.lowerCryptoModuleCall(call, bi, locOf(access));
if (cryptoServed) return cryptoServed;
const nodeModuleServed = lowerNodeModuleCall(this, call, bi, locOf(access));
if (nodeModuleServed) return nodeModuleServed;
const builtinFn = builtinModuleFnOf(this, bi.module, bi.member);
if (!builtinFn) {
this.noLowering(
`${bi.module}.${bi.member}`,
call,
builtinFenceHintOf(bi.module, bi.member),
this.checker.getSymbolAtLocation(access.name),
);
}
return this.lowerBuiltinModuleCall(call, bi, builtinFn, locOf(access));
}
/** `ns.member` on a builtin namespace import as a VALUE: constants
* (path.sep, os.EOL) read as interned string literals; fs.constants
* access-mode bits bake as numbers exactly like the named-import form;
* functions have no closure representation (call sites only); members
* with no lowering fence with the module-qualified name. Null for
* non-namespace receivers (the property chain keeps trying). */
lowerNamespaceBuiltinProperty(expr: ts.PropertyAccessExpression): IrExpr | null {
const loc = locOf(expr);
// fs.constants.X_OK through the namespace: the same four POSIX
// access-mode bits the named-import form bakes (lowerFsConstantsProperty).
if (!expr.questionDotToken && ts.isPropertyAccessExpression(expr.expression)) {
const inner = this.builtinMemberOf(expr.expression);
if (inner && inner.module === "fs" && inner.member === "constants") {
const MODES: Record<string, number | undefined> = { F_OK: 0, X_OK: 1, W_OK: 2, R_OK: 4 };
const value = own(MODES, expr.name.text);
if (value === undefined) {
this.noLowering(
`fs.constants.${expr.name.text}`,
expr,
"F_OK, R_OK, W_OK, and X_OK are the lowered constants",
);
}
return { kind: "numLit", value, type: F64, loc };
}
}
const bi = this.builtinMemberOf(expr);
if (!bi) return null;
// events.defaultMaxListeners READS the process-wide default (its
// write twin routes through emitter.setDefaultMaxChk).
if (bi.module === "events" && bi.member === "defaultMaxListeners") {
return { kind: "libCall", fn: "emitter.getDefaultMax", args: [], type: F64, loc };
}
const c = builtinModuleConstOf(this, bi.module, bi.member);
if (c !== undefined) return builtinConstLit(c, loc);
// module.builtinModules through a namespace binding — the baked
// Node v24 list, a fresh string[] per read.
if (bi.module === "module" && bi.member === "builtinModules") {
return builtinModulesArrayLit(loc);
}
if (bi.module === "http" && bi.member === "METHODS") {
return { kind: "libCall", fn: "http.methods", args: [], type: arrayOf(STRING), loc };
}
if (bi.module === "http" && bi.member === "STATUS_CODES") {
return { kind: "libCall", fn: "http.statusCodes", args: [], type: DYN, loc };
}
// tls.rootCertificates through the namespace: the same runtime-valued
// constant the named-import read lowers to.
{
const roots = lowerTlsRootCertificates(this, bi, loc);
if (roots) return roots;
}
// TypeScript builtin values admitted by an explicit surface-table
// contract materialize as interned closures. JavaScript sources retain
// their established builtin identity-token/fence policy.
if (!isJsSourceFile(expr.getSourceFile())) {
const callable = this.lowerBuiltinCallableValue(bi, loc);
if (callable) return callable;
}
if (bi.member === "constants" && bi.module === "fs") {
// A bare `fs.constants` read (not one of the baked bits above).
this.noLowering(`fs.constants`, expr, "F_OK, R_OK, W_OK, and X_OK are the lowered constants");
}
if (builtinModuleFnOf(this, bi.module, bi.member)) {
this.unsupported(
"SC1090",
expr,
`library functions as values (call '${expr.getText()}' directly)`,
);
}
this.noLowering(
`${bi.module}.${bi.member}`,
expr,
builtinFenceHintOf(bi.module, bi.member),
this.checker.getSymbolAtLocation(expr.name),
);
}
/** Materialize one explicitly value-callable table entry as an interned
* synthetic module function. Direct calls never use this path: they keep
* their existing validated libCall lowering. The value descriptor is
* deliberately narrower than table membership because many table rows
* are only dispatch sentinels for call-site-specific lowering. */
lowerBuiltinCallableValue(
bi: { module: string; member: string },
loc: SrcLoc,
): IrExpr | null {
const fn = builtinModuleFnOf(this, bi.module, bi.member);
const valueParams = fn?.valueParams;
if (!fn || !valueParams) return null;
const shapes: ParamShape[] = valueParams.map((param): ParamShape => {
if (param.mode === "rest") return { mode: "rest", type: arrayOf(param.type) };
if (param.mode === "optional") {
return { mode: "omittable", type: this.withUndefinedArm(param.type), bodyType: param.type };
}
return { mode: "required", type: param.type };
});
const funcType = funcTypeFromParamShapes(shapes, fn.result);
const adapterShape = valueParams
.map((param) => param.mode === "optional" ? `${param.mode}:${param.defaultValue}` : param.mode)
.join(",");
const key = `${fn.fn}:${typeKey(funcType)}:${adapterShape}`;
let fnName = this.builtinCallableValueFns.get(key);
if (!fnName) {
fnName = `%builtin.value.${fn.fn}.${this.builtinCallableValueFns.size}`;
this.builtinCallableValueFns.set(key, fnName);
const params = funcType.params.map((type, index) => ({
localId: `arg.${index}`,
name: `arg${index}`,
type,
}));
const args: IrExpr[] = params.map((param, index) => {
const valueParam = valueParams[index];
if (!valueParam) {
throw new InternalCompilerError(
`builtin callable value '${bi.module}.${bi.member}' has a missing value parameter`,
);
}
const ref: IrExpr = {
kind: "varRef",
localId: param.localId,
type: param.type,
loc,
};
if (valueParam.mode !== "optional") return ref;
if (valueParam.type.kind !== "string" || param.type.kind !== "union") {
throw new InternalCompilerError(
`builtin callable value '${bi.module}.${bi.member}' has an unsupported optional default`,
);
}
const undefTag = this.armTag(param.type.unionId, UNDEFINED_T);
const valueTag = this.armTag(param.type.unionId, valueParam.type);
if (undefTag < 0 || valueTag < 0) {
throw new InternalCompilerError(
`builtin callable value '${bi.module}.${bi.member}' has an invalid optional ABI`,
);
}
return {
kind: "ternary",
cond: {
kind: "unionIsTag",
unionId: param.type.unionId,
tag: undefTag,
negated: false,
value: ref,
type: BOOL,
loc,
},
then: { kind: "strLit", value: valueParam.defaultValue, type: STRING, loc },
else_: {
kind: "unionNarrow",
unionId: param.type.unionId,
tag: valueTag,
value: ref,
type: valueParam.type,
loc,
},
type: valueParam.type,
loc,
};
});
const call: IrExpr = { kind: "libCall", fn: fn.fn, args, type: fn.result, loc };
const body: IrStmt[] = fn.result.kind === "void"
? [
{ kind: "exprStmt", expr: call, loc },
{ kind: "return", value: null, loc },
]
: [{ kind: "return", value: call, loc }];
this.liftedFns.push({
name: fnName,
params,
returnType: fn.result,
locals: params.map((param) => ({
id: param.localId,
name: param.name,
type: param.type,
mutable: false,
})),
body,
loc,
});
}
return { kind: "closure", fnName, captures: [], type: funcType, loc };
}
isBuiltinCallableValue(expr: IrExpr): boolean {
return expr.kind === "closure" &&
[...this.builtinCallableValueFns.values()].some((name) => name === expr.fnName);
}
lowerBuiltinModuleCall(expr: ts.CallExpression,
bi: { module: string; member: string },
fn: BuiltinModuleFn,
loc: SrcLoc,): IrExpr {
return lowerBuiltinModuleCall(this, expr, bi, fn, loc);
}
lowerTimersPromisesSetInterval(
expr: ts.CallExpression,
bi: { module: string; member: string },
loc: SrcLoc,
): IrExpr | null {
return lowerTimersPromisesSetInterval(this, expr, bi, loc);
}
lowerFsToUnixTimestampCall(expr: ts.CallExpression,
bi: { module: string; member: string },
loc: SrcLoc,): IrExpr | null {
return lowerFsToUnixTimestampCall(this, expr, bi, loc);
}
lowerFsLadderCall(expr: ts.CallExpression,
bi: { module: string; member: string },
loc: SrcLoc,): IrExpr | null {
return lowerFsLadderCall(this, expr, bi, loc);
}
lowerChildArgsArg(node: ts.Expression | undefined, loc: SrcLoc): IrExpr {
return lowerChildArgsArg(this, node, loc);
}
lowerSpawnSyncCall(expr: ts.CallExpression, loc: SrcLoc): IrExpr {
return lowerSpawnSyncCall(this, expr, loc);
}
lowerSpawnCall(expr: ts.CallExpression, loc: SrcLoc): IrExpr {
return lowerSpawnCall(this, expr, loc);
}
lowerExecFileCall(expr: ts.CallExpression, loc: SrcLoc): IrExpr {
return lowerExecFileCall(this, expr, loc);
}
lowerExecSyncCall(expr: ts.CallExpression, shell: boolean, loc: SrcLoc): IrExpr {
return lowerExecSyncCall(this, expr, shell, loc);
}
recordToEnvPairs(node: ts.Expression): IrExpr {
return recordToEnvPairs(this, node);
}
envToPairsHelper(shapeId: string, loc: SrcLoc): string | null {
return lowerEnvToPairsHelper(this, shapeId, loc);
}
lowerJsonMethodCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerJsonMethodCall(this, call, access);
}
fencedBuiltinImportOf(ident: ts.Identifier): string | null {
return fencedBuiltinImportOf(this, ident);
}
lowerCryptoComposedCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerCryptoComposedCall(this, call, access);
}
lowerUrlMethodCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerUrlMethodCall(this, call, access);
}
lowerSearchParamsMethodCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerSearchParamsMethodCall(this, call, access);
}
lowerStatsMethodCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerStatsMethodCall(this, call, access);
}
lowerChildMethodCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerChildMethodCall(this, call, access);
}
lowerAtomicsCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerAtomicsCall(this, call, access);
}
// The server-surface spoke (lower-server.ts): net module calls, the
// netServer/netSocket method surface, and the composed address().port.
lowerNetModuleCall(expr: ts.CallExpression,
bi: { module: string; member: string },
loc: SrcLoc,): IrExpr | null {
return lowerNetModuleCall(this, expr, bi, loc);
}
lowerServerMethodCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerServerMethodCall(this, call, access);
}
lowerServerProperty(expr: ts.PropertyAccessExpression): IrExpr | null {
return lowerServerProperty(this, expr);
}
// The dgram/dns spoke (lower-dgram.ts): dgram/dns module calls and the
// dgramSocket method surface.
lowerAssertModuleCall(expr: ts.CallExpression,
bi: { module: string; member: string },
loc: SrcLoc,): IrExpr | null {
return lowerAssertModuleCall(this, expr, bi, loc);
}
lowerAssertDirectCall(expr: ts.CallExpression, loc: SrcLoc): IrExpr | null {
return lowerAssertDirectCall(this, expr, loc);
}
// The util spoke (lower-inspect.ts): inspect/format/formatWithOptions.
lowerUtilModuleCall(expr: ts.CallExpression,
bi: { module: string; member: string },
loc: SrcLoc,): IrExpr | null {
return lowerUtilModuleCall(this, expr, bi, loc);
}
lowerDgramDnsModuleCall(expr: ts.CallExpression,
bi: { module: string; member: string },
loc: SrcLoc,): IrExpr | null {
return lowerDgramDnsModuleCall(this, expr, bi, loc);
}
lowerDgramMethodCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerDgramMethodCall(this, call, access);
}
// The node:test spoke (lower-test.ts): registrations, suites, hooks,
// and the TestContext surface.
lowerNodeTestModuleCall(expr: ts.CallExpression,
bi: { module: string; member: string },
loc: SrcLoc,): IrExpr | null {
return lowerNodeTestModuleCall(this, expr, bi, loc);
}
lowerTestDirectCall(expr: ts.CallExpression, loc: SrcLoc): IrExpr | null {
return lowerTestDirectCall(this, expr, loc);
}
lowerTestMethodCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerTestMethodCall(this, call, access);
}
lowerTestCtxProperty(expr: ts.PropertyAccessExpression): IrExpr | null {
return lowerTestCtxProperty(this, expr);
}
lowerHttpHeadersElement(expr: ts.ElementAccessExpression): IrExpr | null {
return lowerHttpHeadersElement(this, expr);
}
lowerJsonProperty(expr: ts.PropertyAccessExpression): IrExpr | null {
return lowerJsonProperty(this, expr);
}
lowerErrorCodeProperty(expr: ts.PropertyAccessExpression): IrExpr | null {
return lowerErrorCodeProperty(this, expr);
}
lowerStringDecoderMethodCall(call: ts.CallExpression, access: ts.PropertyAccessExpression): IrExpr | null {
return lowerStringDecoderMethodCall(this, call, access);
}
lowerReadlineMethodCall(call: ts.CallExpression, access: ts.PropertyAccessExpression): IrExpr | null {
return lowerReadlineMethodCall(this, call, access);
}
lowerDcChannelMethodCall(call: ts.CallExpression, access: ts.PropertyAccessExpression): IrExpr | null {
return lowerDcChannelMethodCall(this, call, access);
}
lowerAlsMethodCall(call: ts.CallExpression, access: ts.PropertyAccessExpression): IrExpr | null {
return lowerAlsMethodCall(this, call, access);
}
lowerDcChannelProperty(access: ts.PropertyAccessExpression): IrExpr | null {
return lowerDcChannelProperty(this, access);
}
lowerDcTracingChannelMethodCall(call: ts.CallExpression, access: ts.PropertyAccessExpression): IrExpr | null {
return lowerDcTracingChannelMethodCall(this, call, access);
}
lowerDcTracingChannelProperty(access: ts.PropertyAccessExpression): IrExpr | null {
return lowerDcTracingChannelProperty(this, access);
}
strdecHelper(op: "write" | "end", shapeId: string, loc: SrcLoc): string {
return strdecHelper(this, op, shapeId, loc);
}
lowerProcessProperty(expr: ts.PropertyAccessExpression): IrExpr | null {
return lowerProcessProperty(this, expr);
}
lowerFsConstantsProperty(expr: ts.PropertyAccessExpression): IrExpr | null {
return lowerFsConstantsProperty(this, expr);
}
lowerBuiltinConstantsProperty(expr: ts.PropertyAccessExpression): IrExpr | null {
return lowerBuiltinConstantsProperty(this, expr);
}
builtinConstantBindingOf(ident: ts.Identifier): IrExpr | null {
return builtinConstantBindingOf(this, ident);
}
builtinConstantsDestructureDecl(nameNode: ts.Node, init: ts.Expression | undefined): boolean {
return builtinConstantsDestructureDecl(this, nameNode, init);
}
lowerCryptoModuleCall(expr: ts.CallExpression, bi: { module: string; member: string }, loc: SrcLoc): IrExpr | null {
return lowerCryptoModuleCall(this, expr, bi, loc);
}
lowerProcessStreamProperty(expr: ts.PropertyAccessExpression): IrExpr | null {
return lowerProcessStreamProperty(this, expr);
}
isProcessEnv(node: ts.Expression): boolean {
return isProcessEnv(this, node);
}
envValueType(): IrType {
return envValueType(this);
}
lowerProcessEnvGet(expr: ts.PropertyAccessExpression): IrExpr | null {
return lowerProcessEnvGet(this, expr);
}
lowerProcessMethodCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerProcessMethodCall(this, call, access);
}
lowerProcessOptionalMethodCall(call: ts.CallExpression): IrExpr | null {
return lowerProcessOptionalMethodCall(this, call);
}
lowerTimeoutMethodCall(call: ts.CallExpression, access: ts.PropertyAccessExpression): IrExpr | null {
return lowerTimeoutMethodCall(this, call, access);
}
registerPromisifiedBuiltinDecl(nameNode: ts.Node, init: ts.Expression | undefined): boolean {
return registerPromisifiedBuiltinDecl(this, nameNode, init);
}
lowerExecFileAsyncCall(expr: ts.CallExpression, loc: SrcLoc): IrExpr {
return lowerExecFileAsyncCall(this, expr, loc);
}
execFileAsyncHelper(loc: SrcLoc): { name: string; shapeId: string } {
return execFileAsyncHelper(this, loc);
}
envSnapshotHelper(shapeId: string, loc: SrcLoc): string | null {
return envSnapshotHelper(this, shapeId, loc);
}
lowerNumberStaticCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerNumberStaticCall(this, call, access);
}
lowerDateCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerDateCall(this, call, access);
}
lowerTextCodecCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerTextCodecCall(this, call, access);
}
lowerStringStaticCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerStringStaticCall(this, call, access);
}
lowerStringLastIndexOfCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerStringLastIndexOfCall(this, call, access);
}
lowerFilterNarrowCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
// Native array filters use the state-aware HOF helper: its callback
// receives present undefined values and skips holes before a narrowed
// survivor is extracted. The older narrowing helper reads the payload
// directly and therefore cannot represent that three-state contract.
if (this.mapTypeOf(this.typeOf(access.expression))?.kind === "array" && this.isStdlibMember(access)) {
return null;
}
return lowerFilterNarrowCall(this, call, access);
}
lowerPromiseMethodCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerPromiseMethodCall(this, call, access);
}
lowerPromiseStaticCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerPromiseStaticCall(this, call, access);
}
lowerNumberStaticProperty(expr: ts.PropertyAccessExpression): IrExpr | null {
return lowerNumberStaticProperty(this, expr);
}
/* ── the island-backed ambient surface (ISLAND_SURFACE) ───────────── */
requireDynamicApi(feature: string, node: ts.Node): void {
return requireDynamicApi(this, feature, node);
}
lowerMathProperty(expr: ts.PropertyAccessExpression): IrExpr | null {
return lowerMathProperty(this, expr);
}
islandGlobalFnOf(ident: ts.Identifier): IslandFnEntry | null {
return islandGlobalFnOf(this, ident);
}
lowerIslandMethodCall(call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
return lowerIslandMethodCall(this, call, access);
}
}