import { InternalCompilerError } from "../../errors.js"; /* Class lowering: shape collection over the single-inheritance graph * (fields, methods, accessors, overrides), constructor/member lowering with * synthesized derived ctors and field initializers, super calls and super * accessor access, upcasts, `new` expressions, and the builtin Error * hierarchy registration. */ import * as ts from "../ts7/adapter.js"; import type { FnCtx, Lowerer } from "./lowerer.js"; import { BOOL, DATE_T, DYN, F64, bytesOf, type IrClassDef, type IrExpr, type IrFunction, type IrLocal, type IrParam, type IrStmt, type IrType, JSVAL, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, STRING, type SrcLoc, UNDEFINED_T, VOID, arrayOf, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, typeEquals } from "../../ir/ir.js"; import { MAX_GENERIC_INSTANCES, appendImplicitUndefinedReturn, bodyReadsArguments, generatorMeta, genericCallInstance, implicitAnyParamSymbolsOf, implicitCallInstance, implicitMonoFile, omittedArgFor, type GenericFnInfo, type ParamShape } from "./lower-calls.js"; import { isGenericCallableMemberType, jsOpenObjectType, typeKey, withUnitArm } from "../type-mapper.js"; import { cjsClassExprWholeExportOf, isCjsJsFile, isJsSourceFile, isModuleExportsAccess, isNodeTypesPath, locOf } from "../program.js"; import { PoisonError, dynFallbackType, dynUndefinedExpr, newFnCtx, own } from "./lowerer.js"; import { lowerArrayConstructor, lowerMapSeedNew, lowerSetSeedNew } from "./lower-containers.js"; import { bufEncoding } from "./containers/bytes.js"; import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; import { lowerSearchParamsNew, lowerTextCodecNew } from "./lower-builtins.js"; import { requiresDynamicPackageDiag, unsupportedDiag, type ScrDiagnostic } from "../../diagnostics/diagnostic.js"; import { STREAM_API_MEMBERS, STREAM_PROP_MEMBERS, UNDERSCORE_METHODS, lowerStreamNew, lowerStreamSuperCall, streamCtorShape } from "./lower-stream.js"; import { emitOverrideShapeReason, emitSpecSuperForward, emitterRooted, lowerEmitterSuperCall, type EmitOverrideRec } from "./lower-event-emitter.js"; import { declSymbolOf } from "./lower-modules.js"; import { classSymbolKeyOf, classSymbolKeyOfSymbol, symbolFieldDisplayName } from "./symbol-fields.js"; import { builtinFenceHintOf } from "./surfaces.js"; import { lowerHttpAgentNew, lowerHttpServerNew } from "./lower-server.js"; import { ambientNsRootOf, ambientUndefReadType, ambientUndefVarRootOf, ambientUndefinedFnSymbolOf, fenceEarlyAliasUse, fenceEarlyNsMemberRef, nsMemberIdentOf, nsUndefRead } from "./lower-namespaces.js"; import { mixinResultBindingClassOf, type MixinInstanceInfo } from "./lower-mixins.js"; import { hasLexicalPrivateReference, hasStaticThis, rejectStaticThis } from "./static-this.js"; import { lowerUrlNew } from "./lower-url.js"; import { isNativeProxyInitializer, lowerNativeProxy } from "./expressions/native-proxy.js"; import { classStaticDataFor } from "./class-static-data.js"; import { lowerObjectFactoryNew } from "./object-factory-new.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { lowerInstanceConstructorNew } from "./class-instance-constructor.js"; import { classPrototypeData } from "./class-prototypes.js"; import { checkedClassConstruction } from "./class-construction.js"; import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js"; export function returnsOnlyThis(member: ts.MethodDeclaration): boolean { const body = member.body; const last = body?.statements.at(-1); if (!body || !last || !ts.isReturnStatement(last)) return false; let valid = true; const visit = (node: ts.Node): void => { if (ts.isFunctionLike(node)) return; if (ts.isReturnStatement(node)) { let value = node.expression; while (value && ts.isParenthesizedExpression(value)) value = value.expression; if (!value || value.kind !== ts.SyntaxKind.ThisKeyword) valid = false; } node.forEachChild(visit); }; body.forEachChild(visit); return valid; } function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean { for (let parent: ts.Node | undefined = decl.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) { if (ts.isFunctionLike(parent)) return true; } return false; } export function storedClassValueType(lowerer: Lowerer, expression: ts.Expression): IrType | null { if (ts.isPropertyAccessExpression(expression) && !expression.questionDotToken) { const member = nsMemberIdentOf(lowerer, expression); if (member) return storedClassValueType(lowerer, member); } if (expression.kind === ts.SyntaxKind.ThisKeyword) { const stored = lowerer.resolveThis(); if (stored?.type.kind === "classval") return stored.type; } if (ts.isIdentifier(expression)) { const stored = lowerer.peekLocal(expression) ?? lowerer.globalOf(expression); if (stored?.type.kind === "classval") return stored.type; } const type = lowerer.typeOf(expression); const mapped = lowerer.mapTypeOf(type); if (mapped?.kind === "classval" || !ts.isCallExpression(expression)) return mapped; // A checked call can erase its returned constructor's ABI while the // checker still identifies the class declaration. Collect that class in // its factory's lexical scope before choosing the derived native layout. // Cache the module-level closure so its captures and lifted bodies are // exactly the ones used when the declaration executes. for (const signature of lowerer.checker.getConstructSignatures(type)) { const declaration = lowerer.checker.signatureDeclaration(signature); const cls = declaration && ts.isConstructorDeclaration(declaration) ? declaration.parent : null; if (!cls || (!ts.isClassExpression(cls) && !ts.isClassDeclaration(cls))) continue; let factory: ts.ArrowFunction | ts.FunctionExpression | undefined; for (let parent: ts.Node | undefined = cls.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) { if (ts.isArrowFunction(parent) || ts.isFunctionExpression(parent)) factory = parent; else if (ts.isFunctionLike(parent)) { factory = undefined; break; } } if (!factory || !isJsSourceFile(factory.getSourceFile()) || factory.typeParameters?.length || !ts.isVariableDeclaration(factory.parent) || factory.parent.initializer !== factory || !ts.isVariableDeclarationList(factory.parent.parent) || !(factory.parent.parent.flags & ts.NodeFlags.Const) || !ts.isVariableStatement(factory.parent.parent.parent) || !ts.isSourceFile(factory.parent.parent.parent.parent) || lowerer.preparingClassFactories.has(factory)) continue; if (!lowerer.preparedClassFactories.has(factory)) { lowerer.preparingClassFactories.add(factory); const collecting = lowerer.collecting, sink = lowerer.diagSink; lowerer.collecting = false; lowerer.diagSink = null; try { lowerer.preparedClassFactories.set(factory, lowerer.lowerLambda(factory)); } finally { lowerer.collecting = collecting; lowerer.diagSink = sink; lowerer.preparingClassFactories.delete(factory); } } const resolved = lowerer.mapTypeOf(type); if (resolved?.kind === "classval") return resolved; } return mapped; } export interface ClassMethodSignature { params: ParamShape[]; ret: IrType; abstract?: true; async?: true; gen?: NonNullable; } export interface ClassInfo { def: IrClassDef; runtimeStatics?: readonly ts.ClassElement[]; runtimePrototypeMembers?: readonly ts.ClassElement[]; localPrototypeData?: true; prototypeMethodValues?: Map; prototypeReflectionReady?: true; prototypeReflectionRefusal?: string; localClass?: { context: FnCtx | null; bodies: Map; captureSources: string[]; ready: boolean; baseExpression?: ts.Expression; }; /** ALL fields visible on instances — the inherited ones included — for * receiver-side lookup (def.fields carries the layout order). */ fields: Map; /** OWN fields only (declaration order) with their initializers: the * class's constructor runs exactly these — inherited fields initialize in * the base constructor, before/via super(). */ fieldOrder: { name: string; type: IrType; initializer: ts.Expression | undefined; /** A bare JS dyn field defines undefined at its declaration position, * even if an earlier initializer or base constructor wrote the slot. */ undefinedInitializer?: SrcLoc; /** Redeclared field: initialize the existing slot at this position; * no new slot (def.fields excludes it). */ redeclared?: true; }[]; /** OWN declared methods only — inherited lookups walk the base chain * (findMethodOn). An `abstract` entry is a signature with no body (and * no module function): it declares the vtable slot; concrete subclasses * fill it (tsc guarantees every instantiable class implements). * #PRIVATE members key by their spelled name ('#m', "get:#x") — no * public identifier can collide, subclass redeclarations of an * inherited private name are fenced at collection, and tsc confines * every access site to the declaring class's body, so the base-chain * walk IS lexical resolution and privates never join vtables (JS's * no-dynamic-dispatch semantics by construction). A `gen` entry has a * generator body whose direct calls enter through its spawn wrapper. */ methods: Map; /** Own JS overrides whose signature cannot use the inherited ABI. The slot * retains that ABI, but its implementation throws before executing. */ methodEntryFences?: Map; /** OWN GENERIC instance methods (own type parameters — `m(x: T)`), * monomorphized per call site like top-level generic functions: instance * `n` is the module function `%C.m%n` taking `this` as param 0. They * never enter `methods` (no single ABI signature, no vtable slot), so * dispatch is STATIC — calls resolve the nearest declarer on the * receiver's static class, and a receiver whose runtime class could * override (genericOverrideBelow) must be exact or fences. Inherited * lookups walk the base chain (findGenericMethodOn). */ genericMethods?: Map; /** OWN GENERIC static methods — `%C.static:m%n` module functions, the * generic twin of staticMethods (same this/super fence, same * through-a-VALUE shadowing rules via staticShadowBelow). */ genericStatics?: Map; /** DEFERRED-INIT fields (inherited included, like `fields`): a * `stream!: T` definite-assignment assertion (or an SPI-off * initializer-less field) whose first assignment happens past the * constructor's top level. The SLOT is the undefined-armed union — * allocation writes the interned undefined, exactly Node's * pre-assignment read — writes wrap into the arm, and every READ is a * CHECKED extraction back to the declared type: a genuinely * unassigned read throws the catchable TypeError instead of yielding * an undefined the declared type cannot hold (SEMANTICS.md). */ deferredInitFields?: Set; /** null for the builtin error classes (runtime-provided; no source). * Class EXPRESSIONS carry their ts.ClassExpression here — members, * accessors, and locs read identically off either form. */ decl: ts.ClassLikeDeclaration | null; /** Runtime-provided builtin (the Error hierarchy): no bodies lower, `new` * and super() calls become error.* libCalls, toString is the runtime's. */ builtinError?: true; /** Runtime-provided node:events EventEmitter: no bodies lower, `new` and * super() become emitter.* libCalls, and the whole method surface * (on/emit/...) lowers through lower-event-emitter.ts over any class rooted * here. Subclass structs embed the ScrEmitter prefix. */ builtinEmitter?: true; /** Runtime-provided node:stream class (Readable/Writable/Duplex/ * Transform/PassThrough — emitter-rooted): no bodies lower, `new` * becomes a stream constructor libCall, the stream method/property * surface lowers through lower-stream.ts, and the emitter surface rides * the base chain. The value names which SIDES the class carries. User * `extends` of these classes is fenced at the declaration (phase 1). */ builtinStream?: "r" | "w" | "rw"; /** This class's own `emit` override in the FORWARDING SHAPE (the one * EventEmitter member a subclass may re-declare): never in `methods` — * emit calls keep routing through the emitter spoke, which lowers the * body once per event name as the specialization method `emit:` * (lower-event-emitter.ts's emit-overrides block has the whole story). */ emitOverride?: EmitOverrideRec; ctor: ts.ConstructorDeclaration | null; /** PARAMETER PROPERTIES (`constructor(public x: number)`), in parameter * order: each declares a field (placed BEFORE the class's declared * fields in the layout — Node's transform hoists the definitions to the * top of the class body, verified) and assigns it from the parameter's * body local AFTER the field initializers run (Node's order: super() → * field initializers → parameter-property assignments → ctor body). */ paramProps?: { name: string; type: IrType; param: ts.ParameterDeclaration }[]; /** EFFECTIVE constructor params: the own constructor's, or (constructor * omitted) the base's — `new Derived(...)` is typed by tsc against the * inherited signature, and the synthesized constructor forwards to it * (forwarding the completed ABI values; defaults apply in the base). */ ctorParams: ParamShape[]; base: ClassInfo | null; /** A module class may inherit a compiled ordinary JS constructor. Its * callable and prototype are captured when the class declaration runs. */ callableBase?: { expression: ts.Expression; constructorId: string; prototypeId: string }; /** DIRECT subclasses, filled as derived classes collect — the frontend's * side of whole-program devirtualization (overrideBelow). */ subclasses: ClassInfo[]; /** Property names whose setter this class SYNTHESIZES as a throw: a * getter-only override shadows an inherited get/set pair in JS, so a * base-typed write reaches this class and throws TypeError (Node's * behavior, matched exactly — see collectClassShape). */ throwingSetters: string[]; /** STATIC fields with initializers — the honest static subset: each is * a module global (`%g.s..`), assigned once in the declaring * file's %init at the class statement's source position (exactly when * JS evaluates static initializers, so an initializer reading earlier * module bindings sees their values), and read as `C.name` anywhere * (lowerStaticFieldRead). Writable (non-readonly) fields are MUTABLE * globals; writes lower only through the DECLARING class's own name * (`D.x = v` where x is inherited creates an OWN property on D in JS — * different storage — and writes through class VALUES would need the * same dynamic story: both are named fences). Accessors and * initializer-less fields keep the fence. */ staticFields: { name: string; type: IrType; initializer: ts.Expression; globalId: string; readonly: boolean }[]; /** Shared expando data added after a JS class declaration. */ staticDataHelper?: string; /** STATIC methods — ordinary module functions named `%C.static:m` (the * accessor-colon trick: no user identifier can spell it, and statics * never join vtables, so IrClassDef doesn't know them). `C.m(args)` is * a direct call; `const f = C.m` a zero-capture closure; calls through * class VALUES devirtualize when no strict descendant redeclares the * member. Receiver-dependent methods specialize for exact receivers; * detached methods and `super` retain their fences. Absent on builtins. */ staticMethods?: Map; /** `static { ... }` blocks, in declaration order. They are DECLARATION-TIME * CODE, not shape: JS runs each block once when the class statement * evaluates, whether or not anything ever references the class — so their * statements lower into the declaring file's %init at the class statement's * source position, interleaved with the static field initializers in member * order (lowerStaticFieldInits). `this` inside a block (the class * constructor value — no value form here) fences at collection. Absent on * builtin classes and classes without blocks. */ staticBlocks?: ts.ClassStaticBlockDeclaration[]; /** SYMBOL-KEYED fields with stable module-level Symbol()/Symbol.for() * keys: each identity resolves to an ordinary hidden slot in the static * layout. Registry keys share one identity across declarations. The map * goes key identity → reserved layout field name; inspect recovers the * Symbol(limit) spelling. Inherited entries are seeded from the base. * Absent on builtin classes and classes with no symbol-keyed fields. */ symbolFields?: Map; /** Stable symbol keys for computed instance methods. */ symbolMethods?: Map; /** GENERIC class FAMILY (`class Box` itself): the synthetic, * never-constructed ancestor every instantiation extends. It owns what * JS's one runtime `Box` owns — the statics (one storage location for * every instantiation) and the `instanceof Box` interval — and declares * no fields, no instance methods, no constructor function. Construction * and instance types resolve to instantiations instead (`generic` * carries the instance table). */ generic?: GenericClassInfo; /** GENERIC class INSTANTIATION (`Box%0` for `Box`): the family, * the type-parameter bindings member lowering runs under (the * generic-fn typeParamResolver mechanism), the rendered type arguments * for diagnostics, and the demand ordinal (only the FIRST instantiation * counts statements toward coverage — re-instantiations re-visit the * same source lines). */ genericInstance?: { family: ClassInfo; bindings: Map; typeArgsText: string; ordinal: number }; /** MIXIN instantiation (`%mx.` for `M(Base)` at one call * site): the call that minted it, the base-parameter type binding its * members lower under, the forwarding-constructor flag, and where its * static declaration-time code emits (lower-mixins.ts). */ mixinInstance?: MixinInstanceInfo; /** CLASS decorators (`@dec class C`) — standard (TC39 stage-3 / TS 5+) * semantics, lowered statically as declaration-time CALLS in %init at * the class statement's position: decorator expressions evaluate in * source order, applications run in REVERSE order over the class object, * and static field initializers/blocks run AFTER the applications (the * verified Node order). Present exactly when the declaration carries * class-level decorators; `shapes` fills in the post-collection analysis * pass (a decorator's return type may name a subclass declared BELOW the * class, so analysis cannot run while shapes are still collecting). */ classDecorators?: ClassDecorationInfo; /** The class's decoration PROVABLY throws before anything else in its * definition evaluates (the first effectful item in TC39 evaluation * order — class decorators, then heritage, then member decorators and * computed keys interleaved — is an AMBIENT decorator name nothing * defines; Node erases the declaration, so the read is a * ReferenceError). The class registers as an empty SHELL: no members * collect (nothing after the throw ever runs — member fences would be * fences on dead code), the %init at the class statement is exactly the * throw, and every VALUE use (new, the class as a value, extends) * fences — the binding never initializes, so compiled code can never * legitimately reach one. */ decorationThrows?: { name: string }; } /** A decorated class's decoration state (see ClassInfo.classDecorators). */ export interface ClassDecorationInfo { /** The class-level decorator nodes, source order. */ nodes: ts.Decorator[]; /** Per-decorator analysis (parallel to `nodes`). `call`: the decorator * expression's completed function type — the type its VALUE lowers to * and the ABI the application call dispatches — and whether it can * REPLACE the class (return type is the class or a subclass, per the * classval flow rule) rather than returning void/undefined. * `ambientThrow`: the decorator names an ambient declaration NOTHING * defines (`declare let dec: any`, `declare function dec(t: T): T`) * — Node erases it, so evaluating the decorator expression throws the * ReferenceError; the program compiles to exactly that crash. */ shapes?: ( | { kind: "call"; funcType: Extract; replaces: boolean } | { kind: "ambientThrow"; name: string } )[]; /** Analysis fenced — diagnostics already reported; emission skips. */ poisoned?: true; /** The MUTABLE classval module global holding the decoration RESULT, * present exactly when some decorator can replace the class. TC39 binds * the class NAME to the last non-undefined decorator return, so every * reference to the name routes through this value: bare reads load it, * `new C()` dispatches newValue through it, `C.x` takes the * through-a-VALUE static paths, and `instanceof C` reads its interval * (instanceOfValue). Absent when every decorator returns void/undefined * — the binding provably stays the original class object and every * direct path stays direct. */ valueGlobalId?: string; } /** A generic class declaration's monomorphization state, hung off the * FAMILY ClassInfo (registered under the class's own qualified name and * bound to its symbol — `new`, `instanceof`, statics, and extends all * resolve to the family first and reroute to instantiations from there). * Instances key by comma-joined type-argument typeKeys; `info` is null * WHILE the instance's shape collects (self-referential layouts — `next: * Box | null` — re-enter by key and take the name without recursing) * and stays null with `poisoned` set when collection fenced. */ export interface GenericClassInfo { decl: ts.ClassDeclaration; /** Unqualified source name, for diagnostics. */ baseName: string; /** Declaration-order type parameter symbols. */ typeParams: ts.Symbol[]; family: ClassInfo; instances: Map; } /** The KEY symbol behind a LATE-BOUND (`__@name@id`) property: resolved * from the argument of the element-access assignment that declared it * (`this[kLimit] = v` — the declaration list holds the BinaryExpression * or the ElementAccessExpression itself). Null when no declaration has * that shape (well-known-symbol members like `[Symbol.iterator]`). */ function lateBoundKeySymOf(lowerer: Lowerer, p: ts.Symbol): ts.Symbol | null { for (const d of lowerer.checker.declarationsOf(p)) { const access = ts.isBinaryExpression(d) && ts.isElementAccessExpression(d.left) ? d.left : ts.isElementAccessExpression(d) ? d : null; if (!access || !ts.isIdentifier(access.argumentExpression)) continue; const sym = lowerer.resolveValueSymbol(access.argumentExpression); if (sym) return sym; } return null; } /** Symbol-slot RETURN refinement (5.9.3 ABI parity): tsgo synthesizes no * late-bound property for a JS class's `this[k] = v` declaration (the * finding-5 family), so an unannotated method whose returns read a * declared symbol-keyed slot infers `any` — the checked-dynamic * fallback would box a value whose static type the class layout already * knows (5.9.3 inferred it through the late-bound property; runtime * output was identical either way, but the method ABI carried a dyn * box). Recovered here from the layout itself, under a shape that * cannot mis-type: an unannotated, non-async, non-generator JS method * whose LAST top-level statement is a return (no fall-through * `undefined` completion), where EVERY return statement (nested * functions excluded — they return elsewhere) returns `this[k]` with a * statically-resolved key declared in symbolFields, and all the slots * agree on one IR type. Null when the shape doesn't hold — the value * stays checked-dynamic exactly as before. */ function symbolSlotReturnType( lowerer: Lowerer, fnLike: ts.MethodDeclaration, symbolFields: ReadonlyMap, fields: ReadonlyMap, ): IrType | null { if (symbolFields.size === 0) return null; if (fnLike.type !== undefined || !fnLike.body) return null; if (!isJsSourceFile(fnLike.getSourceFile())) return null; if (fnLike.asteriskToken !== undefined) return null; if (fnLike.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null; const stmts = fnLike.body.statements; const last = stmts[stmts.length - 1]; if (!last || !ts.isReturnStatement(last)) return null; const returns: ts.ReturnStatement[] = []; const visit = (n: ts.Node): void => { if (ts.isFunctionLike(n)) return; if (ts.isReturnStatement(n)) returns.push(n); n.forEachChild(visit); }; fnLike.body.forEachChild(visit); let out: IrType | null = null; for (const r of returns) { let e = r.expression; while (e !== undefined && ts.isParenthesizedExpression(e)) e = e.expression; if (e === undefined || !ts.isElementAccessExpression(e)) return null; if (e.expression.kind !== ts.SyntaxKind.ThisKeyword) return null; const key = classSymbolKeyOf(lowerer, e.argumentExpression); const fieldName = key ? symbolFields.get(key.identity) : undefined; const t = fieldName !== undefined ? fields.get(fieldName) : undefined; if (t === undefined || t.kind === "dyn") return null; if (out !== null && !typeEquals(out, t)) return null; out = t; } return out; } /** The builtin Error hierarchy * as eagerly-registered ClassInfos: mapType names them the moment a lib * Error type appears, so the infos must exist before any lowering. They * are runtime-provided — no decl, no lowerable bodies; `new`/super()/ * toString reach them through dedicated error.* libCall lowerings, and * user classes extend them like any base (the emitted subclass struct * embeds ScrError's prefix). */ export function registerBuiltinErrorClasses(lowerer: Lowerer): void { const loc = { file: "", start: 0, end: 0 }; for (const [irName, rec] of RUNTIME_ERROR_CLASSES) { const base = rec.base ? (lowerer.classes.get(rec.base) ?? null) : null; const info: ClassInfo = { def: { name: irName, runtime: true, ...(rec.base ? { base: rec.base } : {}), // Layout only — `%code` is ScrError's third slot (NULL = absent; // fs/exec throw sites stamp it): subclass structs embed it in // their prefix, and teardown releases it NULL-guarded like any // string field. The '%' name keeps it out of user reach (a // subclass declaring its own `code` field lays out AFTER it, // never colliding), and it is NOT in the fields map below: the // READ has its own `string | undefined` lowering (error.code), // never a plain-string field access. `%cause` is the following // owned dyn slot; NULL means absent, dyn undefined means present. fields: [ { name: "name", type: STRING }, { name: "message", type: STRING }, { name: "%code", type: STRING }, { name: "%cause", type: DYN }, { name: "%causeEnumerable", type: BOOL }, { name: "%messagePresent", type: BOOL }, { name: "%messageEnumerable", type: BOOL }, { name: "%namePresent", type: BOOL }, { name: "%nameEnumerable", type: BOOL }, { name: "%stackFrames", type: STRING }, { name: "%stack", type: STRING }, ], loc, }, fields: new Map([ ["name", STRING], ["message", STRING], ]), fieldOrder: [], // Only the root declares toString — subclasses (builtin and user) // reach it through the base-chain walk, so its declarer is always // %Error and calls lower to the one runtime implementation. methods: rec.base === null ? new Map }>([["toString", { params: [], ret: STRING }]]) : new Map(), decl: null, builtinError: true, ctor: null, // Inherited constructors forward both raw arguments. The runtime // applies the undefined message default and InstallErrorCause. ctorParams: [{ type: DYN, mode: "omittable" }, { type: DYN, mode: "omittable" }], base, subclasses: [], throwingSetters: [], staticFields: [], }; if (base) base.subclasses.push(info); lowerer.classes.set(irName, info); } } /** The runtime-provided node:events EventEmitter as an eagerly-registered * ClassInfo (the error-hierarchy story): mapType names `%EventEmitter` * the moment an emitter type appears, so the info must exist before any * lowering. No decl, no lowerable bodies — `new`/super() reach it * through emitter.* libCalls, the method surface lowers through * lower-event-emitter.ts, and user classes extend it like any base (the * emitted subclass struct embeds ScrEmitter's registry/name prefix — * carried by the BACKEND, not by IR fields, so the fields list stays * empty and subclass field layout starts right after the prefix). */ export function registerBuiltinEmitterClass(lowerer: Lowerer): void { const loc = { file: "", start: 0, end: 0 }; const info: ClassInfo = { def: { name: RUNTIME_EMITTER_CLASS, runtime: true, fields: [], loc }, fields: new Map(), fieldOrder: [], methods: new Map(), decl: null, builtinEmitter: true, ctor: null, // `new EventEmitter()` — zero-argument (the options bag fences at // construction sites; the checker may admit it via @types/node). ctorParams: [], base: null, subclasses: [], throwingSetters: [], staticFields: [], }; lowerer.classes.set(RUNTIME_EMITTER_CLASS, info); } /** The runtime-provided node:stream classes as eagerly-registered * ClassInfos (the emitter story): mapType names `%Readable` et al the * moment a stream type appears, so the infos must exist before any * lowering. Each roots at the emitter through its base chain, so the * EventEmitter method surface, upcasts, and instanceof intervals apply * unchanged; the stream method/property surface lowers through * lower-stream.ts. No decl, no lowerable bodies, empty field lists — * every instance is runtime-allocated (user `extends` is fenced). */ export function registerBuiltinStreamClasses(lowerer: Lowerer): void { const loc = { file: "", start: 0, end: 0 }; for (const [irName, rec] of RUNTIME_STREAM_CLASSES) { const base = lowerer.classes.get(rec.base) ?? null; const info: ClassInfo = { def: { name: irName, runtime: true, base: rec.base, fields: [], loc }, fields: new Map(), fieldOrder: [], methods: new Map(), decl: null, builtinStream: rec.sides, ctor: null, // `new Readable(opts?)` — the options bag is parsed structurally // by the stream spoke (lowerNew never completes against this). ctorParams: [], base, subclasses: [], throwingSetters: [], staticFields: [], }; if (base) base.subclasses.push(info); lowerer.classes.set(irName, info); } } /** The stream ClassInfo a VALUE symbol refers to (`new Readable(...)`, * `x instanceof Writable`) — any import spelling resolves to the * ambient class. Provenance: a stdlib-file CLASS declaration inside the * "stream" ambient module, EXCLUDING @types/node's (whose stream.Readable * also types child stdio — under @types/node the childStream mapping * keeps priority and the static stream classes stand down; the shipped * fallback declarations are the supported surface). */ export function builtinStreamInfoOf(lowerer: Lowerer, symbol: ts.Symbol | null | undefined): ClassInfo | null { if (!symbol) return null; if (!lowerer.isStdlibSymbol(symbol)) { // A const ALIAS of a namespace member (`const Writable = // stream.Writable` — the two-step spelling; the one-step // require('stream').Writable rides the same walk): follow the // member to the stdlib class symbol. The declaration itself is // alias plumbing (streamClassAliasDecl — both declaration walks // skip it). const decl = lowerer.checker.valueDeclarationOf(symbol); if ( decl && ts.isVariableDeclaration(decl) && (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0 && decl.initializer !== undefined && ts.isPropertyAccessExpression(decl.initializer) && !decl.initializer.questionDotToken && lowerer.builtinNamespaceModuleOf(decl.initializer.expression) === "stream" ) { const mSym = lowerer.checker.getSymbolAtLocation(decl.initializer.name); const target = mSym && mSym.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(mSym) : mSym; if (target && target !== symbol) return builtinStreamInfoOf(lowerer, target); } return null; } let irName: string | null = null; for (const [name, rec] of RUNTIME_STREAM_CLASSES) { if (rec.lib === symbol.name) irName = name; } if (!irName) return null; const declared = lowerer.checker.declarationsOf(symbol).some((d) => { if (!ts.isClassDeclaration(d)) return false; if (isNodeTypesPath(d.getSourceFile().fileName)) return false; let node: ts.Node | undefined = d.parent; while (node) { if (ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) { return node.name.text === "stream" || node.name.text === "node:stream"; } node = node.parent; } return false; }); return declared ? (lowerer.classes.get(irName) ?? null) : null; } /** A dictionary lookup may be absent even when its checker type is a * required reference. Keep literal nullish fallbacks in inferred JS storage. */ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.Expression): IrType { let node = expression; while (ts.isParenthesizedExpression(node)) node = node.expression; if (ts.isFunctionExpression(node) || ts.isArrowFunction(node)) { const ret = lowerer.declaredReturnType(node, node); const arms = type.kind === "union" ? lowerer.unions.get(type.unionId)?.arms ?? [] : [type]; // Inferred arithmetic may return a checked Number or BigInt. Preserve // the callable when its native return ABI differs from the checker slot. if (arms.some((arm) => arm.kind === "func" && !typeEquals(arm.ret, ret))) return DYN; } if (node.kind === ts.SyntaxKind.NullKeyword) return withUnitArm(type, "nullT", lowerer.unions) ?? type; const inferred = lowerer.typeOf(node); for (const arm of inferred.isUnionType() ? ts.constituentTypes(inferred) : [inferred]) { if ((arm.flags & ts.TypeFlags.Undefined) !== 0) type = lowerer.withUndefinedArmOf(type) ?? type; if ((arm.flags & ts.TypeFlags.Null) !== 0) type = withUnitArm(type, "nullT", lowerer.unions) ?? type; } if (ts.isConditionalExpression(node)) { return jsFieldInitializerType(lowerer, jsFieldInitializerType(lowerer, type, node.whenTrue), node.whenFalse); } if (ts.isBinaryExpression(node)) { if (node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) type = jsFieldInitializerType(lowerer, type, node.left); if (node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || node.operatorToken.kind === ts.SyntaxKind.BarBarToken || node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) return jsFieldInitializerType(lowerer, type, node.right); } return type; } /** The undefined-armed union of a JS class property's inferred type — the * honest slot for a field first assigned outside the constructor's top * level (undefined until the write runs, Node-exact). Null when the * inference is unmappable or an arm-less kind that cannot join a union * (genResultRecord's list, including scalar-backed Date values). */ function undefArmedFieldType(lowerer: Lowerer, p: ts.Symbol): IrType | null { const t = lowerer.checker.getTypeOfSymbol(p); const site = lowerer.checker.declarationsOf(p)[0]; let mapped = lowerer.mapTypeOf(t) ?? (site ? dynFallbackType(lowerer, site, t) : null); if (mapped && site) mapped = jsOpenObjectType(site, mapped, lowerer.shapes, lowerer.unions); if (!mapped || mapped.kind === "void") return null; for (const declaration of lowerer.checker.declarationsOf(p)) { const assignment = ts.isBinaryExpression(declaration) ? declaration : declaration.parent; if (assignment && ts.isBinaryExpression(assignment) && assignment.operatorToken.kind === ts.SyntaxKind.EqualsToken) { mapped = jsFieldInitializerType(lowerer, mapped, assignment.right); } } // An implicit JS value already carries undefined in its native box. if (mapped.kind === "dyn") return mapped; const byKey = new Map(); const arms = mapped.kind === "union" ? (lowerer.unions.get(mapped.unionId)?.arms ?? []) : [mapped]; for (const a of arms) { if ( a.kind === "map" || a.kind === "regex" || a.kind === "date" || a.kind === "jsval" || a.kind === "generator" ) { return null; } byKey.set(typeKey(a), a); } byKey.set(typeKey(UNDEFINED_T), UNDEFINED_T); const sorted = [...byKey.values()].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); return { kind: "union", unionId: lowerer.unions.intern(sorted) }; } /** The emitter ClassInfo a VALUE symbol refers to (`new EventEmitter`, * `extends EventEmitter`, `x instanceof EventEmitter`) — any import * spelling (named/default/namespace member, CJS require) resolves to * the ambient class. Provenance-checked like the error classes: only a * stdlib-file declaration inside the "events" ambient module counts. */ export function builtinEmitterInfoOf(lowerer: Lowerer, symbol: ts.Symbol | null | undefined): ClassInfo | null { if (!symbol) return null; if (!lowerer.isStdlibSymbol(symbol)) { // A const ALIAS of the emitter class member (`const EventEmitter = // require('node:events').EventEmitter` — commander's spelling; the // two-step `const EE = events.EventEmitter` rides the same walk): // follow the member off the module namespace. The declaration // itself is alias plumbing (builtinMemberRequireDecl — both // declaration walks skip it). const decl = lowerer.checker.valueDeclarationOf(symbol); if ( decl !== undefined && ts.isVariableDeclaration(decl) && (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0 && decl.initializer !== undefined && ts.isPropertyAccessExpression(decl.initializer) && !decl.initializer.questionDotToken && decl.initializer.name.text === "EventEmitter" && lowerer.builtinNamespaceModuleOf(decl.initializer.expression) === "events" ) { return lowerer.classes.get(RUNTIME_EMITTER_CLASS) ?? null; } return null; } if (symbol.name !== "EventEmitter") return null; const declared = lowerer.checker.declarationsOf(symbol).some((d) => { if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false; let node: ts.Node | undefined = d.parent; while (node) { if (ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) { return node.name.text === "events" || node.name.text === "node:events"; } node = node.parent; } return false; }); return declared ? (lowerer.classes.get(RUNTIME_EMITTER_CLASS) ?? null) : null; } /** The builtin error ClassInfo a VALUE symbol refers to (`new Error`, * `extends TypeError`, `x instanceof RangeError`), or null. Provenance- * checked: only the standard library's declarations count — a user's own * `class Error` resolves through classBySymbol instead. */ export function builtinErrorInfoOf(lowerer: Lowerer, symbol: ts.Symbol | null | undefined): ClassInfo | null { if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; for (const [irName, rec] of RUNTIME_ERROR_CLASSES) { if (rec.lib === symbol.name) return lowerer.classes.get(irName) ?? null; } return null; } /** The instance-method surface the runtime EventEmitter owns — subclass * members with these names are fenced (collectClassShapeInner) and calls * to them on emitter-rooted receivers lower through lower-event-emitter.ts. */ export const EMITTER_API_MEMBERS: ReadonlySet = new Set([ "on", "addListener", "once", "prependListener", "prependOnceListener", "off", "removeListener", "removeAllListeners", "emit", "listenerCount", "listeners", "rawListeners", "eventNames", "setMaxListeners", "getMaxListeners", ]); /** The decorators of a class-like or member node (they live in * `modifiers` since TS 4.8). */ export function decoratorNodesOf(n: ts.Node): ts.Decorator[] { return (((n as { modifiers?: readonly ts.Node[] }).modifiers ?? []) as ts.Node[]).filter( (m): m is ts.Decorator => m.kind === ts.SyntaxKind.Decorator, ); } /** The AMBIENT name a decorator expression's evaluation throws on, or * null. Node erases ambient declarations (`declare let dec: any`, * `declare const instance: T`, `declare function dec(t: T): T`), so * reading the name is a ReferenceError. Factory spellings ride along — * `@dec(...)` evaluates the CALLEE before any argument — and property * chains throw at their ROOT (`@instance.decorate` reads `instance` * first). */ function ambientDecoratorThrowNameOf(lowerer: Lowerer, dExpr: ts.Expression): string | null { let e: ts.Expression = dExpr; while (ts.isParenthesizedExpression(e)) e = e.expression; const target = ts.isCallExpression(e) ? e.expression : e; const root = ambientUndefVarRootOf(lowerer, target); if (root) return root.text; let callee: ts.Expression = target; while (ts.isParenthesizedExpression(callee)) callee = callee.expression; if (ts.isIdentifier(callee) && ambientUndefinedFnSymbolOf(lowerer, callee) !== null) return callee.text; return null; } /** The guaranteed decoration THROW of a decorated class, or null. Walks * the class definition's evaluation-order items — class decorators * (source order), the heritage expression, then per member in body * order its decorators and computed key (the verified TC39/tsc-downlevel * order) — and answers the first AMBIENT decorator name, provided every * item BEFORE it is provably effect-free and non-throwing: bare * identifier decorators over defined values (a pure read), an absent / * `null` / bare-identifier heritage, literal or bare-identifier * computed keys. Anything richer (factory calls over defined values, * property-access reads, computed-key calls) stops the proof — the * named fences answer instead. */ export function guaranteedDecorationThrow(lowerer: Lowerer, decl: ts.ClassLikeDeclaration,): { name: string; node: ts.Decorator } | null { const stripParens = (e: ts.Expression): ts.Expression => { let x = e; while (ts.isParenthesizedExpression(x)) x = x.expression; return x; }; const decoratorVerdict = (d: ts.Decorator): { name: string; node: ts.Decorator } | "effectFree" | "opaque" => { const name = ambientDecoratorThrowNameOf(lowerer, d.expression); if (name !== null) return { name, node: d }; const e = stripParens(d.expression); // A bare identifier over a DEFINED value: a pure read. if (ts.isIdentifier(e)) return "effectFree"; return "opaque"; }; for (const d of decoratorNodesOf(decl)) { const v = decoratorVerdict(d); if (v === "opaque") return null; if (v !== "effectFree") return v; } const heritage = decl.heritageClauses ?.find((c) => c.token === ts.SyntaxKind.ExtendsKeyword) ?.types[0]; if (heritage) { const h = stripParens(heritage.expression); if (h.kind !== ts.SyntaxKind.NullKeyword && !ts.isIdentifier(h)) return null; } for (const member of decl.members) { for (const d of decoratorNodesOf(member)) { const v = decoratorVerdict(d); if (v === "opaque") return null; if (v !== "effectFree") return v; } const name = (member as { name?: ts.PropertyName }).name; if (name && ts.isComputedPropertyName(name)) { const k = stripParens(name.expression); const literalKey = ts.isStringLiteralLike(k) || ts.isNumericLiteral(k) || ts.isIdentifier(k); if (!literalKey) return null; } } return null; } export function collectClassShape(lowerer: Lowerer, decl: ts.ClassDeclaration): void { const symbol = lowerer.collectDeferring( () => declSymbolOf(lowerer, decl), () => lowerer.collectClassShapeInner(decl), ); // Typed receivers and module retention know the class only by its // qualified IR name — index the deferral under it too. if (symbol) lowerer.deferredClassByName.set(lowerer.classNamer(decl), symbol); // A poisoned class containing a static BLOCK or a DECORATOR must report // EAGERLY: deferral's premise ("an unreached broken declaration costs // nothing") fails here — Node runs the block (and calls the decorator) // when the class statement evaluates, referenced or not, so silently // dropping the declaration would drop observable side effects (the // classStaticBlock13/28 miscompiles). const hasDeclTimeCode = (n: ts.Node): boolean => ts.isClassStaticBlockDeclaration(n) || ((n as { modifiers?: readonly ts.Node[] }).modifiers ?? []).some( (m) => m.kind === ts.SyntaxKind.Decorator, ); if (symbol && (hasDeclTimeCode(decl) || decl.members.some(hasDeclTimeCode))) { const diags = lowerer.deferredDiags.get(symbol); if (diags) { lowerer.deferredDiags.delete(symbol); if (!lowerer.alreadyFlushed.has(symbol)) { lowerer.flushedSymbols.add(symbol); for (const d of diags) lowerer.pushDiag(d); } } } // A poisoned BASE this class EXTENDS must report EAGERLY for the same // reason: the derived statement evaluates its heritage when module // init reaches it (these are top-level declarations), and the base's // fence is the COMPILER's, not Node's — Node defines the base fine and // runs on — so a deferred trap there is a manufactured divergence, not // the Node-parity deferral is licensed by (classFieldSuperAccessibleJs2: // the binary refused at `class D extends C` where Node prints five // lines). Leaf poisoned classes stay deferred — only the extends edge // reports. Resolution runs under the collect pass's guard (this is // collectProgram), so the lookup neither flushes nor fences. { const baseIdent = decl.heritageClauses ?.find((h) => h.token === ts.SyntaxKind.ExtendsKeyword) ?.types.map((t) => t.expression) .filter(ts.isIdentifier)[0]; const baseSym = baseIdent ? lowerer.resolveValueSymbol(baseIdent) : null; const baseDiags = baseSym ? lowerer.deferredDiags.get(baseSym) : undefined; if (baseSym && baseDiags) { lowerer.deferredDiags.delete(baseSym); if (!lowerer.alreadyFlushed.has(baseSym)) { lowerer.flushedSymbols.add(baseSym); for (const d of baseDiags) lowerer.pushDiag(d); } } } } function inferredEmptyCollectionFieldType( lowerer: Lowerer, decl: ts.ClassLikeDeclaration, field: string, initializer: ts.Expression, ): IrType | null { if (!ts.isNewExpression(initializer) || (initializer.arguments?.length ?? 0) !== 0 || !ts.isIdentifier(initializer.expression)) return null; const collection = initializer.expression.text; if (collection !== "Map" && collection !== "Set") return null; const symbol = lowerer.checker.getSymbolAtLocation(initializer.expression); if (!lowerer.isStdlibSymbol(symbol)) return null; const mapKeys: IrType[] = []; const mapValues: IrType[] = []; const setElements: IrType[] = []; const mappedArgument = (argument: ts.Expression): IrType | null => { const type = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(argument)); const mapped = lowerer.mapTypeOf(type); if (!mapped || mapped.kind === "dyn" || mapped.kind === "jsval" || mapped.kind === "void" || isUnitType(mapped)) return null; return mapped; }; const visit = (node: ts.Node): "skip" | undefined => { if (ts.isFunctionLike(node) && !ts.isArrowFunction(node)) return "skip"; if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return undefined; const receiver = node.expression.expression; if ( !ts.isPropertyAccessExpression(receiver) || receiver.expression.kind !== ts.SyntaxKind.ThisKeyword || receiver.name.text !== field ) { return undefined; } if (collection === "Map" && node.expression.name.text === "set" && node.arguments.length === 2) { const key = mappedArgument(node.arguments[0]!); const value = mappedArgument(node.arguments[1]!); if (key !== null && value !== null && isSupportedMapKey(key, key.kind === "union" ? lowerer.unions.get(key.unionId)?.arms : undefined) && isSupportedMapValue(value)) { mapKeys.push(key); mapValues.push(value); } } if ( collection === "Set" && (node.expression.name.text === "add" || node.expression.name.text === "has" || node.expression.name.text === "delete") && node.arguments.length === 1 ) { const element = mappedArgument(node.arguments[0]!); if (element !== null && isSupportedSetElem(element, element.kind === "union" ? lowerer.unions.get(element.unionId)?.arms : undefined)) setElements.push(element); } return undefined; }; for (const member of decl.members) { const body = ts.isConstructorDeclaration(member) || ts.isMethodDeclaration(member) || ts.isAccessor(member) ? member.body : undefined; if (body !== undefined) ts.walkPreorder(body, visit); } const unique = (types: readonly IrType[]): IrType | null => { const first = types[0]; return first !== undefined && types.every((type) => typeEquals(type, first)) ? first : null; }; if (collection === "Map") { const key = unique(mapKeys); const value = unique(mapValues); return key !== null && value !== null ? { kind: "map", key, value } : null; } const element = unique(setElements); return element !== null ? { kind: "set", elem: element } : null; } export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDeclaration, jsNameOverride?: string, inst?: { family: ClassInfo; name: string; bindings: Map; typeArgsText: string; ordinal: number }, /** MIXIN instantiation mode (lower-mixins.ts): the class inside a * mixin function, collected per call site — `base` is the ARGUMENT * class (the heritage clause names the mixin's parameter and is * resolved here, never through the loop below), `name` the * position-derived instance name. */ mixin?: { base: ClassInfo; name: string; call: ts.CallExpression; bindings: Map; context: string; ordinal: number },): void { { // Anonymous class EXPRESSIONS are ordinary (their .name follows // NamedEvaluation — jsNameOverride carries it). The one legal // nameless class DECLARATION is `export default class {}` — its // symbol is the module's default export (declSymbolOf) and it // registers under classNamer's "%anon" spelling (unique per file). if (!decl.name && ts.isClassDeclaration(decl) && declSymbolOf(lowerer, decl) === undefined) { lowerer.unsupported("SC1090", decl, "anonymous classes"); } // Decorators are declaration-time CALLS (they run when the class // statement evaluates and may replace the declaration outright). // CLASS decorators lower statically (collected here, analyzed // post-collection, emitted in %init at the class statement's // position — see ClassDecorationInfo). A decoration that PROVABLY // throws before anything else evaluates (an ambient decorator name, // the corpus's dominant shape — class-level or MEMBER-level) makes // the whole declaration a shell whose %init is exactly the throw. // Remaining MEMBER decorators stay named fences — a method/field // replacement would have to rebind vtable slots and initializer // chains at declaration time, and the standard context object // (addInitializer, access) has no static story yet. Parameter // decorators are not valid ES decorators — the checker rejects // them first. const classDecoratorNodes: ts.Decorator[] = []; { classDecoratorNodes.push(...decoratorNodesOf(decl)); const decoratedMembers = decl.members.filter((m) => decoratorNodesOf(m).length > 0); if (classDecoratorNodes.length > 0 || decoratedMembers.length > 0) { // Node itself cannot execute decorator syntax in a JavaScript // source (V8 has not shipped the proposal; the type-stripping // loaders leave `@dec` in place) — there is no runtime behavior // to be exact against. if (isJsSourceFile(decl.getSourceFile())) { lowerer.unsupported( "SC1090", (classDecoratorNodes[0] ?? decoratorNodesOf(decoratedMembers[0]!)[0])!, "decorators in JavaScript sources (V8 has not shipped decorators — Node cannot execute this file)", ); } // The guaranteed-throw SHELL: declarations only (expressions // lower their throw at the expression — lowerClassExpression), // never instantiations/mixins (they share a family declaration). if ( inst === undefined && mixin === undefined && ts.isClassDeclaration(decl) && decl.typeParameters === undefined ) { const thrown = guaranteedDecorationThrow(lowerer, decl); if (thrown) { const className = lowerer.classNamer(decl); const info: ClassInfo = { def: { name: className, jsName: jsNameOverride ?? decl.name?.text ?? "", fields: [], loc: locOf(decl), }, fields: new Map(), fieldOrder: [], methods: new Map(), decl, ctor: null, ctorParams: [], base: null, subclasses: [], throwingSetters: [], staticFields: [], decorationThrows: { name: thrown.name }, // The existing ambientThrow emission (lowerClassDecoration) // owns the %init: earlier expressions are all pure reads, // so the throw is the first observable effect. classDecorators: { nodes: [thrown.node], shapes: [{ kind: "ambientThrow", name: thrown.name }], }, }; lowerer.classes.set(className, info); const classSymbol = decl.name ? lowerer.checker.getSymbolAtLocation(decl.name) : declSymbolOf(lowerer, decl); if (classSymbol) lowerer.classBySymbol.set(classSymbol, info); return; } } for (const member of decoratedMembers) { const dec = decoratorNodesOf(member)[0]!; const kind = ts.isMethodDeclaration(member) ? "method decorators" : ts.isGetAccessorDeclaration(member) || ts.isSetAccessorDeclaration(member) ? "accessor decorators" : ts.isPropertyDeclaration(member) ? (member.modifiers?.some((m) => m.kind === ts.SyntaxKind.AccessorKeyword) ? "auto-accessor decorators" : "field decorators") : "member decorators"; lowerer.unsupported( "SC1090", dec, `${kind} (the standard context object and member replacement have no static lowering — class decorators and provably-throwing ambient decorations compile)`, ); } } if (classDecoratorNodes.length > 0) { // Each evaluation of a class EXPRESSION decorates a freshly // minted class; only once-evaluated declarations have a single // decoration event to lower. if (!ts.isClassDeclaration(decl)) { lowerer.unsupported( "SC1090", classDecoratorNodes[0]!, "decorators on class expressions (each evaluation decorates a distinct class)", ); } // A generic class declares ONCE in JS (one decoration event over // the one runtime Box) but compiles per instantiation here — the // family object is never constructed and the instantiations were // never individually decorated. if (decl.typeParameters !== undefined) { lowerer.unsupported( "SC1090", classDecoratorNodes[0]!, "decorators on generic classes (JS decorates the one runtime class; the compiled family instantiates per type argument)", ); } } } // An abstract class is a class nothing constructs directly — tsc // rejects `new` on it (through class values too), so no runtime // trap exists to lower. It collects like any class; only the flag // is recorded (abstract MEMBERS are per-member, below). const abstractClass = ts.getModifiers(decl)?.some((m) => m.kind === ts.SyntaxKind.AbstractKeyword) === true; // A GENERIC class declaration collects as its FAMILY (statics + the // instanceof interval; no instance members — those collect per // instantiation, `inst` set). Generic class EXPRESSIONS stay fenced: // each evaluation mints a distinct class in JS, and a distinct // FAMILY of classes has no once-evaluated story. const familyMode = decl.typeParameters !== undefined && inst === undefined; if (familyMode && !ts.isClassDeclaration(decl)) { lowerer.unsupported("SC1090", decl, "generic class expressions"); } const className = inst ? inst.name : mixin ? mixin.name : lowerer.classNamer(decl); // program-wide qualified name // Single inheritance: `extends` of a class declared in the program. // tsc guarantees the base is declared before the derived class (its // use-before-declaration error), and collection runs in module order, // so the base's ClassInfo already exists here. An INSTANTIATION's // base is its family (whose base is the declared one) — the heritage // clause resolved when the family collected. const computedBase = lowerer.computedClassBases.get(lowerer.classNamer(decl)); let base: ClassInfo | null = inst ? inst.family : mixin ? mixin.base : computedBase?.classInfo ?? null; let callableBase: ClassInfo["callableBase"]; const adoptCallableBase = (expression: ts.Expression): boolean => { if (familyMode || inst || mixin) return false; if (lowerer.computedCallableBases.has(decl)) { callableBase = { expression, constructorId: `%g.${className}.base`, prototypeId: `%g.${className}.basePrototype` }; return true; } const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(expression)); const source = signatures.length === 1 ? lowerer.checker.signatureDeclaration(signatures[0]!) : undefined; if (!source || !isJsSourceFile(source.getSourceFile()) || !(ts.isFunctionDeclaration(source) || ts.isFunctionExpression(source))) return false; callableBase = { expression, constructorId: `%g.${className}.base`, prototypeId: `%g.${className}.basePrototype` }; return true; }; // A family whose `extends` clause mentions its OWN type parameters // (`class D extends Box`) would need a different base per // instantiation — no single family interval can sit above all of // them. Named fence at the declaration. if (familyMode && decl.heritageClauses !== undefined) { const tpSyms = new Set(); for (const tp of decl.typeParameters!) { const s = lowerer.checker.getSymbolAtLocation(tp.name); if (s) tpSyms.add(s); } for (const clause of decl.heritageClauses) { if (clause.token !== ts.SyntaxKind.ExtendsKeyword) continue; for (const t of clause.types) { let mentions = false; ts.walkPreorder(t, (n) => { const s = ts.isIdentifier(n) ? lowerer.checker.getSymbolAtLocation(n) : undefined; if (s && tpSyms.has(s)) { mentions = true; return "stop"; } return undefined; }); if (mentions) { lowerer.unsupported( "SC1090", t, "generic classes whose 'extends' clause mentions their own type parameters (each instantiation would need a different base)", ); } } } } for (const clause of inst || mixin || computedBase ? [] : (decl.heritageClauses ?? [])) { // `implements` is pure type-world: tsc checked the conformance and // the clause erases — nothing about the runtime class changes. // (Assigning an instance INTO an interface-typed slot is a separate // question, owned by the shape-coercion fences at those sites.) if (clause.token === ts.SyntaxKind.ImplementsKeyword) continue; const t = clause.types[0]; // `extends events.EventEmitter` — the namespace-member spelling of // the ambient emitter base resolves like the named import. if (t && ts.isPropertyAccessExpression(t.expression) && ts.isIdentifier(t.expression.name)) { const memberSym = lowerer.checker.getSymbolAtLocation(t.expression.name); const resolved = memberSym && memberSym.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(memberSym) : memberSym; const globalError = !t.expression.questionDotToken && lowerer.isStdlibGlobal(t.expression.expression, "globalThis") ? lowerer.builtinErrorInfoOf(resolved) : null; if (globalError && globalError.def.name !== "%DOMException") { if (t.typeArguments) lowerer.unsupported("SC1090", t, "extending generic classes"); base = globalError; continue; } const emitterBase = lowerer.builtinEmitterInfoOf(resolved); const streamBaseNs = builtinStreamInfoOf(lowerer, resolved); if (emitterBase || streamBaseNs) { if (t.typeArguments) lowerer.unsupported("SC1090", t, "extending generic classes"); base = (emitterBase ?? streamBaseNs)!; continue; } // `class Tower extends Shapes.Cube` — the namespace-qualified // base: the member resolves to the registered program class // (import= alias chains included), with the source-order guard // (the class statement evaluates at its init position; a base // block below it would still be uninitialized in Node). if (!t.expression.questionDotToken && nsMemberIdentOf(lowerer, t.expression)) { if (t.typeArguments) lowerer.unsupported("SC1090", t, "extending generic classes"); if (memberSym) fenceEarlyNsMemberRef(lowerer, t.expression, memberSym); const stored = storedClassValueType(lowerer, t.expression); const nsBase = (resolved ? lowerer.classBySymbol.get(resolved) : undefined) ?? (stored?.kind === "classval" ? lowerer.classes.get(stored.className) : undefined); if (!nsBase) { if (adoptCallableBase(t.expression)) continue; lowerer.unsupported( "SC1090", t, `extending the namespace member '${t.expression.name.text}' (no class lowering)`, ); } base = nsBase; continue; } // `Common.O = class extends Common.I {}` — the base is a // PROPERTY-ASSIGNED class expression (the salsa expando form and // its CJS spellings `exports.I` / `module.exports.I`): the // member's single top-level assignment pins the class, so the // base resolves like a declaration. Source order guards the // same-file case — Node evaluates the extends clause at THIS // statement, and a base assigned below it is still undefined // here (TypeError at runtime; the fence is the honest answer). // Reassigned properties never reach this branch (the resolver // answers null for them) and keep the computed-expression fence: // the runtime base is whichever assignment ran last. if (!t.expression.questionDotToken) { const propBase = propertyAssignedClassInfoOf(lowerer, memberSym); if (propBase) { if (t.typeArguments) lowerer.unsupported("SC1090", t, "extending generic classes"); const baseDecl = propBase.decl; if ( baseDecl != null && baseDecl.getSourceFile() === decl.getSourceFile() && baseDecl.getStart() > decl.getStart() ) { lowerer.unsupported( "SC1090", t, `extending '${t.expression.getText()}' above the statement that assigns it (the property is still undefined when this class evaluates — Node throws here; assign the base first)`, ); } base = propBase; continue; } // The REASSIGNED spelling of the same family gets its own // fence (the generic computed-expression one below would hide // what actually blocks it). const rebinds = memberSym !== undefined && lowerer.checker .declarationsOf(memberSym) .filter( (d) => ts.isBinaryExpression(d) && d.operatorToken.kind === ts.SyntaxKind.EqualsToken, ).length > 1; if (rebinds) { lowerer.unsupported( "SC1090", t, `extending the reassigned property '${t.expression.getText()}' (the runtime base is whichever assignment ran last — bind the class exactly once)`, ); } } } // `class extends class {…} {…}` — a class-EXPRESSION base: // collect it recursively (JS evaluates the extends clause first, // so its statics queue ahead of the derived class's — the // recursion order delivers exactly that). if (t && ts.isClassExpression(t.expression)) { if (t.typeArguments) lowerer.unsupported("SC1090", t, "extending generic classes"); const baseExpr = lowerer.lowerClassExpressionInfo(t.expression); base = baseExpr; continue; } // `class D extends Mixin(Base)` — a MIXIN call as the base: the // call's per-site instantiation (its heritage the argument class) // is the base — interval nesting, fields, and methods compose // through the monomorphized chain (lower-mixins.ts). A call whose // callee is NOT a mixin function keeps the computed-expression // fence below. if (t && ts.isCallExpression(t.expression) && !t.typeArguments) { const mixinBase = lowerer.mixinCallClassInfoOf(t.expression); if (mixinBase) { base = mixinBase; continue; } } if (!t || !ts.isIdentifier(t.expression)) { if (t && adoptCallableBase(t.expression)) continue; lowerer.unsupported("SC1090", clause, "extending computed expressions"); } const symbol = lowerer.resolveValueSymbol(t.expression); // Extending a REBINDABLE decorated class (analysis already ran — // this collection is a class expression or a generic // instantiation demanded during lowering): the runtime base is // the decoration result, not the declaration. Declared // subclasses collected BEFORE analysis meet the same fence from // analyzeClassDecoration's subclasses check. { const directBase = symbol && lowerer.classBySymbol.get(symbol); if (directBase && directBase.classDecorators?.valueGlobalId !== undefined) { lowerer.unsupported( "SC1090", t, `extending the decorated class '${directBase.def.jsName ?? directBase.def.name}' (its decorators may replace it — the runtime base would be the decoration result)`, ); } if (directBase) fenceDecorationThrows(lowerer, directBase, t); } // `extends DOMException`: the runtime instance carries hidden // slots (the legacy code, the cause) BEYOND the ScrError prefix // the IR fields describe — a subclass layout would overlap them. if (lowerer.builtinErrorInfoOf(symbol)?.def.name === "%DOMException") { lowerer.unsupported( "SC1090", t, "extending DOMException (its runtime layout carries hidden slots a subclass would overlap — extend Error and set name/code yourself)", ); } const named = (symbol && lowerer.classBySymbol.get(symbol)) ?? lowerer.builtinErrorInfoOf(symbol) ?? lowerer.builtinEmitterInfoOf(symbol) ?? builtinStreamInfoOf(lowerer, symbol) ?? // A const BINDING holding exactly one class (`const B = Animal`, // `const B = class {…}`): the base is that class — extends // through the alias is the declaration story (a general class // VALUE stays fenced: the runtime base would be dynamic). exactClassOfReceiver(lowerer, t.expression) ?? // A require BINDING of a class-expression whole export // (`const C = require('./x')` over `module.exports = class {…}`): // the alias resolves to the expression's own symbol — the same // declaration story, collected on demand. propertyAssignedClassInfoOf(lowerer, symbol) ?? // A const BINDING of a mixin call (`const Tagged = M(Base); // class D extends Tagged {}`): the binding pins that call's // instantiation — collected on demand (lower-mixins.ts). mixinResultBindingClassOf(lowerer, symbol) ?? (() => { const stored = storedClassValueType(lowerer, t.expression); return stored?.kind === "classval" ? lowerer.classes.get(stored.className) : null; })() ?? null; // `extends Box` — a GENERIC program class as the base: the // base is the concrete INSTANTIATION, resolved through the heritage // type (mapType registers/reuses `Box%0`). if (named?.generic) { const instT = lowerer.checker.getTypeAtLocation(t); const mappedBase = lowerer.mapTypeOf(instT); const instBase = mappedBase?.kind === "object" ? lowerer.classes.get(mappedBase.className) : undefined; if (!instBase || instBase.generic) { lowerer.unsupported( "SC1090", t, `extending the generic class '${t.expression.text}' without a compiled concrete instantiation (the type arguments must map — see the instantiation's own diagnostic)`, ); } base = instBase; continue; } if (t.typeArguments) lowerer.unsupported("SC1090", t, "extending generic classes"); base = named; if (!base) { if (adoptCallableBase(t.expression)) continue; lowerer.unsupported( "SC1090", t, `extending classes not declared in the program ('${t.expression.text}')`, ); } } if (base?.localClass && !functionLocalClass(decl) && !computedBase) lowerer.unsupported("SC1090", decl, "extending a function-local class outside a function"); if (lowerer.collectingExprClasses.has(decl)) { for (let parent = base; parent; parent = parent.base) { if (parent.builtinEmitter || parent.builtinStream !== undefined) { lowerer.unsupported("SC1090", decl, "classes extending builtin bases as values (the inherited runtime constructor has no class-value adapter)"); } } } const fields = new Map(base ? base.fields : []); const errorRooted = (() => { for (let c = base; c; c = c.base) if (c.builtinError) return true; return false; })(); const symbolFields = new Map(); if (base?.symbolFields) for (const [symbol, name] of base.symbolFields) symbolFields.set(symbol, name); const symbolMethods = new Map(); if (base?.symbolMethods) for (const [symbol, name] of base.symbolMethods) symbolMethods.set(symbol, name); const fieldOrder: ClassInfo["fieldOrder"] = []; const methods = new Map }>(); const methodEntryFences = new Map(); // Own accessor declarations ("get:x"/"set:x" → node), for the // partial-override analysis below (diagnostics need the node). const accessorNodes = new Map(); /** Non-override methods whose collected return stayed dyn — the * symbol-slot refinement retries them after the constructor scan * declares this class's OWN symbol-keyed slots. */ const dynRetMethods = new Map(); /** The class's own `emit` override in the forwarding shape (emitter- * rooted classes only) — recorded here, NEVER in `methods`, so emit * calls keep routing through the emitter spoke's dispatch. */ let emitOverride: EmitOverrideRec | undefined; let ctor: ts.ConstructorDeclaration | null = null; /** Initializer-less fields whose type cannot hold undefined and whose * definite assignment tsc did NOT verify (a `!` assertion, or * strictPropertyInitialization off) — checked against the * constructor's top-level assignments after the member loop. */ const unguardedFields: { node: ts.Node; name: string; why: string }[] = []; /** Parameter properties, in parameter order — spliced in FRONT of the * declared fields after the member loop (Node's layout, probed: the * transform hoists their definitions above every declared field). */ const paramProps: NonNullable = []; const staticFields: ClassInfo["staticFields"] = []; const staticMethods: NonNullable = new Map(); const staticBlocks: ts.ClassStaticBlockDeclaration[] = []; const runtimeStatics: ts.ClassElement[] = []; const runtimePrototypeMembers: ts.ClassElement[] = []; // GENERIC methods (own type parameters), instance and static: only // the SYNTAX is checked here — parameter/return types mention the // type parameters and cannot map yet; bodies lower per call-site // instantiation (collectGenericSignature's rule, member form). The // `member.cls` backlink fills after the ClassInfo assembles below. const genericMethods = new Map(); const genericStatics = new Map(); // Accepts generic METHODS and instance FIELDS initialized with a // generic arrow/function expression (`time = async (...) => {...}` // — the field form of a generic method: no closure slot can hold a // generic function, so the member collects aside like a method and // calls dispatch statically per instantiation; the arrow's lexical // `this` IS the instance, exactly the method's param 0). ASYNC // members collect too: a generic async instance is an async // IrFunction like any other (lowerGenericInstance), calls enter // through the instance's own spawn wrapper, and no vtable slot is // ever involved — generic members always dispatch statically. const collectGenericMember = (member: ts.MethodDeclaration | ts.PropertyDeclaration, isStatic: boolean): void => { const fnNode: ts.MethodDeclaration | ts.ArrowFunction | ts.FunctionExpression = ts.isPropertyDeclaration(member) ? (genericFieldFnNodeOf(member) as ts.ArrowFunction | ts.FunctionExpression) : member; if (!ts.isIdentifier(member.name) && !ts.isPrivateIdentifier(member.name)) { lowerer.unsupported("SC1090", member, "computed generic method names"); } if (fnNode.asteriskToken) lowerer.unsupported("SC1071", member); const mName = (member.name as ts.Identifier | ts.PrivateIdentifier).text; const typeParams: ts.Symbol[] = []; for (const tp of fnNode.typeParameters!) { const sym = lowerer.checker.getSymbolAtLocation(tp.name); if (!sym) lowerer.unsupported("SC1090", member, "this method form"); typeParams.push(sym); } for (const param of fnNode.parameters) { if (!ts.isIdentifier(param.name) && !ts.isObjectBindingPattern(param.name) && !ts.isArrayBindingPattern(param.name)) { lowerer.unsupported("SC1031", param); } } (isStatic ? genericStatics : genericMethods).set(mName, { decl: fnNode, baseName: mName, qualifiedName: `%${className}.${isStatic ? "static:" : ""}${mName}`, typeParams, instances: new Map(), }); }; for (const member of decl.members) { if (functionLocalClass(decl) && isJsSourceFile(decl.getSourceFile()) && (ts.isClassStaticBlockDeclaration(member) || ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword))) { runtimeStatics.push(member); continue; } if (functionLocalClass(decl) && isJsSourceFile(decl.getSourceFile()) && (ts.isMethodDeclaration(member) || ts.isAccessor(member)) && member.body && ts.isComputedPropertyName(member.name) && classMemberNameOf(lowerer, member.name) === null) { runtimePrototypeMembers.push(member); continue; } if (ts.isClassStaticBlockDeclaration(member)) { // Statics live on the FAMILY (JS has one class, one static // storage, however many instantiations exist) — instantiations // skip them. if (inst) continue; // A static block is declaration-time CODE — Node runs it when the // class statement evaluates, referenced or not — so it collects // for %init lowering (lowerStaticFieldInits) instead of fencing. // `this` (and super) inside the block means the class constructor // value, which has no value form here: fenced at the reference, // with arrow functions transparent (they inherit the block's // `this`) and this-binding function forms opaque (their `this` is // their own). rejectStaticThis( lowerer, member.body, () => "'this' in class static blocks (it names the class — reference the class by name instead)", ); staticBlocks.push(member); continue; } const modifiers = ts.canHaveModifiers(member) ? ts.getModifiers(member) : undefined; if (modifiers?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword)) { // Statics live on the FAMILY (one storage location for every // instantiation — JS's one class); instantiations skip them and // reach them through the base chain (findStaticOn). if (inst) continue; // The honest static subset: a field WITH an initializer is a // module global (mutable when not readonly) assigned once at // the class statement's position in module init and read as // `C.name` anywhere; a static METHOD is an ordinary module // function `%C.static:m`. No per-class runtime property table // exists, so the members that would need one — accessors, and // initializer-less fields (undefined until someone assigns // them) — keep the fence, each named at its use site. // #PRIVATE statics ride along under their spelled names // ('#count' → the module global %g.s.C.#count, '#make' → the // module function %C.static:#make): tsc confines every access // to the declaring class's body, and the resolution guard in // findStaticOn's callers keeps a SUBCLASS-named receiver // (`D.#s` — Node's brand TypeError) from resolving up the // chain. Class-VALUE receivers fence for privates (a classval // slot can hold a descendant at runtime, and only the declaring // class object carries the brand in JS). if ( ts.isPropertyDeclaration(member) && (ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) && member.initializer && member.postfixToken?.kind !== ts.SyntaxKind.QuestionToken ) { const type = lowerer.irTypeOf(member.name); if (type.kind === "void") lowerer.badType(member.name, lowerer.typeOf(member.name)); if (type.kind === "dyn" && !isJsSourceFile(member.getSourceFile())) { lowerer.unsupported("SC1090", member.name, "'unknown'-typed static fields"); } staticFields.push({ name: member.name.text, type, initializer: member.initializer, globalId: `%g.s.${lowerer.classNamer(decl)}.${member.name.text}`, readonly: modifiers.some((m) => m.kind === ts.SyntaxKind.ReadonlyKeyword), }); } // Async statics collect like any static method: the module // function `%C.static:m` is an async IrFunction (fiber spawn // wrapper), no vtable in sight — statics never dispatch. if ( ts.isMethodDeclaration(member) && (ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) && member.body && member.typeParameters === undefined ) { const { shapes, funcType: ft } = lowerer.lambdaSignature(member); if (member.asteriskToken !== undefined && ft.ret.kind !== "generator") { lowerer.badType(member.name, lowerer.typeOf(member.name)); } staticMethods.set(member.name.text, { params: shapes, ret: ft.ret, member }); if (implicitMonoFile(decl.getSourceFile()) && !functionLocalClass(decl) && inst === undefined && decl.typeParameters === undefined) { const implicit = implicitAnyParamSymbolsOf(lowerer, member, true); if (implicit) genericStatics.set(member.name.text, { decl: member, baseName: member.name.text, qualifiedName: `%${className}.static:${member.name.text}%implicit`, typeParams: [], instances: new Map(), implicitParams: implicit, }); } } // GENERIC static methods monomorphize like top-level generic // functions (`%C.static:m%n`), async ones included — a generic // async instance is an async module function entered through its // own spawn wrapper (the async-static precedent above, per // instantiation). if ( ts.isMethodDeclaration(member) && (ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) && member.body && member.typeParameters !== undefined ) { collectGenericMember(member, true); } // Statics that don't qualify for the module-global/module- // function treatment (accessors, initializer-less fields, // async/generic methods) never live on instances, so they must // not poison the class either — constructions and instance // members stay compilable, and each USE of an unsupported // static fences at its own site. continue; } // INSTANCE members of a generic class collect per instantiation // (`inst` set, the type-parameter bindings threaded through every // mapType) — the family declares none. if (familyMode) continue; // 7's ClassElement base carries no `name`; read it structurally // (every named member kind stores a PropertyName there). const memberName = (member as { name?: ts.PropertyName }).name; if (errorRooted && memberName && classMemberNameOf(lowerer, memberName) === "cause") { lowerer.unsupported("SC1090", memberName, "redeclaring Error.cause (pass the cause in the Error constructor options)"); } // #PRIVATE members compile: their names ('#m') are unspellable by // any public identifier, so they ride the ordinary fields/methods // maps collision-free — with the base-chain walks doubling as // LEXICAL resolution because a subclass re-declaring an inherited // private NAME is fenced here (JS would give the two classes // DISTINCT private slots under one spelling; one name, one slot is // the static story — rename one). tsc guarantees every access site // sits inside the declaring class's body, and privates never // enter vtables (no redeclaration below ⇒ overrideBelow is false // ⇒ every call devirtualizes), which is exactly JS's semantics: // lexically bound, no dynamic dispatch, a subclass cannot // override. if (memberName && ts.isPrivateIdentifier(memberName)) { const pname = memberName.text; if ( base !== null && (base.fields.has(pname) || lowerer.findMethodOn(base, pname) !== null || lowerer.findMethodOn(base, `get:${pname}`) !== null || lowerer.findMethodOn(base, `set:${pname}`) !== null || findGenericMethodOn(lowerer, base, pname) !== null) ) { lowerer.unsupported( "SC1090", memberName, `redeclaring the private name '${pname}' of a base class (JS gives each class its own distinct '${pname}' slot; these layouts have one slot per name — rename one)`, ); } } // The EventEmitter API surface is runtime-provided: a subclass // member with one of its names would shadow behavior the runtime // dispatches internally (meta events, once removal), so the // override is fenced rather than silently split-brained — with ONE // exception: `emit` in the forwarding shape on a plain (non- // stream) emitter-rooted class monomorphizes per event name // (lower-event-emitter.ts's emit-overrides block). Stream-rooted classes // keep the fence for emit too: the runtime stream machinery emits // 'data'/'end'/... internally, which could never route through // the override. if ( memberName && ts.isIdentifier(memberName) && EMITTER_API_MEMBERS.has(memberName.text) && (() => { for (let c = base; c; c = c.base) if (c.builtinEmitter) return true; return false; })() ) { const streamRooted = (() => { for (let c = base; c; c = c.base) if (c.builtinStream !== undefined) return true; return false; })(); if ( memberName.text === "emit" && !streamRooted && !inst && !mixin && ts.isMethodDeclaration(member) && !member.modifiers?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword) ) { const reason = emitOverrideShapeReason(lowerer, member); if (reason === null) { const eventSym = lowerer.checker.getSymbolAtLocation(member.parameters[0]!.name); const restSym = lowerer.checker.getSymbolAtLocation(member.parameters[1]!.name); if (eventSym && restSym) { emitOverride = { decl: member, eventSym, restSym }; continue; } } lowerer.unsupported( "SC1090", memberName, `overriding EventEmitter's 'emit' outside the forwarding shape (${reason ?? "its parameters do not resolve statically"}; the compiled form is \`emit(event: string, ...args: unknown[]): boolean\`)`, ); } lowerer.unsupported( "SC1090", memberName, `overriding the EventEmitter member '${memberName.text}' (the runtime owns the emitter surface)`, ); } // The stream surface is likewise runtime-provided on stream-rooted // subclasses: API members (push/read/write/...) and the property // family (readableEnded/destroyed/...) dispatch into the runtime // state, so an override or shadowing field would split-brain. // Underscore methods are the SUPPORTED override form — but only // the ones the class's own base consumes (a `_read` on a // Transform, or `_writev`/`_construct` anywhere, would be consumed // by Node machinery that has no lowering here). if (memberName && ts.isIdentifier(memberName)) { const streamBase = (() => { for (let c = base; c; c = c.base) if (c.builtinStream) return c; return null; })(); if (streamBase) { const name = memberName.text; if (STREAM_API_MEMBERS.has(name) || STREAM_PROP_MEMBERS.has(name)) { lowerer.unsupported( "SC1090", memberName, `overriding the stream member '${name}' (the runtime owns the stream surface)`, ); } if (name === "_writev" || name === "_construct") { lowerer.unsupported( "SC1090", memberName, `declaring '${name}' on a stream subclass (${name === "_writev" ? "batched writes are" : "deferred construction is"} not lowered — writes deliver one chunk at a time)`, ); } const accepted = streamCtorShape(streamBase.def.name).accepted; for (const [option, methodName] of UNDERSCORE_METHODS) { if (name === methodName && !accepted.includes(option)) { lowerer.unsupported( "SC1090", memberName, `declaring '${name}' on a ${streamBase.def.name.slice(1)} subclass (its constructor consumes ${accepted.map((a) => `'${UNDERSCORE_METHODS.get(a)}'`).join("/")})`, ); } } } } if (ts.isPropertyDeclaration(member)) { // An ABSTRACT property declaration is erased at runtime — Node // defines NO field for it (verified: `abstract p: number` in the // base leaves the concrete subclass's own `p = 3` as the only // property, at the SUBCLASS's position in inspect order). So it // contributes nothing to the layout; the concrete subclass's // declaration is an ordinary OWN field (tsc guarantees every // instantiable subclass declares it and, under // strictPropertyInitialization, initializes it). Reads through // ABSTRACT-typed receivers have no slot to read and keep a // per-site fence. if (modifiers?.some((m) => m.kind === ts.SyntaxKind.AbstractKeyword)) continue; if (modifiers?.some((m) => m.kind === ts.SyntaxKind.AccessorKeyword)) { // `accessor x = 1` desugars (in JS) to a private slot plus a // get/set pair — declare the field and accessors explicitly. lowerer.unsupported("SC1090", member, "auto-accessor fields ('accessor x')"); } if (ts.isComputedPropertyName(member.name)) { const key = classSymbolKeyOf(lowerer, member.name.expression); if (!key || (!member.initializer && !isJsSourceFile(member.getSourceFile()))) { lowerer.unsupported("SC1090", member, "computed class fields without an initializer and a stable module-level literal Symbol()/Symbol.for() key"); } const declared = lowerer.typeOf(member); const inferred = member.type || !member.initializer ? declared : lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(member.initializer)); const mapped = member.initializer ? lowerer.mapTypeOf(inferred) ?? dynFallbackType(lowerer, member, inferred) : DYN; const type = mapped ? jsOpenObjectType(member, mapped, lowerer.shapes, lowerer.unions) : null; if (!type || type.kind === "void" || (type.kind === "dyn" && !isJsSourceFile(member.getSourceFile()))) lowerer.badType(member, inferred); const undefinedInitializer = !member.initializer ? locOf(member) : undefined; const previous = fields.get(key.fieldName); if (previous) { if (!symbolFields.has(key.identity)) { lowerer.unsupported("SC1090", member.name, `distinct keys sharing the printable name '${symbolFieldDisplayName(key.fieldName)}' in one class`); } if (!typeEquals(previous, type)) { lowerer.unsupported("SC1090", member.name, "redeclaring symbol-keyed fields at a different type"); } fieldOrder.push({ name: key.fieldName, type, initializer: member.initializer, ...(undefinedInitializer ? { undefinedInitializer } : {}), redeclared: true }); } else { fields.set(key.fieldName, type); fieldOrder.push({ name: key.fieldName, type, initializer: member.initializer, ...(undefinedInitializer ? { undefinedInitializer } : {}) }); } symbolFields.set(key.identity, key.fieldName); continue; } // #private fields ride the ordinary field machinery — the '#' // name is unspellable publicly, so the slot never collides, and // enumeration surfaces (inspect) exclude it like Node. if (!ts.isIdentifier(member.name) && !ts.isPrivateIdentifier(member.name)) { lowerer.unsupported("SC1090", member, "computed field names"); } // A field initialized with a GENERIC arrow/function expression // (`time = async (label, fn) => {...}` — the Output.time // idiom): a generic MEMBER, not a field. No closure slot can // hold a generic function (the record-shape exclusion rule), so // the member collects like a generic method — no field slot, // static per-instantiation dispatch, `this` as param 0 (the // arrow's lexical `this` IS the instance). Reads of the field as // a VALUE and writes to it fence at their sites — there is no // slot — which enforces never-reassigned by construction. // Guarded on the member TYPE still carrying its type parameters: // an annotation that pins a concrete signature makes an ordinary // closure field, which the normal path below owns. if ( genericFieldFnNodeOf(member) !== null && isGenericCallableMemberType(lowerer.typeOf(member.name), lowerer.checker) ) { if (fields.has(member.name.text)) { lowerer.unsupported("SC1090", member.name, "redeclaring inherited fields"); } if (lowerer.findMethodOn(base, member.name.text)) { lowerer.unsupported("SC1090", member.name, "fields shadowing inherited methods"); } if (findGenericMethodOn(lowerer, base, member.name.text)) { lowerer.unsupported( "SC1090", member.name, `redeclaring the inherited generic member '${member.name.text}' (generic members dispatch statically, so the base's call sites could never reach this redeclaration)`, ); } collectGenericMember(member, false); continue; } // Non-generic fields shadowing an inherited GENERIC member split // the two dispatch worlds (static per-instantiation calls would // never see the field's value) — the method-vs-generic mixing // fence, field form. if (findGenericMethodOn(lowerer, base, member.name.text)) { lowerer.unsupported( "SC1090", member.name, `fields shadowing the inherited generic member '${member.name.text}' (generic members dispatch statically and would never reach this field's value)`, ); } // OPTIONAL fields (`a?: string`) are the record-field precedent // applied to class shapes: the checker already types the slot // `string | undefined`, the allocation writes the interned // undefined arm (undefFieldInitLineC — Node defines the property // as undefined on construction, verified), and reads/writes ride // the ordinary undefined-armed union machinery. let type = lowerer.irTypeOf(member.name); // Unannotated JavaScript callback slots retain the original // callable and its receiver instead of adapting its signature. if (isJsSourceFile(member.getSourceFile()) && !member.type && !/@type\b/.test(member.getSourceFile().text.slice(member.pos, member.getStart())) && type.kind === "func" && lowerer.dynConvertible(type)) type = DYN; // JS permits reads before a constructor's first assignment even // when its inferred field type omits undefined. if (isJsSourceFile(member.getSourceFile()) && !member.initializer && type.kind !== "dyn") { type = lowerer.withUndefinedArmOf(type) ?? DYN; } if (type.kind === "void") lowerer.badType(member.name, lowerer.typeOf(member.name)); // Bundled JS declares fields without annotations before assigning // them in the constructor. Use the same native checked-dynamic // storage as implicit constructor-assigned JS fields. if (type.kind === "dyn" && !isJsSourceFile(member.getSourceFile())) { lowerer.unsupported("SC1090", member.name, "'unknown'-typed class fields"); } const undefinedInitializer = type.kind === "dyn" && !member.initializer ? locOf(member) : undefined; if (fields.has(member.name.text)) { // REDECLARING an inherited field: Node [[Define]]s the OWN // property again when THIS class's field initializers run // (after super()), so the base slot simply takes the new // value at that position — a slot-type-exact redeclare WITH // an initializer lowers as an assignment into the inherited // slot, no new slot, no layout change (the `class // ConfigError extends Error { name = "ConfigError" }`; the // builtin Error prefix included — reads, toString, and throw // reports all answer the overwritten name like Node). A bare // JS dyn redeclaration resets the existing slot to undefined. // Other bare or type-changing redeclarations keep the fence. const baseType = fields.get(member.name.text)!; if ((member.initializer || type.kind === "dyn") && typeEquals(type, baseType)) { fieldOrder.push({ name: member.name.text, type, initializer: member.initializer, ...(undefinedInitializer ? { undefinedInitializer } : {}), redeclared: true }); continue; } lowerer.unsupported( "SC1090", member.name, member.initializer ? "redeclaring inherited fields at a different type" : "redeclaring inherited fields without an initializer (Node resets the field to undefined)", ); } if (lowerer.findMethodOn(base, member.name.text)) { lowerer.unsupported("SC1090", member.name, "fields shadowing inherited methods"); } // Initializer-less fields whose type ADMITS undefined start as // JS's undefined (the allocation writes the interned undefined // arm — see the backend's undefFieldInitLineC), exactly Node's // fresh-instance read. A field whose type CANNOT hold undefined // has no honest pre-assignment value in these monomorphic // layouts — zeroed memory would read as garbage (0, NULL) where // Node reads undefined — so it needs a definite-assignment // guarantee. tsc's strictPropertyInitialization is that // guarantee; where the program waives it — a `x!: T` assertion, // or a project tsconfig with the option off (scriptc adopts the // project's strictness knobs) — the field goes on the deferred // list checked against the constructor after the member loop // (the constructor may be declared later in the class body). const admitsUndefined = (type.kind === "union" && (lowerer.unions.get(type.unionId)?.arms.some((a) => a.kind === "undefinedT") ?? false)) || type.kind === "jsval" || type.kind === "dyn"; if (!member.initializer && !admitsUndefined) { const opts = lowerer.program.getCompilerOptions(); const spi = opts.strictPropertyInitialization ?? opts.strict ?? false; if (member.postfixToken?.kind === ts.SyntaxKind.ExclamationToken) { unguardedFields.push({ node: member.name, name: member.name.text, why: `definite assignment assertions on fields not assigned at the constructor's top level ('${member.name.text}!' defers the first assignment past construction — the field would hold garbage, not undefined, until it runs; assign it in the constructor or include undefined in its type)`, }); } else if (!spi) { unguardedFields.push({ node: member.name, name: member.name.text, why: `initializer-less fields not assigned at the constructor's top level when strictPropertyInitialization is off (nothing guarantees '${member.name.text}' is assigned before a read — enable the option, assign it unconditionally at the top of the constructor, or include undefined in its type)`, }); } } fields.set(member.name.text, type); fieldOrder.push({ name: member.name.text, type, initializer: member.initializer, ...(undefinedInitializer ? { undefinedInitializer } : {}) }); } else if (ts.isConstructorDeclaration(member)) { // A body-less constructor is an OVERLOAD SIGNATURE: type-world, // lowers to nothing — tsc resolved each `new` against the // signatures, and construction flows through the implementation's // ABI (its parameter types are supersets by the // overload-compatibility rules). if (!member.body) continue; if (ctor) lowerer.unsupported("SC1090", member, "constructor overloads"); // PARAMETER PROPERTIES (`constructor(public x: number)`): pure // sugar — the parameter declares a field and assigns it from the // parameter's value. Visibility (public/private/protected) and // readonly/override are type-world; the field is an ordinary // property at runtime. The field's type is the parameter's BODY // type (paramShape's contract: the plain T of a defaulted // `public x = e`, the `T | undefined` union of `public x?: T`) — // exactly what the ctor's body local carries, so the synthesized // assignment is slot-exact. Layout/inspect position and // assignment order are Node's, probed exactly: the fields define // FIRST (before every declared field, as undefined), and the // assignments run after super() and the field initializers, in // parameter order (see paramPropInitStmts). for (const p of member.parameters) { const isParamProp = p.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, ); if (!isParamProp) { // Non-keyword modifiers (parameter decorators) are rejected // by tsc under standard decorators; defensive. if (p.modifiers?.length) lowerer.unsupported("SC1090", p, "this parameter form"); continue; } // tsc rejects binding patterns (TS1187) and rest params // (TS1317) as parameter properties; defensive. if (!ts.isIdentifier(p.name) || p.dotDotDotToken) { lowerer.unsupported("SC1090", p, "this parameter property form"); } const name = (p.name as ts.Identifier).text; if (errorRooted && name === "cause") { lowerer.unsupported("SC1090", p.name, "redeclaring Error.cause (pass the cause in the Error constructor options)"); } const shape = lowerer.paramShape(p); const type = shape.bodyType ?? shape.type; if (type.kind === "void") lowerer.badType(p.name, lowerer.typeOf(p.name)); // The class-field dyn rule verbatim (KEEP NARROW). if (type.kind === "dyn") { lowerer.unsupported("SC1090", p.name, "'unknown'-typed class fields"); } // `override x` (and any same-named inherited member) would // redeclare a base slot — the declared-field rule verbatim. if (fields.has(name)) { lowerer.unsupported("SC1090", p.name, "redeclaring inherited fields"); } if (lowerer.findMethodOn(base, name)) { lowerer.unsupported("SC1090", p.name, "fields shadowing inherited methods"); } paramProps.push({ name, type, param: p }); } ctor = member; } else if (ts.isMethodDeclaration(member)) { const mName = classMemberNameOf(lowerer, member.name); if (mName === null) lowerer.unsupported("SC1090", member, "computed method names"); if (ts.isComputedPropertyName(member.name)) { const key = classSymbolKeyOf(lowerer, member.name.expression); if (key) { if ([...symbolMethods, ...symbolFields].some(([identity, name]) => name === mName && identity !== key.identity)) { lowerer.unsupported("SC1090", member.name, `distinct symbol keys sharing the printable name '${symbolFieldDisplayName(mName)}' in one class`); } symbolMethods.set(key.identity, mName); } } // Generator calls use their spawn wrapper. Public methods are // direct calls only when no subclass overrides the method; // override chains involving generators fence below. // An ABSTRACT method is a signature with no body — type-world, // except that it declares the vtable slot: calls through // base-typed receivers are ordinary virtual dispatch, and tsc // guarantees every instantiable subclass implements it (so a // dispatch can never land on the empty declaration). It enters // `methods` (marked abstract) for slot declaration and the // override-exactness rule; no module function ever exists. if (ts.getModifiers(member)?.some((m) => m.kind === ts.SyntaxKind.AbstractKeyword)) { // A GENERIC abstract method has no body to monomorphize — // per-call-site instantiation needs a nearest declarer WITH a // body, which an abstract declaration never has. if (member.typeParameters !== undefined) { lowerer.unsupported( "SC1090", member, "abstract generic methods (generic methods monomorphize from the nearest declaration's body, and an abstract declaration has none)", ); } const { shapes, ret } = abstractMemberSignature(lowerer, member); if (fields.has(mName)) { lowerer.unsupported("SC1090", member.name, "methods shadowing inherited fields"); } if (findGenericMethodOn(lowerer, base, mName)) { lowerer.unsupported( "SC1090", member.name, `overriding the inherited generic method '${mName}' with a non-generic method (generic methods dispatch statically and would never reach this override)`, ); } // Abstract re-declarations keep the overridden ABI exactly, // like any override (a concrete implementation below must // agree with BOTH, which exactness makes one constraint). const overridden = lowerer.findMethodOn(base, mName); if ( overridden && (overridden.sig.params.length !== shapes.length || !overridden.sig.params.every((p, i) => typeEquals(p.type, shapes[i]!.type)) || !typeEquals(overridden.sig.ret, ret)) ) { lowerer.unsupported( "SC1090", member.name, "overriding a method with a different signature (parameter and return types must match the base declaration exactly)", ); } methods.set(mName, { params: shapes, ret, abstract: true }); continue; } // A body-less method is an OVERLOAD SIGNATURE (abstract methods // collected above): type-world, exactly the constructor story. if (!member.body) continue; // GENERIC methods (own type parameters): collected aside — never // in `methods` (no single ABI signature, no vtable slot); bodies // lower per call-site instantiation as `%C.m%n`. Mixing generic // and non-generic declarations of one name across the hierarchy // fences (the two dispatch worlds — static per-instantiation // calls vs vtable slots — cannot see each other's overrides). if (member.typeParameters !== undefined) { if (fields.has(mName)) { lowerer.unsupported("SC1090", member.name, "methods shadowing inherited fields"); } if (lowerer.findMethodOn(base, mName)) { lowerer.unsupported( "SC1090", member.name, `overriding the inherited method '${mName}' with a generic method (generic methods dispatch statically, so the base's vtable slot could never reach this override)`, ); } collectGenericMember(member, false); continue; } // Async methods collect with the same direct-call spawn wrapper // for JavaScript and TypeScript classes. Override chains remain // fenced below because vtable slots cannot enter that wrapper. // IMPLICIT-ANY monomorphization (npm-static JS): a method whose // signature carries bindable untyped params collects like a // GENERIC method — into genericMethods, no vtable slot, one // instance per call-site type tuple (the untyped params ARE the // type parameters; see lower-calls' implicit section). DECLINES // (falls through to the normal all-dyn ABI) wherever the two // dispatch worlds could meet: an inherited declaration of the // name (the override stays on the vtable), a shadowed field, or // a generic-class instantiation's member. if ( implicitMonoFile(decl.getSourceFile()) && !functionLocalClass(decl) && ts.isIdentifier(member.name) && !lowerer.virtualJsMethods.has(member) && !lowerer.prototypeMethodAccesses.has(member.name.text) && inst === undefined && decl.typeParameters === undefined && !fields.has(member.name.text) && !lowerer.findMethodOn(base, member.name.text) && !findGenericMethodOn(lowerer, base, member.name.text) ) { const implicit = implicitAnyParamSymbolsOf(lowerer, member, true); if (implicit) { genericMethods.set(member.name.text, { decl: member, baseName: member.name.text, qualifiedName: `%${className}.${member.name.text}`, typeParams: [], instances: new Map(), implicitParams: implicit, }); continue; } } const { shapes, funcType: ft } = lowerer.lambdaSignature(member); if (isJsSourceFile(member.getSourceFile()) && lowerer.prototypeMethodAccesses.has(mName) && !lowerer.findMethodOn(base, mName)?.declarer.def.runtime && shapes.every((p) => p.mode === "required" || p.mode === "omittable")) { for (const shape of shapes) { shape.type = DYN; if (shape.bodyType) shape.bodyType = DYN; } ft.params = shapes.map((shape) => shape.type); if (ft.ret.kind !== "void") ft.ret = DYN; } // JS subclasses can add parameters to a method. Reserve checked // argument slots in the base before constructing its vtable; shorter // bodies ignore them and omitted call arguments become undefined. const virtualArity = lowerer.virtualJsMethodArity.get(member) ?? shapes.length; if (virtualArity > shapes.length && !bodyReadsArguments(member) && shapes.every((p) => p.type.kind === "dyn" && (p.mode === "required" || p.mode === "omittable"))) { while (shapes.length < virtualArity) shapes.push({ type: DYN, mode: "omittable" }); } if (fields.has(mName)) { lowerer.unsupported("SC1090", member.name, "methods shadowing inherited fields"); } if (findGenericMethodOn(lowerer, base, mName)) { lowerer.unsupported( "SC1090", member.name, `overriding the inherited generic method '${mName}' with a non-generic method (generic methods dispatch statically and would never reach this override)`, ); } // Symbol-slot return refinement, INHERITED slots (own ctor-declared // slots refine in the post-scan pass below — the constructor hasn't // been scanned yet here, but base slots are already in // symbolFields/fields). Doing it before the exactness check keeps // a derived override of a refined base method agreeing. if (ft.ret.kind === "dyn") { const refined = symbolSlotReturnType(lowerer, member, symbolFields, fields); if (refined) ft.ret = refined; } // Overrides keep the EXACT overridden ABI signature. tsc's method // bivariance would let a narrowed parameter type through, and a // vtable-dispatched call could then hand the override a base // instance it reads out-of-bounds fields from — exactness keeps // every slot sound (covariant returns can come later). Comparing // ABI types only (not modes) is deliberate: call sites complete // against the STATIC receiver's shape, so `m(x?: number)` and // `m(x: number | undefined)` interchange soundly in overrides. const overridden = lowerer.findMethodOn(base, mName); if (overridden && isJsSourceFile(member.getSourceFile())) { // An unannotated JS override can return a checked value into // the inherited native ABI, just as its untyped parameters do. // Body lowering inserts the conversion at each return. if (!member.type && ft.ret.kind === "dyn" && overridden.sig.ret.kind !== "dyn" && !overridden.sig.async && !overridden.sig.gen) ft.ret = overridden.sig.ret; // Fluent overrides return the same native receiver. Keep the // inherited return ABI and upcast `this` when lowering returns. if (overridden.sig.ret.kind === "object" && base && (base.def.name === overridden.sig.ret.className || lowerer.isSubclassOf(base.def.name, overridden.sig.ret.className)) && returnsOnlyThis(member)) ft.ret = overridden.sig.ret; // An unannotated JS parameter can use the base's typed ABI. // Defaults stay local to each declaration, so only inherited // required/optional slots participate in this refinement. for (let i = 0; i < shapes.length; i++) { const parameter = member.parameters[i]; const inherited = overridden.sig.params[i]; if (parameter && inherited && !parameter.type && !parameter.initializer && !parameter.dotDotDotToken && shapes[i]!.type.kind === "dyn" && inherited.type.kind !== "dyn" && (inherited.mode === "required" || inherited.mode === "omittable")) { shapes[i] = { ...inherited }; } } } // JS overrides may ignore trailing arguments. Retain the base's // checked-value ABI slots as unused parameters, and let shorter // calls fill them with undefined. Typed tails and rest/arguments // readers keep the exact-signature fence. if ( overridden && isJsSourceFile(member.getSourceFile()) && shapes.length < overridden.sig.params.length && !bodyReadsArguments(member) && shapes.every((p, i) => (p.mode === "required" || p.mode === "omittable") && typeEquals(p.type, overridden.sig.params[i]!.type)) && overridden.sig.params.slice(shapes.length).every((p) => p.type.kind === "dyn" && (p.mode === "required" || p.mode === "omittable")) ) { shapes.push(...overridden.sig.params.slice(shapes.length).map((p): ParamShape => ({ type: p.type, mode: "omittable" }))); } if ( overridden && (overridden.sig.params.length !== shapes.length || !overridden.sig.params.every((p, i) => typeEquals(p.type, shapes[i]!.type)) || !typeEquals(overridden.sig.ret, ft.ret)) ) { const fencedJsOverride = isJsSourceFile(member.getSourceFile()) && overridden.declarer.decl !== null && isJsSourceFile(overridden.declarer.decl.getSourceFile()) && member.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) !== true && overridden.sig.async !== true && member.asteriskToken === undefined && overridden.sig.gen === undefined; if (fencedJsOverride) { // A dormant JS method must not poison the whole class. Keep // the inherited call/dispatch ABI and refuse at method entry; // lowering this body with the base signature would change JS. const returnOnly = overridden.sig.params.length === shapes.length && overridden.sig.params.every((p, i) => typeEquals(p.type, shapes[i]!.type)); methodEntryFences.set(mName, unsupportedDiag( "SC1090", locOf(member.name), returnOnly ? `overriding method '${mName}' with a different return type (the native return type must match the base declaration exactly)` : `overriding method '${mName}' with a different signature (the native parameter and return types must match the base declaration exactly)`, )); shapes.splice(0, shapes.length, ...overridden.sig.params); ft.ret = overridden.sig.ret; } else { lowerer.unsupported( "SC1090", member.name, "overriding a method with a different signature (parameter and return types must match the base declaration exactly)", ); } } const asyncMember = member.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; // Async methods dispatch STATICALLY (the body enters through its // fiber spawn wrapper; vtable slots hold raw implementations) — // an override chain touching an async method on either end would // put a spawn wrapper behind a virtual slot, so it fences. if (overridden && (asyncMember || overridden.sig.async === true)) { lowerer.unsupported( "SC1090", member.name, `overriding ${overridden.sig.async === true ? "the async method" : "a method with an async method"} '${mName}' (async methods dispatch statically — the vtable slot machinery has no fiber-spawn story)`, ); } if (overridden && (member.asteriskToken !== undefined || overridden.sig.gen !== undefined)) { lowerer.unsupported( "SC1090", member.name, `overriding the generator method '${mName}' (generator methods dispatch statically — a virtual slot cannot enter their spawn wrapper)`, ); } // A GENERATOR method carries its channels on the sig: // the body lowers as a generator IrFunction (`this` as param 0), // and every call — direct by construction — enters through the // emitted gen-spawn wrapper, answering the suspended generator. if (member.asteriskToken !== undefined) { if (ft.ret.kind !== "generator") lowerer.badType(member.name, lowerer.typeOf(member.name)); methods.set(mName, { params: shapes, ret: ft.ret, gen: generatorMeta(lowerer, ft.ret), ...(asyncMember ? { async: true as const } : {}), }); } else { methods.set(mName, asyncMember ? { params: shapes, ret: ft.ret, async: true as const } : { params: shapes, ret: ft.ret }); } // Overrides keep the inherited ABI exactly, so only non-override // methods may still refine once the ctor scan runs. if (ft.ret.kind === "dyn" && !overridden) { dynRetMethods.set(mName, member); } } else if (ts.isGetAccessor(member) || ts.isSetAccessor(member)) { // Accessors are methods with property syntax: `get x()` collects // as the method entry "get:x" (a name no user identifier can // spell, so it can never collide with a real method) and `set x` // as "set:x" — every downstream mechanism (override exactness, // whole-program devirtualization, vtable slots, may-throw) then // applies verbatim, with the get and set halves independent. const isGet = ts.isGetAccessor(member); // #private accessors collect as "get:#x"/"set:#x" — the same // reserved spelling, one more unspellable segment. const prop = classMemberNameOf(lowerer, member.name); if (prop === null) { lowerer.unsupported("SC1090", member, "computed accessor names"); } if (ts.isComputedPropertyName(member.name)) { const key = classSymbolKeyOf(lowerer, member.name.expression); if (key) { if ([...symbolMethods, ...symbolFields].some(([identity, name]) => name === prop && identity !== key.identity)) { lowerer.unsupported("SC1090", member.name, `distinct symbol keys sharing the printable name '${symbolFieldDisplayName(prop)}' in one class`); } symbolMethods.set(key.identity, prop); } } // ABSTRACT accessors are the abstract-method story with property // syntax: body-less by definition, they enter `methods` (marked // abstract) as their "get:x"/"set:x" halves — slot declaration // and override exactness verbatim; no module function exists. const abstractAccessor = ts.getModifiers(member)?.some((m) => m.kind === ts.SyntaxKind.AbstractKeyword) === true; if (!member.body && !abstractAccessor) lowerer.unsupported("SC1090", member, "bodyless accessors"); const mName = `${isGet ? "get" : "set"}:${prop}`; if (fields.has(prop) && !((prop === "message" || prop === "name") && base && (base.def.name === "%Error" || lowerer.isSubclassOf(base.def.name, "%Error")))) { // tsc rejects field/accessor mixing (TS2610/2611); defensive. lowerer.unsupported("SC1090", member.name, "accessors sharing a name with a field"); } let sig: { params: ParamShape[]; ret: IrType }; if (isGet) { const ret = lowerer.declaredReturnType(member, member.name); if (ret.kind === "void") lowerer.badType(member.name, lowerer.typeOf(member.name)); sig = { params: [], ret }; } else { // tsc rejects optional/default/rest setter params (TS1051-53). sig = { params: lowerer.paramShapes(member.parameters), ret: VOID }; } // One property, ONE type: tsc (5.1+) admits get/set pairs with // unrelated annotated types; a property slot here has a single // IR type, so the pair must agree exactly. const twin = methods.get(`${isGet ? "set" : "get"}:${prop}`); const twinType = twin ? (isGet ? twin.params[0]!.type : twin.ret) : null; const ownType = isGet ? sig.ret : sig.params[0]!.type; if (twinType && !typeEquals(twinType, ownType)) { lowerer.unsupported( "SC1090", member.name, `getter/setter pairs with different types (the property '${prop}' must have one type)`, ); } // Same exactness rule as methods — an accessor override keeps // the overridden accessor's type (getter return / setter param). const overridden = lowerer.findMethodOn(base, mName); if ( overridden && (overridden.sig.params.length !== sig.params.length || !overridden.sig.params.every((p, i) => typeEquals(p.type, sig.params[i]!.type)) || !typeEquals(overridden.sig.ret, sig.ret)) ) { lowerer.unsupported( "SC1090", member.name, "overriding an accessor with a different type (the property type must match the base declaration exactly)", ); } methods.set(mName, abstractAccessor ? { ...sig, abstract: true } : sig); // Abstract accessors stay OUT of accessorNodes: they are erased // at runtime (nothing shadows an inherited pair, nothing needs a // synthesized throwing setter) — the partial-override analysis // below reasons about accessors that EXIST on the instance. if (!abstractAccessor) accessorNodes.set(mName, member); } else if (ts.isIndexSignatureDeclaration(member)) { lowerer.unsupported("SC1090", member, "index signatures"); } else if (!ts.isSemicolonClassElement(member)) { lowerer.unsupported("SC1090", member, `syntax '${ts.syntaxKindName(member.kind)}'`); } } // Parameter properties join the shape FIRST among own fields — // Node's transform hoists their definitions above every declared // field (probed: `constructor(public x, private w)` after a declared // `z` still prints `{ x, w, z }`), so layout/inspect order follows. // No definite-assignment analysis applies: the constructor assigns // them unconditionally (paramPropInitStmts). if (paramProps.length > 0) { for (const pp of paramProps) fields.set(pp.name, pp.type); fieldOrder.unshift(...paramProps.map((pp): ClassInfo["fieldOrder"][number] => ({ name: pp.name, type: pp.type, initializer: undefined }))); } // The deferred definite-assignment check: a field on the unguarded // list passes only with an unconditional `this.x = ...` at the // constructor's TOP LEVEL — the same standard the JS-class path // below applies to constructor-declared fields. Anything less // (conditional branches, assignment in a method, no constructor at // all) leaves a window where Node reads undefined and these layouts // would read zeroed memory, so it fences instead. const deferredInitFields = new Set(base?.deferredInitFields ?? []); if (unguardedFields.length > 0) { const topAssigned = new Set(); for (const stmt of ctor?.body?.statements ?? []) { if ( ts.isExpressionStatement(stmt) && ts.isBinaryExpression(stmt.expression) && stmt.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(stmt.expression.left) && stmt.expression.left.expression.kind === ts.SyntaxKind.ThisKeyword ) { topAssigned.add(stmt.expression.left.name.text); } } for (const f of unguardedFields) { if (topAssigned.has(f.name)) continue; // DEFERRED INITIALIZATION (the Output.initialize idiom — // `stream!: T` assigned inside a method the constructor calls): // the slot becomes the undefined-armed union — allocation writes // the interned undefined (Node's pre-assignment value), writes // wrap, and reads CHECKED-extract the declared type, trapping a // genuinely-unassigned read with the catchable TypeError. // Only single-arm declared types take the deferral (the checked // extraction targets one arm); union-typed `!` fields keep the // fence. const declared = fields.get(f.name); const armable = declared !== undefined && !isUnitType(declared) && declared.kind !== "union" && declared.kind !== "jsval" && declared.kind !== "dyn" && declared.kind !== "map" && declared.kind !== "date" && declared.kind !== "generator" && declared.kind !== "void" && declared.kind !== "caught"; const armed = armable ? lowerer.withUndefinedArm(declared) : null; if (armed !== null && armed.kind === "union") { fields.set(f.name, armed); const fo = fieldOrder.find((x) => x.name === f.name); if (fo) fo.type = armed; deferredInitFields.add(f.name); continue; } lowerer.unsupported("SC1090", f.node, f.why); } } // Partial overrides of an inherited accessor pair. JS gives the // derived class ONE own accessor property that SHADOWS the whole // inherited pair — the missing half does NOT resolve to the base's // (verified against Node): // - getter-only override where the chain has a setter: a write // through a base-typed reference (tsc-clean — the base has a // setter) throws TypeError at runtime. Matched exactly: a // synthesized throwing setter fills the derived class's slot. // - setter-only override where the chain has a getter: a read // through a base-typed reference yields undefined — a value // these property types cannot represent. Rejected. const throwingSetters: string[] = []; for (const [mName, node] of accessorNodes) { const prop = mName.slice(4); if (mName.startsWith("get:") && !accessorNodes.has(`set:${prop}`)) { // An ABSTRACT inherited setter is erased at runtime — there is // no accessor pair to shadow, so no throwing setter to // synthesize (tsc makes an instantiable class implement it, and // that implementation shadows nothing either). const baseSet = lowerer.findMethodOn(base, `set:${prop}`); if (baseSet && baseSet.sig.abstract !== true) { if (!typeEquals(baseSet.sig.params[0]!.type, methods.get(mName)!.ret)) { // Unreachable when the base pair agrees (induction through // the exactness rule); a base setter-only + new getter of a // different type would break the slot signature. lowerer.unsupported("SC1090", node.name, "accessors whose getter and inherited setter types differ"); } methods.set(`set:${prop}`, { params: [baseSet.sig.params[0]!], ret: VOID }); throwingSetters.push(prop); } } if (mName.startsWith("set:") && !accessorNodes.has(`get:${prop}`)) { // The abstract-inherited-getter case is the same erasure story. const baseGet = lowerer.findMethodOn(base, `get:${prop}`); if (baseGet && baseGet.sig.abstract !== true) { lowerer.unsupported( "SC1090", node.name, `overriding only the setter of an inherited accessor pair (JS shadows the inherited getter — reads of '${prop}' would yield undefined; declare the getter too)`, ); } } } // JavaScript classes declare fields by ASSIGNMENT: `this.x = v` in // the constructor IS the declaration (checkJs infers the property — // its type is the checker's, exactly like an annotated field). The // supported form is a definite assignment at the constructor's TOP // LEVEL, in source order — the layout is then as fixed as a TS field // list and the assignment itself doubles as the initializer (fields // are zero until the ctor body runs, same as TS's ctor-assigned // declared fields). Properties the checker infers from anywhere else // (conditional branches, methods) would be readable before any // assignment ran — the zeroed-memory trap a TS declaration order // forbids via strictPropertyInitialization — so they keep a named // fence instead of a silent undefined. if (isJsSourceFile(decl.getSourceFile())) { const hasAccessor = (name: string): boolean => methods.has(`get:${name}`) || methods.has(`set:${name}`) || lowerer.findMethodOn(base, `get:${name}`) !== null || lowerer.findMethodOn(base, `set:${name}`) !== null; // Named classes (declarations and self-binding expressions) resolve // by name; the nameless default-export declaration by its module's // default-export symbol. const classSym = decl.name ? lowerer.checker.getSymbolAtLocation(decl.name) : ts.isClassDeclaration(decl) ? declSymbolOf(lowerer, decl) : undefined; const instType = classSym ? lowerer.checker.getDeclaredTypeOfSymbol(classSym) : undefined; // LATE-BOUND properties (the checker's `__@name@id` spelling — // `this[kLimit] = v` where kLimit is a unique symbol const) by // their KEY symbol: the scan below needs the checker's property // type (tsc types computed declarations with unique-symbol keys // statically, exactly like named ones) but getSymbolAtLocation // answers null on element-access declaration sites, so the link // goes through the key. const lateBoundByKey = new Map(); for (const p of instType ? lowerer.checker.getPropertiesOfType(instType) : []) { if (!p.name.startsWith("__@")) continue; const keySym = lateBoundKeySymOf(lowerer, p); if (keySym) lateBoundByKey.set(keySym, p); } if (ctor?.body) { for (const stmt of ctor.body.statements) { const lhs = ts.isExpressionStatement(stmt) && ts.isBinaryExpression(stmt.expression) && stmt.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken ? stmt.expression.left : null; if ( lhs && ts.isPropertyAccessExpression(lhs) && lhs.expression.kind === ts.SyntaxKind.ThisKeyword ) { const assign = lhs; const name = assign.name.text; if (errorRooted && name === "cause") continue; // Later assignments to an already-declared field (own or // inherited) are writes, not declarations. if (fields.has(name) || hasAccessor(name)) continue; if (methods.has(name) || lowerer.findMethodOn(base, name)) { lowerer.unsupported("SC1090", assign, "constructor-assigned fields shadowing methods"); } const sym = lowerer.checker.getSymbolAtLocation(assign); const t = sym ? lowerer.checker.getTypeOfSymbol(sym) : undefined; const rhs = ((stmt as ts.ExpressionStatement).expression as ts.BinaryExpression).right; // Implicit-any fields (assigned from UNTYPED ctor params — // countdown.js's `this.limit = limit`) take the JS checked- // dynamic fallback like every JS binding: the slot holds a dyn // box, reads validate per use, writes convert in (dynFrom). // TS-annotated `unknown` fields keep their fence (KEEP NARROW // applies where an annotation could say better). let type = t ? (lowerer.mapTypeOf(t) ?? dynFallbackType(lowerer, assign, t)) : null; if (t && (t.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0 && isJsSourceFile(decl.getSourceFile())) type = DYN; if (type) type = jsOpenObjectType(assign, type, lowerer.shapes, lowerer.unions); if (type?.kind === "dyn") { type = inferredEmptyCollectionFieldType(lowerer, decl, name, rhs) ?? type; } if (!type || type.kind === "void") lowerer.badType(assign, t ?? lowerer.typeOf(assign)); type = jsFieldInitializerType(lowerer, type, rhs); // A JSDoc claim the BODY contradicts (`@type {Command}` // assigned `undefined` — the lazy-init idiom): the // representation follows the body — the field widens to the // undefined-armed union, so the declaring assignment and // every pre-init read carry Node's actual undefined. // Trust-but-verify: the claim never silently narrows the // runtime value. { const rhsT = lowerer.typeOf(rhs); const assignsUndef = (rhsT.flags & ts.TypeFlags.Undefined) !== 0; const admitsUndef = type.kind === "dyn" || isUnitType(type) || (type.kind === "union" && lowerer.armTag(type.unionId, UNDEFINED_T) >= 0); if (assignsUndef && !admitsUndef) { const widened = lowerer.withUndefinedArmOf(type); if (widened !== null) type = widened; } } fields.set(name, type); fieldOrder.push({ name, type, initializer: undefined }); continue; } // `this[kLimit] = v` at the constructor's top level with a // STATICALLY-RESOLVABLE symbol key (classSymbolKeyOf's // contract — the countdown.js idiom): the key is a compile-time // identity, so the member is an ordinary hidden field of the // static layout under Node's inspect spelling; no runtime // symbol table exists. Its type is the checker's late-bound // property type, through the same JS checked-dynamic fallback // as named fields. Keys that DON'T resolve fall through to the // late-bound fence below. if ( lhs && ts.isElementAccessExpression(lhs) && lhs.expression.kind === ts.SyntaxKind.ThisKeyword ) { const key = classSymbolKeyOf(lowerer, lhs.argumentExpression); if (!key) continue; // A key already declared (own or inherited) makes later // assignments writes, not declarations. if (symbolFields.has(key.identity)) continue; if (fields.has(key.fieldName)) { // Two DISTINCT Symbol(...) consts with one description in // one layout would need one printable name for two slots. lowerer.unsupported( "SC1090", lhs, `distinct symbol keys sharing the printable name '${symbolFieldDisplayName(key.fieldName)}' in one class`, ); } const propSym = lateBoundByKey.get(key.sym); // tsgo does not synthesize the late-bound `__@name@id` // property for a JS `this[k] = v` declaration (the finding-5 // family: no expando/late-bound synthesis in its stricter // CJS-JS modeling), so when the key found no property the // field's type comes from the SAME inference source 5.9.3's // property type did — the declaring assignment's RHS, widened. const rhs = ts.isExpressionStatement(stmt) && ts.isBinaryExpression(stmt.expression) ? stmt.expression.right : undefined; const t = propSym ? lowerer.checker.getTypeOfSymbol(propSym) : rhs ? lowerer.checker.getBaseTypeOfLiteralType(lowerer.checker.getTypeAtLocation(rhs)) : undefined; let type = t ? (lowerer.mapTypeOf(t) ?? dynFallbackType(lowerer, lhs, t)) : null; if (type) type = jsOpenObjectType(lhs, type, lowerer.shapes, lowerer.unions); if (!type || type.kind === "void") lowerer.badType(lhs, t ?? lowerer.typeOf(lhs)); fields.set(key.fieldName, type); symbolFields.set(key.identity, key.fieldName); fieldOrder.push({ name: key.fieldName, type, initializer: undefined }); } } } // Every OTHER inferred instance property — assigned only in // methods, only in conditional constructor positions, or via // computed keys — is undefined until its first write, which these // static layouts cannot represent. Named fence, at the first // assignment site. for (const p of instType ? lowerer.checker.getPropertiesOfType(instType) : []) { if (errorRooted && p.name === "cause") continue; if (fields.has(p.name) || methods.has(p.name)) continue; if (hasAccessor(p.name)) continue; if (base && (base.fields.has(p.name) || lowerer.findMethodOn(base, p.name))) continue; const site = lowerer.checker.declarationsOf(p).find( (d) => ts.isPropertyAccessExpression(d) || ts.isBinaryExpression(d) || ts.isElementAccessExpression(d), ); if (!site) continue; // Late-bound properties: the ones the scan above collected are // real fields under their printable names — skip. The rest keep // a fence that names the supported form: keys that are runtime // identities (symbol parameters, mutable bindings, computed // descriptions) or assignments outside the constructor's top // level. if (p.name.startsWith("__@")) { const keySym = lateBoundKeySymOf(lowerer, p); const key = keySym ? classSymbolKeyOfSymbol(lowerer, keySym) : null; if (key && symbolFields.has(key.identity)) continue; lowerer.unsupported( "SC1090", site, "symbol-keyed class fields outside the supported form (a stable module-level literal Symbol()/Symbol.for() key, assigned unconditionally at the top of the constructor)", ); } // JS classes: a property first assigned in a method or a // conditional constructor position holds `undefined` until the // write runs — exactly representable as the undefined-armed // union of the inferred property type, so the field COLLECTS // (pre-write reads answer undefined, like Node) instead of // poisoning the class (commander's `this.required` switch // assignment, `this.runningCommand` method assignment). // Unmappable inferences and arm-less kinds keep the fence. // TypeScript classes keep the loud fence too: an annotated // program can spell `T | undefined` itself. if (isJsSourceFile(decl.getSourceFile())) { const armed = undefArmedFieldType(lowerer, p); if (armed !== null) { // Unknown JS properties use the instance bag: they do not // exist until assigned, and a subclass may declare the same // name as an ordinary layout field. if (armed.kind === "dyn") continue; fields.set(p.name, armed); fieldOrder.push({ name: p.name, type: armed, initializer: undefined }); continue; } } lowerer.unsupported( "SC1090", site, `fields assigned outside the constructor's top level ('this.${p.name}' would be undefined until the first assignment runs — assign it unconditionally at the top of the constructor)`, ); } } // Second refinement chance, OWN symbol slots: the member loop ran // before the constructor scan declared this class's own symbol-keyed // fields, so methods returning those slots (1731's `extra()` — // `return this[kExtra]`) retry here with the layout complete. for (const [mName, node] of dynRetMethods) { const sig = methods.get(mName); if (!sig || sig.ret.kind !== "dyn") continue; const refined = symbolSlotReturnType(lowerer, node, symbolFields, fields); if (refined) sig.ret = refined; } // The mixin FORWARDING constructor — `constructor(...args: any[]) { // super(...args); … }`: under monomorphization the base's signature // is known, so the instantiation's ABI IS the base's — synthetic // params forward to super unchanged (defaults apply in the base's // own prologue, exactly JS's raw-argument forwarding) and the rest // parameter never materializes. A rest constructor in a mixin that // is NOT the pure forwarding shape has no static story — named // fence, never a mis-typed array. const mixinForwarding = mixin !== undefined && ctor !== null && mixinForwardingCtor(lowerer, ctor); if (mixin && ctor && !mixinForwarding && ctor.parameters.some((p) => p.dotDotDotToken)) { lowerer.unsupported( "SC1090", ctor.parameters.find((p) => p.dotDotDotToken)!, "mixin constructors whose rest parameter does anything but forward (`super(...args)` as the first statement is the compiled shape)", ); } // Constructor omitted on a derived class: it inherits the base's // (tsc types `new Derived(...)` against the inherited signature; the // synthesized constructor forwards the same params to super). const ctorParams: ParamShape[] = ctor && !mixinForwarding ? lowerer.paramShapes(ctor.parameters) : (base?.ctorParams ?? (callableBase ? [{ type: DYN, mode: "dynRest" }] : [])); if (ctor && !mixinForwarding && isJsSourceFile(ctor.getSourceFile()) && !ctorParams.some((shape) => shape.mode === "rest" || shape.mode === "dynRest" || shape.mode === "islandRest") && bodyReadsArguments(ctor)) { ctorParams.push({ type: DYN, mode: "arguments" }); } const info: ClassInfo = { ...(runtimeStatics.length ? { runtimeStatics } : {}), ...(runtimePrototypeMembers.length ? { runtimePrototypeMembers } : {}), def: { name: className, // The JS-observable .name (the class object's name string and // what `C.name` folds to): the declared name, or NamedEvaluation's // answer for class expressions ("" when truly anonymous). An // INSTANTIATION prints its family's name — JS has one `Box`. jsName: jsNameOverride ?? decl.name?.text ?? "", ...(base ? { base: base.def.name } : {}), jsLength: ctor?.parameters.findIndex((parameter) => parameter.initializer || parameter.dotDotDotToken) === -1 ? ctor.parameters.length : Math.max(0, ctor?.parameters.findIndex((parameter) => parameter.initializer || parameter.dotDotDotToken) ?? 0), // Layout order: the base chain's fields as an IDENTICAL prefix, // then this class's own — what makes an upcast a reinterpret. // Redeclared INHERITED fields contribute no slot (their // initializers assign the prefix slot). fields: [ ...(base?.def.fields ?? []), ...fieldOrder.filter((f) => f.redeclared !== true).map((f) => ({ name: f.name, type: f.type })), ], ...(methods.size > 0 ? { methods: [...methods.keys()] } : {}), ...(abstractClass ? { abstract: true as const } : {}), ...((): { abstractMethods?: string[] } => { const am = [...methods.entries()].filter(([, s]) => s.abstract === true).map(([n]) => n); return am.length > 0 ? { abstractMethods: am } : {}; })(), ...(inst ? { genericOf: inst.family.def.name } : {}), loc: locOf(decl), }, fields, fieldOrder, methods, ...(methodEntryFences.size > 0 ? { methodEntryFences } : {}), decl, ...(emitOverride !== undefined ? { emitOverride } : {}), ctor, ctorParams, ...(paramProps.length > 0 ? { paramProps } : {}), base, ...(callableBase ? { callableBase } : {}), subclasses: [], throwingSetters, staticFields, ...(staticMethods.size > 0 ? { staticMethods } : {}), ...(staticBlocks.length > 0 ? { staticBlocks } : {}), ...(symbolFields.size > 0 ? { symbolFields } : {}), ...(symbolMethods.size > 0 ? { symbolMethods } : {}), ...(classDecoratorNodes.length > 0 ? { classDecorators: { nodes: classDecoratorNodes } } : {}), ...(deferredInitFields.size > 0 ? { deferredInitFields } : {}), }; // GENERIC members get their declaring-class backlink now that the // info exists (instance lowering reads it for `this` typing and the // generic-class binding merge). if (genericMethods.size > 0) { for (const gm of genericMethods.values()) gm.member = { cls: info, kind: "method" }; info.genericMethods = genericMethods; } if (genericStatics.size > 0) { for (const gs of genericStatics.values()) gs.member = { cls: info, kind: "static" }; info.genericStatics = genericStatics; } if (inst) { info.genericInstance = { family: inst.family, bindings: inst.bindings, typeArgsText: inst.typeArgsText, ordinal: inst.ordinal, }; } if (mixin) { info.mixinInstance = { call: mixin.call, bindings: mixin.bindings, context: mixin.context, ordinal: mixin.ordinal, ...(mixinForwarding ? { forwardingCtor: true } : {}), }; } if (familyMode) { const typeParams: ts.Symbol[] = []; for (const tp of decl.typeParameters!) { const sym = lowerer.checker.getSymbolAtLocation(tp.name); if (!sym) lowerer.unsupported("SC1090", tp, "this type parameter form"); typeParams.push(sym); } info.generic = { decl: decl as ts.ClassDeclaration, baseName: decl.name?.text ?? "%anon", typeParams, family: info, instances: new Map(), }; lowerer.genericClassByDecl.set(decl, info.generic); } if (base) base.subclasses.push(info); lowerer.classes.set(className, info); if (computedBase && !functionLocalClass(decl)) { info.def.baseValueGlobal = `%g.${className}.computedBase`; lowerer.globalsList.push({ id: info.def.baseValueGlobal, name: `${className} base`, type: computedBase.value.type, mutable: false }); } if (callableBase && !functionLocalClass(decl)) { lowerer.globalsList.push( { id: callableBase.constructorId, name: `${className} base`, type: DYN, mutable: false }, { id: callableBase.prototypeId, name: `${className} base prototype`, type: DYN, mutable: false }, ); } // A NAMED class binds its name (declarations in their scope, class // expressions inside their own bodies — tsc resolves both to this // symbol); a nameless default-export declaration binds its module's // default-export symbol; anonymous expressions have nothing to bind. // Instantiations bind nothing — the FAMILY owns the symbol. A mixin // instantiation binds nothing either: the inner class's name would // alias EVERY instantiation (self-references by name inside mixin // classes fence at their use sites). const classSymbol = inst || mixin ? undefined : decl.name ? lowerer.checker.getSymbolAtLocation(decl.name) : ts.isClassDeclaration(decl) ? declSymbolOf(lowerer, decl) : undefined; if (classSymbol) lowerer.classBySymbol.set(classSymbol, info); // Static-field storage registers with the module's globals only // once the whole shape collected (a poisoned class never leaves a // half-registered global behind). for (const f of staticFields) { lowerer.globalsList.push({ id: f.globalId, name: `${info.def.jsName ?? className}.${f.name}`, type: f.type, mutable: !f.readonly }); } } } /** mapType's generic-class hook: the INSTANTIATION a concrete type * reference (`Box`) names — registered on first demand. The * instance's NAME reserves its key before the shape collects, so * self-referential layouts (`next: Box | null`) re-enter here and * take the name without recursing; a poisoned collection (a field type * with no lowering under these bindings — the diagnostic carries the * instantiation context) leaves the entry poisoned and the type * unmapped, the fenced-JS-class story. Null answers (unmappable type * arguments, the instance cap, an uncollected family) make the whole * reference unmappable — per-site diagnostics own the fence. */ export function genericClassInstanceType(lowerer: Lowerer, decl: ts.ClassLikeDeclaration, ref: ts.Type, mapArgument?: (type: ts.Type) => IrType | null): IrType | null { const gci = lowerer.genericClassByDecl.get(decl); if (!gci) { // The family never collected (a deferred/poisoned declaration): the // pre-generics answer — the class's own name, unregistered, so dead // storage prunes (typeNamesUnregisteredClass) and live references // flush the declaration's deferred diagnostics (moduleArtifacts / // the validator backstop). Exactly the fenced-class story. return { kind: "object", className: lowerer.classNamer(decl) }; } // A degenerate reference collapses to the FAMILY's object type instead // of going unmapped: `Box` under a static build, wilder arguments // no instantiation can carry (`X<() => T>`), and the instance cap. // The family is nominal Box-ness with only the INHERITED layout: no // value can be CONSTRUCTED at such a type (construction resolves // instantiations and fences), real instantiations may UPCAST into its // slots (the ancestor rule — `let b: Box = new Box(1)`), interval // instanceof answers for the whole family, inherited concrete fields // read through the shared prefix, and every per-instantiation member // keeps a named per-site fence. const familyT: IrType = { kind: "object", className: gci.family.def.name }; // The checker appends `this` (and outer type parameters) to // getTypeArguments — only the declaration's own count participates. const args = lowerer.checker.getTypeArguments(ref as ts.TypeReference).slice(0, gci.typeParams.length); const mapped: IrType[] = []; if (args.length === gci.typeParams.length) { for (const a of args) { const m = lowerer.mapTypeOf(a, mapArgument); if (!m || m.kind === "void") { // An UNBOUND type parameter argument (`Box` outside any // instantiation) stays honestly unmapped — nothing concrete is // being named; everything else degrades to the family. return a.flags & ts.TypeFlags.TypeParameter ? null : familyT; } mapped.push(m); } } else { // No argument list — the `this` TYPE inside the generic class's own // body (`this.v = v` types the receiver as `this`, not a reference). // Inside an instantiation the CURRENT bindings are the arguments; // anywhere else the reference is honestly unmappable. for (const tp of gci.typeParams) { const b = lowerer.typeParamBindings?.get(tp); if (!b) return null; mapped.push(b); } } const key = mapped.map(typeKey).join(","); const existing = gci.instances.get(key); if (existing) { if (existing.info) lowerer.noteGenericClassInstanceDemand(existing.info); return existing.poisoned ? null : { kind: "object", className: existing.name }; } // The generic-fn cap, same rationale (polymorphic recursion through // class fields would mint instances forever). mapType has no // diagnostic channel — the family answer keeps the site compilable // where the OBJECT itself is never touched; touched members fence. if (gci.instances.size >= MAX_GENERIC_INSTANCES) return familyT; const ordinal = gci.instances.size; const name = `${gci.family.def.name}%${ordinal}`; const entry: { name: string; info: ClassInfo | null; poisoned?: boolean } = { name, info: null }; gci.instances.set(key, entry); const bindings = new Map(); gci.typeParams.forEach((tp, i) => bindings.set(tp, mapped[i]!)); const rendered = mapped.map((m) => lowerer.fmt(m)).join(", "); const typeArgsText = `<${rendered.length > 80 ? rendered.slice(0, 77) + "..." : rendered}>`; const prevBindings = lowerer.typeParamBindings; const prevContext = lowerer.instantiationContext; lowerer.typeParamBindings = bindings; lowerer.instantiationContext = `instantiating class '${gci.baseName}' with ${typeArgsText}`; try { lowerer.collectClassShapeInner(decl, undefined, { family: gci.family, name, bindings, typeArgsText, ordinal }); } catch (e) { // Collection fenced under THESE bindings: the diagnostic (with the // instantiation context) is recorded; the type stays unmapped. if (!(e instanceof PoisonError)) throw e; entry.poisoned = true; return null; } finally { lowerer.typeParamBindings = prevBindings; lowerer.instantiationContext = prevContext; } const info = lowerer.classes.get(name); if (!info) { entry.poisoned = true; return null; } entry.info = info; lowerer.genericClassInstances.push(info); lowerer.noteGenericClassInstanceDemand(info); lowerer.onLateClassCollected?.(info); return { kind: "object", className: name }; } /** Runs a member-lowering thunk under an INSTANTIATION's type-parameter * bindings (the generic-fn typeParamResolver mechanism) — the checker * keeps reporting the unsubstituted `T`s inside the shared body AST. * Coverage counts a generic class's statements once: only the FIRST * instantiation contributes (the lowerGenericInstance rule). A no-op * for ordinary classes. */ function withInstanceBindings(lowerer: Lowerer, info: ClassInfo, fn: () => T): T { const gi = info.genericInstance; if (!gi) { // MIXIN instantiations ride the same mechanism: T (the base // parameter's type parameter) resolves to the argument's classval, // fences carry the instantiation context, and only the first // instantiation of a mixin's class counts toward coverage. const mi = info.mixinInstance; if (!mi) return fn(); const prevBindings = lowerer.typeParamBindings; const prevContext = lowerer.instantiationContext; const prevSuppress = lowerer.suppressStats; const prevMixinCtx = lowerer.mixinTypeContext; lowerer.typeParamBindings = mi.bindings; lowerer.instantiationContext = mi.context; lowerer.suppressStats = prevSuppress || mi.ordinal > 0; lowerer.mixinTypeContext = { classNode: info.decl!, className: info.def.name }; try { return fn(); } finally { lowerer.typeParamBindings = prevBindings; lowerer.instantiationContext = prevContext; lowerer.suppressStats = prevSuppress; lowerer.mixinTypeContext = prevMixinCtx; } } const prevBindings = lowerer.typeParamBindings; const prevContext = lowerer.instantiationContext; const prevSuppress = lowerer.suppressStats; lowerer.typeParamBindings = gi.bindings; lowerer.instantiationContext = `instantiating class '${gi.family.generic?.baseName ?? info.def.jsName ?? ""}' with ${gi.typeArgsText}`; lowerer.suppressStats = prevSuppress || gi.ordinal > 0; try { return fn(); } finally { lowerer.typeParamBindings = prevBindings; lowerer.instantiationContext = prevContext; lowerer.suppressStats = prevSuppress; } } /** The `%init` statements for one class's static readonly fields AND * static blocks, interleaved in member order — emitted at the class * statement's source position (see lowerFileInit's merge), exactly when * JS evaluates static initializers and blocks. Field failures poison per * field, like fieldInitStmts; a block lowers as the block statement it * is, so its statements poison individually inside lowerStmts. */ export function lowerStaticFieldInits(lowerer: Lowerer, info: ClassInfo): IrStmt[] { if (info.localClass) return []; // Mixin instantiations lower their initializers under the // instantiation's bindings/context (a no-op for everything else — // generic FAMILIES own their statics and carry no genericInstance). return withInstanceBindings(lowerer, info, () => lowerStaticFieldInitsInner(lowerer, info)); } function lowerStaticFieldInitsInner(lowerer: Lowerer, info: ClassInfo): IrStmt[] { // Decoration first: TC39 evaluates decorator expressions, creates the // class, applies the decorators, and only THEN runs static field // initializers and static blocks (verified against Node — the // decorated result is what `this`/the class name mean inside them). const out: IrStmt[] = [...lowerClassDecoration(lowerer, info)]; const computedBase = info.decl ? lowerer.computedClassBases.get(lowerer.classNamer(info.decl)) : undefined; if (computedBase && info.def.baseValueGlobal) { const loc = computedBase.value.loc; out.push({ kind: "assign", localId: info.def.baseValueGlobal, value: computedBase.value, loc }); out.push({ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.classInherit", args: [ lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl!), DYN), lowerer.coerceToExpected({ kind: "varRef", localId: info.def.baseValueGlobal, type: computedBase.value.type, loc }, DYN), ], type: DYN, loc }, loc }); } if (info.callableBase) { const { expression, constructorId, prototypeId } = info.callableBase; const loc = locOf(expression); out.push( { kind: "assign", localId: constructorId, value: info.decl && lowerer.computedCallableBases.has(info.decl) ? lowerer.coerceToExpected(lowerer.computedCallableBases.get(info.decl)!, DYN) : lowerer.lowerExprExpecting(expression, DYN), loc }, { kind: "assign", localId: prototypeId, value: { kind: "libCall", fn: "dyn.classBasePrototype", args: [{ kind: "varRef", localId: constructorId, type: DYN, loc }], type: DYN, loc }, loc }, ); const prototype = classPrototypeData(lowerer, info, loc); if (prototype) out.push({ kind: "exprStmt", expr: prototype, loc }); out.push({ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.classInherit", args: [ lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl!), DYN), { kind: "varRef", localId: constructorId, type: DYN, loc }, ], type: DYN, loc }, loc }); } type Item = | { pos: number; kind: "field"; f: ClassInfo["staticFields"][number] } | { pos: number; kind: "block"; b: ts.ClassStaticBlockDeclaration }; const items: Item[] = [ ...info.staticFields.map((f): Item => ({ pos: f.initializer.getStart(), kind: "field", f })), ...(info.staticBlocks ?? []).map((b): Item => ({ pos: b.getStart(), kind: "block", b })), ].sort((a, b) => a.pos - b.pos); for (const item of items) { if (item.kind === "block") { // The block's body IS a Block statement: lowerStmts scopes its // let/const like any nested block and poisons per inner statement. out.push(...lowerer.lowerStmts([item.b.body])); continue; } const f = item.f; lowerer.stats.statementsTotal++; lowerer.bumpFileStat(locOf(f.initializer).file, "total"); const diagsBefore = lowerer.diags.length; try { // `this` in a static field initializer names the CLASS (like a // static block's), with arrows transparent and this-binding // function forms opaque — the static-block rule verbatim, named // here so the generic outside-a-method fence never fires first. rejectStaticThis( lowerer, f.initializer, () => "'this' in static field initializers (it names the class — reference the class by name instead)", true, ); const value = lowerer.lowerExprExpecting(f.initializer, f.type); out.push({ kind: "assign", localId: f.globalId, value, loc: locOf(f.initializer) }); } catch (e) { if (!(e instanceof PoisonError)) throw e; lowerer.stats.statementsFailed++; lowerer.bumpFileStat(locOf(f.initializer).file, "failed"); if (isJsSourceFile(f.initializer.getSourceFile()) && lowerer.diagSink === null) { const fence = lowerer.deferToRuntimeFence(diagsBefore, f.initializer, { kind: "statement" }); if (fence) out.push(fence); } } } return out; } /** Post-collection analysis of a decorated class (all shapes registered — * a decorator's return type may name a subclass declared BELOW the * class). Classifies every class-level decorator by its checker type: * exactly one parameter, itself a classval the decorated class legally * flows into (the class, or a base sharing its completed constructor * ABI — the classval widening rule), returning void/undefined (an * effect-only decorator) or the class/a same-ABI subclass (a REPLACING * decorator, whose result rebinds the name). Everything else is a named * fence: the standard context parameter, structural sibling * replacements tsc admits but the nominal classval world cannot carry, * unions mixing the class with undefined. A replacing decorator * registers the mutable classval global the name rebinds through, and * fences the two shapes the value rebinding cannot keep exact — * subclasses of the decorated class (the compiled hierarchy is fixed at * build time; the runtime base would be the decoration result) and * namespace-nested declarations (qualified references resolve the class * directly, not through the rebound value). */ export function analyzeClassDecoration(lowerer: Lowerer, info: ClassInfo): void { const cd = info.classDecorators; if (!cd || cd.shapes !== undefined || cd.poisoned) return; const display = info.def.jsName ?? info.def.name; try { const shapes: NonNullable = []; for (const d of cd.nodes) { // An ambient declaration NOTHING defines (`declare let dec: any`, // `declare function dec(t: T): T` — the conformance corpus's // dominant decorator shape): Node erases it, so the decorator // EXPRESSION itself throws ReferenceError when the class statement // evaluates. That is runnable semantics, not a fence — the // undefRead story (the ambient `declare const` stance verbatim). // Factory spellings ride along: `@dec("x")` evaluates the CALLEE // before any argument, so the ReferenceError is still the first // observable effect. const ambientCallee = ts.isCallExpression(d.expression) && ts.isIdentifier(d.expression.expression) ? d.expression.expression : ts.isIdentifier(d.expression) ? d.expression : null; if (ambientCallee !== null) { const sym = lowerer.resolveValueSymbol(ambientCallee); const vdecl = sym && !lowerer.isStdlibSymbol(sym) ? lowerer.checker.declarationsOf(sym)[0] : undefined; const ambientVar = vdecl !== undefined && ts.isVariableDeclaration(vdecl) && vdecl.initializer === undefined && (ts.getCombinedModifierFlags(vdecl) & ts.ModifierFlags.Ambient) !== 0 && !vdecl.getSourceFile().isDeclarationFile; if (ambientVar || ambientUndefinedFnSymbolOf(lowerer, ambientCallee) !== null) { shapes.push({ kind: "ambientThrow", name: ambientCallee.text }); continue; } } const t = lowerer.typeOf(d.expression); // Param-count first, off the checker signature: the context-taking // shape deserves its own name before mapType (whose failure on // ClassDecoratorContext would blur the story). const sigs = lowerer.checker.getCallSignatures(t); if (sigs.length === 1 && sigs[0]!.getParameters().length > 1) { lowerer.unsupported( "SC1090", d, "class decorators that take the standard 'context' parameter (its object — addInitializer, metadata — has no static lowering; single-parameter decorators compile)", ); } const mapped = lowerer.mapTypeOf(t); if (!mapped || mapped.kind !== "func" || mapped.rest === true || mapped.params.length > 1) { lowerer.unsupported( "SC1090", d, "class decorators without one concrete (class) => class-or-void signature ('any'-typed and generic decorators have no compilable call ABI — declare the parameter as the class type)", ); } // The parameter: a classval slot the decorated class's object can // legally inhabit — the class itself, or a BASE with the same // completed constructor ABI (the classval widening rule). if (mapped.params.length === 1) { const p = mapped.params[0]!; const paramOk = p.kind === "classval" && (p.className === info.def.name || (isSubclassOf(lowerer, info.def.name, p.className) && (() => { const sup = lowerer.classes.get(p.className); return sup !== undefined && !sup.generic && ctorAbiEquals(lowerer, info, sup); })())); if (!paramOk) { lowerer.unsupported( "SC1090", d, `class decorators whose parameter is not the decorated class ('${display}' cannot flow into a '${lowerer.fmt(p)}' slot — declare the parameter as 'typeof ${display}' or a base class sharing its constructor signature)`, ); } } // The return: void/undefined keeps the original binding; the class // itself or a same-ABI SUBCLASS is a legal replacement (a classval // of the decorated class per the flow rule). tsc also admits // structurally-compatible siblings and bases — the nominal classval // world cannot carry those, so they fence by name. const ret = mapped.ret; const replaces = ret.kind === "classval"; if (replaces) { const retOk = ret.className === info.def.name || (isSubclassOf(lowerer, ret.className, info.def.name) && (() => { const sub = lowerer.classes.get(ret.className); return sub !== undefined && !sub.generic && ctorAbiEquals(lowerer, sub, info); })()); if (!retOk) { lowerer.unsupported( "SC1090", d, `class decorators returning '${lowerer.fmt(ret)}' (a replacement must be '${display}' itself or a subclass sharing its constructor signature — tsc's structural check admits shapes the compiled nominal hierarchy cannot rebind)`, ); } } else if (ret.kind !== "void") { lowerer.unsupported( "SC1090", d, `class decorators returning '${lowerer.fmt(ret)}' (supported returns: the decorated class type, a subclass with the same constructor signature, or void)`, ); } shapes.push({ kind: "call", funcType: mapped, replaces }); } if (shapes.some((s) => s.kind === "call" && s.replaces)) { // The name can rebind at runtime: every reference must route // through the decoration result. Two shapes cannot: a compiled // subclass (its base pointer, vtable prefix, and interval are // fixed at build time, but JS would extend the decoration result) // and namespace-nested declarations (the qualified-access paths // resolve the class directly, not through the rebound binding). if (info.subclasses.length > 0) { lowerer.unsupported( "SC1090", cd.nodes[0]!, `class decorators that can replace a class with subclasses ('${info.subclasses[0]!.def.jsName ?? info.subclasses[0]!.def.name}' extends '${display}', but the runtime base would be the decoration result — return void, or decorate the leaf classes)`, ); } if (info.decl && !ts.isSourceFile(info.decl.parent)) { lowerer.unsupported( "SC1090", cd.nodes[0]!, "class decorators that can replace a namespace-nested class (qualified references resolve the declaration directly — return void, or declare the class at top level)", ); } const globalId = `%g.dec.${info.def.name}`; lowerer.globalsList.push({ id: globalId, name: `${display}.decorated`, type: { kind: "classval", className: info.def.name }, mutable: true, }); cd.valueGlobalId = globalId; } cd.shapes = shapes; } catch (e) { if (!(e instanceof PoisonError)) throw e; cd.poisoned = true; } } /** The decoration statements of a decorated class — the %init code that * runs at the class statement's position, BEFORE its static field * initializers and blocks (lowerStaticFieldInits composes them; the * lower-modules interleave places the whole bundle). Verified Node * order: decorator expressions evaluate in SOURCE order (factories run * here), then applications run in REVERSE member order over the class * object, each replacing decorator's non-undefined result feeding the * next application; the final value binds the class name (the mutable * classval global) when any decorator can replace. */ function lowerClassDecoration(lowerer: Lowerer, info: ClassInfo): IrStmt[] { const cd = info.classDecorators; if (!cd || cd.poisoned || cd.shapes === undefined || info.decl === null) return []; const loc = locOf(info.decl); const stmts: IrStmt[] = []; try { // 1. Decorator expressions evaluate in source order, into hidden // locals — a later factory's side effects must not precede an // earlier one's, and every expression evaluates before any applies. // An ambient (never-defined) decorator name throws Node's // ReferenceError HERE: earlier expressions still evaluate, nothing // after — expression, application, or the class's own static // initializers — ever runs (the %init unwinds). const temps: { localId: string; funcType: Extract; replaces: boolean }[] = []; for (let i = 0; i < cd.nodes.length; i++) { const d = cd.nodes[i]!; const shape = cd.shapes[i]!; if (shape.kind === "ambientThrow") { stmts.push({ kind: "exprStmt", expr: nsUndefRead(lowerer, shape.name, d, F64), loc: locOf(d) }); return stmts; } const value = lowerer.lowerExprExpecting(d.expression, shape.funcType); const local = lowerer.declareHiddenLocal("dec", shape.funcType); stmts.push({ kind: "varDecl", localId: local.id, init: value, loc: locOf(d) }); temps.push({ localId: local.id, funcType: shape.funcType, replaces: shape.replaces }); } // 2. Applications, reverse order, over the accumulating class value. let current: IrExpr = classValueRef(lowerer, info, info.decl); for (let i = temps.length - 1; i >= 0; i--) { const t = temps[i]!; const dLoc = locOf(cd.nodes[i]!); const callee: IrExpr = { kind: "varRef", localId: t.localId, type: t.funcType, loc: dLoc }; const args: IrExpr[] = []; if (t.funcType.params.length === 1) { const p = t.funcType.params[0]!; const widened = lowerer.coerceToExpected(current, p); lowerer.requireExactShape(cd.nodes[i]!, widened.type, p); args.push(widened); } const call: IrExpr = { kind: "callValue", callee, args, type: t.funcType.ret, loc: dLoc }; if (t.replaces) { const target: IrType = { kind: "classval", className: info.def.name }; const widened = lowerer.coerceToExpected(call, target); lowerer.requireExactShape(cd.nodes[i]!, widened.type, target); const res = lowerer.declareHiddenLocal("decres", target); stmts.push({ kind: "varDecl", localId: res.id, init: widened, loc: dLoc }); current = { kind: "varRef", localId: res.id, type: target, loc: dLoc }; } else { stmts.push({ kind: "exprStmt", expr: call, loc: dLoc }); } } // 3. The binding: TC39 rebinds the class name to the final result. if (cd.valueGlobalId !== undefined) { stmts.push({ kind: "assign", localId: cd.valueGlobalId, value: current, loc }); } return stmts; } catch (e) { if (!(e instanceof PoisonError)) throw e; lowerer.stats.statementsFailed++; lowerer.bumpFileStat(loc.file, "failed"); return []; } } /** The nearest declaration of static member `name` at or above `info` — * the compile-time prototype-chain walk (`D.x` reads C's global when C * declared x and nothing between redeclares it; a redeclaration shadows * with its OWN storage, exactly JS). */ export function findStaticOn(lowerer: Lowerer, info: ClassInfo | null, name: string): | { declarer: ClassInfo; field: ClassInfo["staticFields"][number]; method?: undefined } | { declarer: ClassInfo; method: { params: ParamShape[]; ret: IrType; member: ts.MethodDeclaration; receiverInfo?: GenericFnInfo }; field?: undefined } | null { for (let c = info; c; c = c.base) { const field = c.staticFields.find((s) => s.name === name); if (field) return { declarer: c, field }; const method = c.staticMethods?.get(name); if (method) return { declarer: c, method }; } return null; } /** True when some STRICT descendant of `info` redeclares static `name` — * the through-a-VALUE devirtualization test: a classval(info) slot can * hold any descendant, and a shadowing redeclaration means the runtime * class decides which storage answers. */ export function staticShadowBelow(lowerer: Lowerer, info: ClassInfo, name: string): boolean { return info.subclasses.some( (s) => s.staticFields.some((f) => f.name === name) || s.staticMethods?.has(name) === true || s.genericStatics?.has(name) === true || staticShadowBelow(lowerer, s, name), ); } /** The nearest GENERIC static declaration of `name` at/above `info` — * findStaticOn's twin over the genericStatics tables. */ export function findGenericStaticOn(lowerer: Lowerer, info: ClassInfo | null, name: string,): { declarer: ClassInfo; info: GenericFnInfo } | null { for (let c = info; c; c = c.base) { const gs = c.genericStatics?.get(name); if (gs) return { declarer: c, info: gs }; } return null; } /** A static METHOD taken as a value: the zero-capture closure over its * module function — the declared-function-as-value rule verbatim * (interned by the backend, so `C.m === C.m` holds). */ function staticMethodValue(lowerer: Lowerer, declarer: ClassInfo, name: string, sig: { params: ParamShape[]; ret: IrType }, blame: ts.Expression, loc: SrcLoc): IrExpr { const fnName = `%${declarer.def.name}.static:${name}`; lowerer.noteEdge(fnName); const funcType: IrType = { kind: "func", params: sig.params.filter((p) => p.mode !== "dynRest").map((p) => p.type), ret: sig.ret, ...(sig.params.some((p) => p.mode === "dynRest") ? { rest: true as const } : {}), }; lowerer.requireExactArityValue(blame, blame, sig.params, funcType); return { kind: "closure", fnName, captures: [], type: funcType, loc }; } /** The class itself taken as a VALUE (`const X = C`, an argument, an * array element, a class expression's result): the classRef over the * per-class immortal class object. The construct thunk needs a thunk- * shaped constructor, so classes whose construction is libCall-shaped — * the runtime-provided builtins and anything inheriting a builtin * constructor (Error/EventEmitter/stream chains complete their `new` * by special rules) — are named fences here. The constructor edge is * noted at every classRef: a value can always be constructed through. */ export function classValueRef(lowerer: Lowerer, info: ClassInfo, blame: ts.Node): IrExpr { const display = info.def.name.replace(/^%|^%m\d+\./, ""); fenceDecorationThrows(lowerer, info, blame); if (info.localClass && ts.isIdentifier(blame)) { const stored = lowerer.resolveLocal(blame); if (stored) return lowerer.coerceToExpected({ kind: "varRef", localId: stored.id, type: stored.type, loc: locOf(blame) }, { kind: "classval", className: info.def.name }); } if (info.localClass && !ts.isClassExpression(blame) && !ts.isClassDeclaration(blame)) { lowerer.unsupported("SC1090", blame, "named self references inside function-local classes"); } if (info.generic) { // `typeof Box` — the uninstantiated FAMILY as a value: no thunk, no // single constructor ABI. INSTANTIATIONS have class objects // (`const B = Box`, `new (v: number) => Box` slots). lowerer.unsupported( "SC1090", blame, `generic classes as values ('typeof ${display}' keeps the type parameter — instantiation expressions ('${display}') and concrete constructor-typed slots compile)`, ); } if (info.builtinError || info.builtinEmitter || info.builtinStream !== undefined) { lowerer.unsupported( "SC1090", blame, `builtin classes as values ('${display}' is runtime-provided — reference program-declared classes instead)`, ); } if ( lowerer.inheritsBuiltinEmitterCtor(info) || inheritsBuiltinStreamCtor(lowerer, info) || (() => { for (let c = info.base; c; c = c.base) if (c.builtinEmitter || c.builtinStream !== undefined) return true; return false; })() ) { lowerer.unsupported( "SC1090", blame, `classes extending builtin bases as values ('${display}' inherits a runtime-provided constructor)`, ); } lowerer.noteEdge(`%${info.def.name}.constructor`); return { kind: "classRef", className: info.def.name, type: { kind: "classval", className: info.def.name }, loc: locOf(blame), }; } /** Constructor-ABI equality — the classval widening rule: a classval(D) * value may flow into a classval(C) slot only when D's completed * constructor signature equals C's (same count, modes, and ABI types), * which is what keeps newValue completion against C's one signature * sound for every value legally in the slot. */ export function ctorAbiEquals(lowerer: Lowerer, sub: ClassInfo, sup: ClassInfo): boolean { const a = sub.ctorParams; const b = sup.ctorParams; return a.length === b.length && a.every((p, i) => p.mode === b[i]!.mode && typeEquals(p.type, b[i]!.type)); } /** Writable storage for an own static field named through its exact class. * Inherited writes create a new subclass property in JS, so they must not * update the declaring class's global. */ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAccessExpression): { id: string; type: IrType } | null { if (access.questionDotToken || (!ts.isIdentifier(access.expression) && access.expression.kind !== ts.SyntaxKind.ThisKeyword)) return null; const info = lowerer.exactClassOfReceiver(access.expression); if (!info) return null; const found = lowerer.findStaticOn(info, access.name.text); if (!found?.field) return null; if (found.declarer !== info) { lowerer.unsupported("SC1090", access, `assigning the inherited static '${access.name.text}' through a subclass name (JS creates an OWN property on the subclass — assign through '${found.declarer.def.jsName ?? found.declarer.def.name}' instead)`); } if (found.field.readonly) { lowerer.unsupported("SC1090", access, `assigning the readonly static '${access.name.text}'`); } return { id: found.field.globalId, type: found.field.type }; } /** `C.x` where C is a class declared in the program and x a static * member of its chain: field reads are the module global, static * methods become interned closures, and `.name` folds to the class's * compile-time name. Null for everything else — unresolved members * fall through to the ordinary chain so the static fence or the * generic member rejection names the site. */ export function lowerStaticFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null { if (expr.name.text === "prototype") return null; if (expr.questionDotToken) return null; if (!ts.isIdentifier(expr.expression) && expr.expression.kind !== ts.SyntaxKind.ThisKeyword) return null; const symbol = ts.isIdentifier(expr.expression) ? lowerer.resolveValueSymbol(expr.expression) : null; const info = (symbol ? lowerer.classBySymbol.get(symbol) : undefined) ?? // A require binding over `module.exports = class {…}` (the alias // lands on the expression's own symbol): resolve/collect on demand, // or `C.name` below would fall through to paths that answer for // stdlib globals instead of this class. propertyAssignedClassInfoOf(lowerer, symbol) ?? exactClassOfReceiver(lowerer, expr.expression) ?? undefined; if (!info) return null; // A decorated name that can REBIND (a replacing decorator): the // receiver is the decoration result, not the declaration — fall // through to the through-a-VALUE paths (lowerClassValueProperty), // whose devirtualization and .name rules answer for every legal // runtime value. if (info.classDecorators?.valueGlobalId !== undefined) return null; const loc = locOf(expr); const found = findStaticOn(lowerer, info, expr.name.text); if (hasRuntimeStatics(info)) return { kind: "dynKeyGet", value: lowerer.coerceToExpected(classValueRef(lowerer, info, expr.expression), DYN), key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, type: DYN, loc }; // A #private static resolves only through the DECLARING class's own // name: in JS the brand lives on that one constructor object, so // `D.#s` (a subclass receiver) throws Node's TypeError instead of // reaching up the chain — fenced rather than silently resolved. if (found && expr.name.text.startsWith("#") && found.declarer !== info) { lowerer.unsupported( "SC1090", expr, `reading the private static '${expr.name.text}' through the subclass '${info.def.name.replace(/^%|^%m\d+\./, "")}' (JS brands the declaring class object alone — Node throws a TypeError here; spell the declaring class's name)`, ); } if (found?.field !== undefined) { return lowerer.maybeNarrow( { kind: "varRef", localId: found.field.globalId, type: found.field.type, loc }, expr, ); } if (found) { return staticMethodValue(lowerer, found.declarer, expr.name.text, found.method, expr, loc); } // A GENERIC static method as a VALUE: the pinned-value rule verbatim // (lowerGenericFnValue) — a slot spelling one concrete signature names // an instance, an unpinned reference fences by name. { const gfound = findGenericStaticOn(lowerer, info, expr.name.text); if (gfound) return lowerer.lowerGenericFnValue(expr, gfound.info); } // `C.name` — the JS-observable class name, a compile-time constant on // the direct spelling (tsc rejects user statics named `name`, so the // chain above can never shadow it in TypeScript sources). if (expr.name.text === "name" && info.def.jsName !== undefined) { return { kind: "strLit", value: info.def.jsName, type: STRING, loc }; } if (info.callableBase) return { kind: "dynKeyGet", value: lowerer.coerceToExpected(classValueRef(lowerer, info, expr.expression), DYN), key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, type: DYN, loc }; const data = classStaticDataFor(lowerer, info, expr.name.text, loc); if (data) return { kind: "dynKeyGet", value: data, key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, type: DYN, loc }; return null; } /** NamedEvaluation's answer for a class expression's `.name`: its own * declared name, else the binding name when the expression is the * direct initializer of a variable declaration / the RHS of a simple * assignment / an object-literal property value / a default parameter — * "" everywhere else (array elements, call arguments). Verified against * Node for each shape. */ function namedEvaluationName(expr: ts.ClassLikeDeclaration): string { if (expr.name) return expr.name.text; let p: ts.Node | undefined = expr.parent; while (ts.isParenthesizedExpression(p)) p = p.parent; if (ts.isVariableDeclaration(p) && ts.isIdentifier(p.name) && p.initializer !== undefined) return p.name.text; if (ts.isBinaryExpression(p) && p.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(p.left)) return p.left.text; if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name)) return p.name.text; if (ts.isParameter(p) && ts.isIdentifier(p.name)) return p.name.text; return ""; } /** Collect a class expression once per enclosing specialization. Function-local * evaluations allocate fresh constructor objects; top-level expressions use * immortal templates. Static initialization remains limited to whole top-level * initializers, where pendingClassExprInits preserves its evaluation point. */ export function lowerClassExpressionInfo(lowerer: Lowerer, expr: ts.ClassLikeDeclaration): ClassInfo { const cached = lowerer.exprClassInfoByName.get(lowerer.classNamer(expr)); if (cached) return cached; // Reentrancy guard: heritage resolution can DEMAND another class // expression's collection (extends through property assignments — // propertyAssignedClassInfoOf), so a cyclic base chain would re-enter // its own collection here. The tsc gate rejects every such cycle it // can see (TS2506/TS2303 — direct, indirect, and cross-file require // cycles all probed); this fence is the backstop that turns anything // it misses into a diagnostic instead of a stack overflow. if (lowerer.collectingExprClasses.has(expr)) { lowerer.unsupported( "SC1090", expr, "class expressions whose extends chain re-enters their own collection (a cyclic base through property assignments)", ); } let local = false; for (let p: ts.Node | undefined = expr.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) { if (ts.isClassStaticBlockDeclaration(p)) { lowerer.unsupported( "SC1090", expr, "class expressions inside static blocks", ); } if (ts.isFunctionLike(p)) local = true; } if (local && expr.members.some((member) => member.name && ts.isPrivateIdentifier(member.name))) { lowerer.unsupported("SC1090", expr, "function-local classes with private brands"); } if (local && (expr.typeParameters?.length || decoratorNodesOf(expr).length || expr.members.some((member) => decoratorNodesOf(member).length || (!isJsSourceFile(expr.getSourceFile()) && (ts.isClassStaticBlockDeclaration(member) || ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword))) || (ts.isMethodDeclaration(member) && member.typeParameters?.length)))) { lowerer.unsupported("SC1090", expr, "function-local classes with statics, decorators, or generic members"); } lowerer.collectingExprClasses.add(expr); try { if (local && lowerer.fnStack.length > 0 && !lowerer.computedClassBases.has(lowerer.classNamer(expr))) { const heritage = expr.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword)?.types[0]?.expression; if (heritage && (ts.isCallExpression(heritage) || ts.isIdentifier(heritage) && (lowerer.peekLocal(heritage) ?? lowerer.globalOf(heritage)) != null)) { let value = lowerer.lowerExpr(heritage); const known = value.type.kind === "dyn" ? storedClassValueType(lowerer, heritage) : null; if (known?.kind === "classval") value = lowerer.coerceToExpected(value, known); const base = value.type.kind === "classval" ? lowerer.classes.get(value.type.className) : undefined; if (base) lowerer.computedClassBases.set(lowerer.classNamer(expr), { value, classInfo: base }); else if (value.type.kind === "dyn" || value.type.kind === "func") lowerer.computedCallableBases.set(expr, value); } } lowerer.collectClassShapeInner(expr, namedEvaluationName(expr)); } finally { lowerer.collectingExprClasses.delete(expr); } const info = lowerer.classes.get(lowerer.classNamer(expr)); if (!info) throw new PoisonError(); // collection poisoned and reported if (local) { info.def.localCaptures = []; info.def.fields.push({ name: `%classEnvironment:${info.def.name}`, type: { kind: "classval", className: info.def.name } }); info.localClass = { context: null, bodies: new Map(), captureSources: [], ready: false }; if (info.base?.localClass) { const baseExpression = expr.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword)?.types[0]?.expression; if (baseExpression) info.localClass.baseExpression = baseExpression; } } lowerer.exprClassInfoByName.set(info.def.name, info); lowerer.exprClasses.push(info); lowerer.onExprClassCollected?.(info); // Static field initializers and static blocks run when the class // expression EVALUATES. The supported positions evaluate exactly once, // at the top-level statement containing the expression — the pending // buffer lands them immediately before that statement (lowerFileInit // drains it), which is JS's order for whole-initializer positions. // Anything subtler (multi-declarator statements, arguments evaluated // after other side effects) is a named fence, never a reordering. if (info.staticFields.length > 0 || (info.staticBlocks?.length ?? 0) > 0) { let holder: ts.Node | undefined = expr.parent; while ( ts.isParenthesizedExpression(holder) || ts.isClassExpression(holder) || ts.isHeritageClause(holder) || ts.isExpressionWithTypeArguments(holder) ) { holder = holder.parent; } const wholeInit = (ts.isVariableDeclaration(holder) && holder.initializer !== undefined && ts.isVariableDeclarationList(holder.parent) && holder.parent.declarations.length === 1 && ts.isVariableStatement(holder.parent.parent) && ts.isSourceFile(holder.parent.parent.parent)) || (ts.isBinaryExpression(holder) && holder.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isExpressionStatement(holder.parent) && ts.isSourceFile(holder.parent.parent)) || (ts.isExpressionStatement(holder) && ts.isSourceFile(holder.parent)); if (!wholeInit) { lowerer.unsupported( "SC1090", expr, "class expressions with static initializers or static blocks outside a whole-initializer position (their declaration-time code must run exactly where the expression evaluates — bind the class in its own top-level `const C = class …` statement)", ); } lowerer.pendingClassExprInits.push(...lowerer.lowerStaticFieldInits(info)); } return info; } /** `class {…}` in expression position: a class definition bound to no * statement — once the static side is a value, the expression IS the * definition plus a classRef over it. A DECORATED class expression * whose decoration provably throws (the ambient-decorator shape) never * mints a class at all: evaluating the expression IS the * ReferenceError, so it lowers to exactly that read — every evaluation * throws identically, which is why the once-evaluated restriction and * the member fences don't apply. */ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclaration): IrExpr { if ( decoratorNodesOf(expr).length > 0 || expr.members.some((m) => decoratorNodesOf(m).length > 0) ) { if (!isJsSourceFile(expr.getSourceFile()) && expr.typeParameters === undefined) { const thrown = guaranteedDecorationThrow(lowerer, expr); if (thrown) { // The expression's static type never materializes — the read // throws — so the nominal IR type only has to satisfy the // consumer. F64 is the ambient-undefRead convention. return nsUndefRead(lowerer, thrown.name, expr, F64); } } } const info = lowerClassExpressionInfo(lowerer, expr); if (info.localClass && !info.localClass.ready) { const context = newFnCtx(true, null, null, VOID); info.localClass.context = context; lowerer.fnStack.push(context); try { const bodies = lowerClassMembers(lowerer, info); for (const body of bodies) info.localClass.bodies.set(body.name, body); info.def.localCaptures = context.captures!; info.localClass.captureSources = context.captureSources; if (info.localClass.baseExpression && info.base) { info.def.localBaseCapture = info.def.localCaptures.length; info.def.localCaptures.push({ localId: "%base", name: "base", type: { kind: "classval", className: info.base.def.name } }); } info.localClass.ready = true; lowerer.onExprClassCollected?.(info); } finally { lowerer.fnStack.pop(); info.localClass.context = null; } } const value = classValueRef(lowerer, info, expr); if (info.localClass && info.callableBase && value.kind === "classRef") { const loc = locOf(expr); const base = lowerer.lowerExprExpecting(info.callableBase.expression, DYN); const baseLocal = lowerer.declareHiddenLocal("%classBase", DYN); const clsLocal = lowerer.declareHiddenLocal("%classValue", value.type); const baseRef: IrExpr = { kind: "varRef", localId: baseLocal.id, type: DYN, loc }; const clsRef: IrExpr = { kind: "varRef", localId: clsLocal.id, type: value.type, loc }; const boxed: IrExpr = { kind: "dynFrom", value: clsRef, type: DYN, loc }; info.localPrototypeData = true; return initializeRuntimeStatics(lowerer, info, { kind: "seqExpr", stmts: [ { kind: "varDecl", localId: baseLocal.id, init: base, loc }, { kind: "varDecl", localId: clsLocal.id, init: { ...value, captures: info.localClass.captureSources }, loc }, { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.classInherit", args: [boxed, baseRef], type: DYN, loc }, loc }, { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.classPrototype", args: [boxed, { kind: "libCall", fn: "dyn.classBasePrototype", args: [baseRef], type: DYN, loc }, ], type: DYN, loc }, loc }, ], result: clsRef, type: value.type, loc }); } if (info.localClass?.baseExpression && info.base && value.kind === "classRef") { const loc = locOf(expr); const base = lowerer.lowerExprExpecting(info.localClass.baseExpression, { kind: "classval", className: info.base.def.name }); const baseLocal = lowerer.declareHiddenLocal("%classBase", base.type); baseLocal.boxed = true; const clsLocal = lowerer.declareHiddenLocal("%classValue", value.type); const baseRef: IrExpr = { kind: "varRef", localId: baseLocal.id, type: base.type, loc }; const clsRef: IrExpr = { kind: "varRef", localId: clsLocal.id, type: value.type, loc }; const basePrototype = classPrototypeData(lowerer, info.base, loc, baseRef)!; info.localPrototypeData = true; return initializeRuntimeStatics(lowerer, info, { kind: "seqExpr", stmts: [ { kind: "varDecl", localId: baseLocal.id, init: base, loc }, { kind: "varDecl", localId: clsLocal.id, init: { ...value, captures: [...info.localClass.captureSources, baseLocal.id] }, loc }, { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.classInherit", args: [ { kind: "dynFrom", value: clsRef, type: DYN, loc }, { kind: "dynFrom", value: baseRef, type: DYN, loc }, ], type: DYN, loc }, loc }, { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.classPrototype", args: [ { kind: "dynFrom", value: clsRef, type: DYN, loc }, basePrototype, ], type: DYN, loc }, loc }, ], result: clsRef, type: value.type, loc }); } return initializeRuntimeStatics(lowerer, info, info.localClass && value.kind === "classRef" ? { ...value, captures: info.localClass.captureSources } : value); } /** The EXACT class a receiver expression is statically known to BE (not * merely be typed by): the class name itself, or a `const` binding * whose initializer is a class expression / class name. Such receivers * can never hold a subclass at runtime, so static WRITES through them * hit the declaring class's storage exactly (the shadowing hazards of * general class values don't arise). Null for everything else. */ export function exactClassOfReceiver(lowerer: Lowerer, expr: ts.Expression): ClassInfo | null { // A class-valued lexical this is introduced only for an exact static // receiver specialization; ordinary instance this remains an object. if (expr.kind === ts.SyntaxKind.ThisKeyword) { const receiver = lowerer.resolveThis(); return receiver?.type.kind === "classval" ? lowerer.classes.get(receiver.type.className) ?? null : null; } if (!ts.isIdentifier(expr)) return null; const symbol = lowerer.resolveValueSymbol(expr); if (!symbol) return null; const direct = lowerer.classBySymbol.get(symbol); // A rebindable decorated name is NOT exactly its class — the binding // may hold a replacing decorator's result (a subclass value), where a // static write would create an own property in JS. The general // class-value write fence answers instead. if (direct) return direct.classDecorators?.valueGlobalId !== undefined ? null : direct; const decl = lowerer.checker.valueDeclarationOf(symbol); if ( !decl || !ts.isVariableDeclaration(decl) || decl.initializer === undefined || !ts.isVariableDeclarationList(decl.parent) || (decl.parent.flags & ts.NodeFlags.Const) === 0 ) { return null; } let init: ts.Expression = decl.initializer; while (ts.isParenthesizedExpression(init)) init = init.expression; if (ts.isClassExpression(init)) return lowerer.exprClassInfoByName.get(lowerer.classNamer(init)) ?? null; if (ts.isIdentifier(init)) { const initSym = lowerer.resolveValueSymbol(init); const aliased = initSym ? (lowerer.classBySymbol.get(initSym) ?? null) : null; // `const X = C` over a rebindable decorated name: X holds the // decoration result — not exactly C (see the direct case above). return aliased?.classDecorators?.valueGlobalId !== undefined ? null : aliased; } return null; } /** The class a PROPERTY-ASSIGNMENT binding pins — the salsa/CJS * declaration forms of a class expression: `Common.I = class {…}` * (expando members of a plain object), `exports.I = class {…}` / * `module.exports.I = class {…}` (CJS member exports), and * `module.exports = class {…}` (the whole-export replacement, whose * export symbol requirer bindings alias to). The symbol arrives in two * shapes — an ALIAS resolving to the class expression's own symbol * (valueDeclaration IS the ts.ClassExpression), or the expando property * symbol whose declarations are the assignment BinaryExpressions — and * both pin the class exactly when ONE top-level assignment declares it: * a reassigned property is a dynamic binding (the runtime class is * whichever assignment ran last), so it answers null and the caller's * fence names it. Collection is on demand and idempotent * (lowerClassExpressionInfo), so resolution order between files and * passes never matters. */ export function propertyAssignedClassInfoOf( lowerer: Lowerer, symbol: ts.Symbol | null | undefined, ): ClassInfo | null { if (!symbol) return null; const resolved = symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol; const registered = lowerer.classBySymbol.get(resolved); if (registered) return registered; const decls = lowerer.checker.declarationsOf(resolved); // The class expression's OWN symbol (tsgo's answer through CJS export // aliases): the declaration is the expression itself. Its top-level // assignment statement must be the binding's ONLY producer. The // resolved symbol registers in classBySymbol so every downstream // path — static reads, the `.name` fold, instanceof — answers like a // declaration from then on. if (decls.length === 1 && decls[0] !== undefined && ts.isClassExpression(decls[0])) { const assign = enclosingTopLevelClassAssignment(decls[0]); if (!assign || countAssignmentsTo(assign) !== 1) return null; const info = lowerer.lowerClassExpressionInfo(decls[0]); lowerer.classBySymbol.set(resolved, info); return info; } // The expando property symbol: every top-level `X.N = …` assignment is // one of its declarations — exactly one, binding a class expression, // pins the class. const assigns = decls.filter( (d): d is ts.BinaryExpression => ts.isBinaryExpression(d) && d.operatorToken.kind === ts.SyntaxKind.EqualsToken, ); if (assigns.length !== 1 || assigns.length !== decls.length) return null; const a = assigns[0]!; if (!ts.isExpressionStatement(a.parent) || !ts.isSourceFile(a.parent.parent)) return null; let rhs: ts.Expression = a.right; while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression; if (!ts.isClassExpression(rhs)) return null; const info = lowerer.lowerClassExpressionInfo(rhs); lowerer.classBySymbol.set(resolved, info); return info; } /** The top-level `… = ` assignment a class * expression is the (paren-unwrapped) RHS of, or null. */ function enclosingTopLevelClassAssignment(expr: ts.ClassExpression): ts.BinaryExpression | null { let value: ts.Expression = expr; while (ts.isParenthesizedExpression(value.parent)) value = value.parent; const p = value.parent; if ( !ts.isBinaryExpression(p) || p.operatorToken.kind !== ts.SyntaxKind.EqualsToken || p.right !== value || !ts.isExpressionStatement(p.parent) || !ts.isSourceFile(p.parent.parent) ) { return null; } return p; } /** How many top-level statements of the file assign the same target as * `assign` (textual LHS match — `module.exports`, `exports.I`, * `Common.I`): a second assignment makes the binding dynamic, so * callers refuse to pin the first one's class. */ function countAssignmentsTo(assign: ts.BinaryExpression): number { const sf = assign.getSourceFile(); // `exports.I` and `module.exports.I` are the SAME binding in Node // (exports aliases module.exports until a table replaces it) — fold // the member spellings together before comparing. const canon = (lhs: ts.Expression): string => { const text = lhs.getText().replace(/\s+/g, ""); return text.startsWith("module.exports.") ? text.slice("module.".length) : text; }; const target = canon(assign.left); let n = 0; for (const stmt of sf.statements) { if (!ts.isExpressionStatement(stmt)) continue; const e = stmt.expression; if ( ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.EqualsToken && canon(e.left) === target ) { n++; } } return n; } /** `C.m(args)` / `X.m(args)` — static method calls, on the class name * directly or through a class VALUE. Resolution walks the chain * (nearest declarer, the compile-time prototype chain); through a * VALUE the call devirtualizes exactly when no strict descendant * redeclares the member (values never leave the static class's * subtree). A func-typed static FIELD in call position reads the * global and calls through the value. Null when the receiver isn't a * class name/value or the member doesn't resolve (the fences name the * site downstream). */ export function lowerStaticMethodCall(lowerer: Lowerer, call: ts.CallExpression, access: ts.PropertyAccessExpression,): IrExpr | null { if (access.questionDotToken) return null; if (access.expression.kind === ts.SyntaxKind.ThisKeyword) { const receiver = lowerer.resolveThis(); const info = receiver?.type.kind === "classval" ? lowerer.classes.get(receiver.type.className) : undefined; if (info) return staticCallOn(lowerer, call, access, info, false); } // `module.exports.describe()` in a module whose whole export IS a // class expression: the receiver is exactly that class (the kept // export assignment pins it) — the direct-name rules apply. if (!ts.isIdentifier(access.expression)) { if (!isModuleExportsAccess(access.expression) || !isCjsJsFile(access.getSourceFile(), lowerer.program)) { const receiverType = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); if (receiverType?.kind !== "classval") return null; const info = lowerer.classes.get(receiverType.className); if (!info) return null; const result = staticCallOn(lowerer, call, access, info, true); if (!result) return null; // Evaluate and validate the receiver before evaluating arguments. // A typed checker projection cannot turn a replaced field into a call // to the class that originally occupied it. const receiver = lowerer.lowerExprExpecting(access.expression, receiverType); return { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: receiver, loc: locOf(access.expression) }], result, type: result.type, loc: locOf(call) }; } const whole = cjsClassExprWholeExportOf(access.getSourceFile()); if (!whole) return null; return staticCallOn(lowerer, call, access, lowerer.lowerClassExpressionInfo(whole.classExpr), false); } const symbol = lowerer.resolveValueSymbol(access.expression); const direct = (symbol ? lowerer.classBySymbol.get(symbol) : undefined) ?? // A require binding over `module.exports = class {…}`: the alias // lands on the expression's own symbol — exact, like the name. propertyAssignedClassInfoOf(lowerer, symbol) ?? undefined; let info = direct ?? null; // A rebindable decorated name is a class VALUE receiver: the call // devirtualizes under the value rules (shadow fences below). let throughValue = direct?.classDecorators?.valueGlobalId !== undefined; if (!info) { const recvT = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); if (recvT?.kind !== "classval") return null; info = lowerer.classes.get(recvT.className) ?? null; throughValue = true; } if (!info) return null; return staticCallOn(lowerer, call, access, info, throughValue); } /** True when a class-VALUE receiver can only hold `info`'s own class * object at runtime: the receiver provably IS the class * (exactClassOfReceiver — the name, or a const bound to the class * expression / class name), or `info` is a LEAF class, so no strict * descendant exists to flow into the slot (the static-write path's * rule — a rebindable decorated name rides this arm, since replacement * decorators fence any class with subclasses before minting the * rebindable global). Exactly then a #private static access through * the value always carries the declaring class's brand — Node's * TypeError cannot arise. */ function classValueIsExactlyOwn(lowerer: Lowerer, recv: ts.Expression, info: ClassInfo): boolean { return exactClassOfReceiver(lowerer, recv) === info || info.subclasses.length === 0; } function staticCallOn( lowerer: Lowerer, call: ts.CallExpression, access: ts.PropertyAccessExpression, info: ClassInfo, throughValue: boolean, ): IrExpr | null { const loc = locOf(call); if (info.callableBase || hasRuntimeStatics(info)) return { kind: "dynInvoke", recv: lowerer.lowerExprExpecting(access.expression, DYN), method: access.name.text, calleeName: access.name.text, args: call.arguments.map((argument) => lowerer.lowerExprExpecting(argument, DYN)), type: DYN, loc }; // #private statics: through-a-VALUE receiver the call devirtualizes // exactly when the value can only BE the declaring class // (classValueIsExactlyOwn — a slot that could hold a descendant at // runtime fences, since only the declaring class object carries the // brand in JS), and the direct spelling resolves only on the // declaring class itself — `D.#s` is Node's TypeError. if (access.name.text.startsWith("#") && throughValue && !classValueIsExactlyOwn(lowerer, access.expression, info)) { lowerer.unsupported( "SC1090", call, `calling the private static '${access.name.text}' through a class value (JS brands the declaring class object alone — call it through the class's own name)`, ); } const found = findStaticOn(lowerer, info, access.name.text); if (found && access.name.text.startsWith("#") && found.declarer !== info) { lowerer.unsupported( "SC1090", call, `calling the private static '${access.name.text}' through the subclass '${info.def.name.replace(/^%|^%m\d+\./, "")}' (JS brands the declaring class object alone — Node throws a TypeError here; spell the declaring class's name)`, ); } if (!found) { // GENERIC static methods: monomorphized like top-level generic // functions, called directly as `%C.static:m%n` — with the same // through-a-VALUE shadowing fence as plain statics. const gfound = findGenericStaticOn(lowerer, info, access.name.text); if (!gfound) return null; if (throughValue && staticShadowBelow(lowerer, info, access.name.text)) { lowerer.unsupported( "SC1090", call, `calling the static member '${access.name.text}' through a class value (a subclass of '${info.def.name.replace(/^%|^%m\d+\./, "")}' redeclares it, so the runtime class decides which declaration answers)`, ); } const instance = genericCallInstance(lowerer, call, staticMethodForReceiver(lowerer, access, gfound.info, info, throughValue)); const args = lowerer.completeArgs(call.arguments, instance.params, loc, call); return { kind: "call", callee: instance.name, args, type: instance.returnType, loc }; } if (throughValue && staticShadowBelow(lowerer, info, access.name.text)) { lowerer.unsupported( "SC1090", call, `calling the static member '${access.name.text}' through a class value (a subclass of '${info.def.name.replace(/^%|^%m\d+\./, "")}' redeclares it, so the runtime class decides which declaration answers)`, ); } const specialized = findGenericStaticOn(lowerer, info, access.name.text); if (specialized?.declarer === found.declarer && specialized.info.implicitParams) { const instance = implicitCallInstance(lowerer, call, staticMethodForReceiver(lowerer, access, specialized.info, info, throughValue)); const args = lowerer.completeArgs(call.arguments, instance.params, loc, call); return { kind: "call", callee: instance.name, args, type: instance.returnType, loc }; } if (found.field !== undefined) { // A func-typed static field in call position: read the global, // call through the value (the ctor-assigned-callback pattern). if (found.field.type.kind !== "func") return null; const callee: IrExpr = { kind: "varRef", localId: found.field.globalId, type: found.field.type, loc }; const params = found.field.type.params; const args = call.arguments.map((a, i) => lowerer.lowerExprExpecting(a, params[i])); for (let i = args.length; i < params.length; i++) { const absent = omittedArgFor(lowerer, params[i]!, loc); if (!absent) { lowerer.unsupported("SC1090", call, "calls omitting a non-optional parameter of the callee's type"); } args.push(absent); } return { kind: "callValue", callee, args, type: found.field.type.ret, loc }; } if (found.method.member.body && hasStaticThis(found.method.member)) { const generic = found.method.receiverInfo ??= { decl: found.method.member, baseName: access.name.text, qualifiedName: `%${found.declarer.def.name}.static:${access.name.text}`, typeParams: [], instances: new Map(), member: { cls: found.declarer, kind: "static" }, }; const instance = genericCallInstance(lowerer, call, staticMethodForReceiver(lowerer, access, generic, info, throughValue)); return { kind: "call", callee: instance.name, args: lowerer.completeArgs(call.arguments, instance.params, loc, call), type: instance.returnType, loc }; } const fnName = `%${found.declarer.def.name}.static:${access.name.text}`; lowerer.noteEdge(fnName); const args = lowerer.completeArgs(call.arguments, found.method.params, loc, call); return { kind: "call", callee: fnName, args, type: found.method.ret, loc }; } function staticMethodForReceiver(lowerer: Lowerer, access: ts.PropertyAccessExpression, method: GenericFnInfo, receiver: ClassInfo, throughValue: boolean): GenericFnInfo { if (!hasStaticThis(method.decl)) return method; // A private name in the inherited body belongs to the declaring class, // even when the receiver declares a different private with that spelling. // Do not resolve it by receiver name until the static ABI tracks the brand. if (method.member && method.member.cls !== receiver && hasLexicalPrivateReference(method.decl)) { lowerer.unsupported("SC1090", access, "private member references in an inherited receiver-dependent static method"); } if (throughValue && !classValueIsExactlyOwn(lowerer, access.expression, receiver)) { lowerer.unsupported("SC1090", access, "receiver-dependent static methods through a polymorphic class value"); } const instances = method.receiverSpecializations ??= new Map(); let specialized = instances.get(receiver.def.name); if (!specialized) { specialized = { ...method, qualifiedName: `${method.qualifiedName}%on.${receiver.def.name}`, instances: new Map(), staticReceiver: receiver, receiverSpecializations: new Map() }; instances.set(receiver.def.name, specialized); } return specialized; } /** Static member access through a class VALUE (`X.m` where X is * classval-typed): devirtualized — the member resolves against the * static class's chain, exact when no strict descendant redeclares it * (values in the slot never leave the subtree). `X.name` is the one * genuinely dynamic member: the class.name libCall reads the runtime * class object's stored name. Null when the receiver isn't a class * value or the member doesn't resolve. */ export function lowerClassValueProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null { if (expr.questionDotToken) return null; const recvT = storedClassValueType(lowerer, expr.expression); if (recvT?.kind !== "classval") return null; const loc = locOf(expr); const member = expr.name.text; const info = lowerer.classes.get(recvT.className); if (info && (info.callableBase || hasRuntimeStatics(info))) return { kind: "dynKeyGet", value: lowerer.lowerExprExpecting(expr.expression, DYN), key: { kind: "strLit", value: member, type: STRING, loc }, type: DYN, loc }; // `X.name` reads the RUNTIME class object's stored name — the one // genuinely dynamic member. It CONSUMES the receiver (no evaluation // is discarded), so any receiver expression is fine here. if (member === "name") { const recv = lowerer.lowerExpr(expr.expression); if (recv.type.kind !== "classval") return null; return { kind: "libCall", fn: "class.name", args: [recv], type: STRING, loc }; } if (!info) return null; if ( !ts.isIdentifier(expr.expression) && expr.expression.kind !== ts.SyntaxKind.ThisKeyword && // `module.exports.label` in a class-replaced CJS module: the // receiver is the exact exported class, and the read is // side-effect-free — as bindable as an identifier. !(isModuleExportsAccess(expr.expression) && isCjsJsFile(expr.getSourceFile(), lowerer.program)) ) { // Devirtualized reads DISCARD the receiver value, so only // side-effect-free receivers are claimed (the instanceOf fold // rule); computed ones meet the pointed fence with a bindable fix. lowerer.unsupported( "SC1090", expr, "static member access through a computed class-value expression (bind the class value to a variable first)", ); } // The direct class-name spelling resolved in lowerStaticFieldRead; // reaching here means the receiver is a classval-typed BINDING. const found = findStaticOn(lowerer, info, member); if (!found) { lowerer.unsupported( "SC1090", expr, `the static member '${member}' of class '${info.def.name.replace(/^%|^%m\d+\./, "")}' (static accessors and initializer-less static fields have no lowering, and Function members like .call/.bind/.prototype have no value form)`, ); } // #private statics read through a class VALUE exactly when the value // can only BE the declaring class (classValueIsExactlyOwn — a const // bound to the class expression / class name, or a leaf class): a // slot that could hold a descendant at runtime fences, since JS // brands the declaring class object alone (Node's TypeError on any // other receiver). The direct class-name spelling resolved in // lowerStaticFieldRead, so a private reaching here is a // classval-typed binding. if (member.startsWith("#")) { if (!classValueIsExactlyOwn(lowerer, expr.expression, info)) { lowerer.unsupported( "SC1090", expr, `reading the private static '${member}' through a class value (JS brands the declaring class object alone — spell the declaring class's name)`, ); } if (found.declarer !== info) { lowerer.unsupported( "SC1090", expr, `reading the private static '${member}' through the subclass '${info.def.name.replace(/^%|^%m\d+\./, "")}' (JS brands the declaring class object alone — Node throws a TypeError here; spell the declaring class's name)`, ); } } if (staticShadowBelow(lowerer, info, member)) { lowerer.unsupported( "SC1090", expr, `reading the static member '${member}' through a class value (a subclass of '${info.def.name.replace(/^%|^%m\d+\./, "")}' redeclares it, so the runtime class decides which declaration answers)`, ); } if (found.field !== undefined) { return lowerer.maybeNarrow( { kind: "varRef", localId: found.field.globalId, type: found.field.type, loc }, expr, ); } return staticMethodValue(lowerer, found.declarer, member, found.method, expr, loc); } /** An abstract method's signature — lambdaSignature minus the body check * (an abstract declaration IS exactly a signature; tsc rejects the * async/generator/generic-with-body combinations before this runs, and * the generic case fences at the caller). */ function abstractMemberSignature(lowerer: Lowerer, member: ts.MethodDeclaration): { shapes: ParamShape[]; ret: IrType } { for (const param of member.parameters) { if (!ts.isIdentifier(param.name)) lowerer.unsupported("SC1031", param); } return { shapes: lowerer.paramShapes(member.parameters), ret: lowerer.declaredReturnType(member, member.name) }; } /** The nearest declaration of `name` at or above `info` — the method a * receiver of that static class runs when nothing below overrides it. */ export function findMethodOn(lowerer: Lowerer, info: ClassInfo | null, name: string,): { declarer: ClassInfo; sig: { params: ParamShape[]; ret: IrType; abstract?: true; async?: true; gen?: NonNullable } } | null { for (let c = info; c; c = c.base) { const sig = c.methods.get(name); if (sig) return { declarer: c, sig }; } return null; } /** True when `sub` is a STRICT descendant of `sup` in the class graph. */ export function isSubclassOf(lowerer: Lowerer, sub: string, sup: string): boolean { for (let c = lowerer.classes.get(sub)?.base ?? null; c; c = c.base) { if (c.def.name === sup) return true; } return false; } /** In an extends-hierarchy (as base or derived): the class carries a * vtable and participates in dynamic instanceof; standalone classes keep * their exact pre-inheritance layout and behavior. */ export function inHierarchy(lowerer: Lowerer, info: ClassInfo): boolean { // The runtime emitter class is ALWAYS a hierarchy member: ScrEmitter // carries its vtable word whether or not the program subclasses it // (the runtime allocates bare instances with scr_emitter_vt). return info.base !== null || info.subclasses.length > 0 || info.builtinEmitter === true; } /** True when some STRICT descendant of `info` declares `name` with a BODY * — the whole-program devirtualization test: a call through this static * class can reach a distinct implementation, so it must dispatch * dynamically. Abstract re-declarations don't count (they carry no * implementation; the concrete ones below them do, via the recursion). */ export function overrideBelow(lowerer: Lowerer, info: ClassInfo, name: string): boolean { return info.subclasses.some((s) => { const m = s.methods.get(name); return (m !== undefined && m.abstract !== true) || lowerer.overrideBelow(s, name); }); } /** The nearest GENERIC-method declaration of `name` at/above `info` — * findMethodOn's twin over the genericMethods tables. */ export function findGenericMethodOn(lowerer: Lowerer, info: ClassInfo | null, name: string,): { declarer: ClassInfo; info: GenericFnInfo } | null { for (let c = info; c; c = c.base) { const gm = c.genericMethods?.get(name); if (gm) return { declarer: c, info: gm }; } return null; } /** True when some STRICT descendant of `info` re-declares the generic * method `name` — overrideBelow's twin: generic methods have no vtable * slot, so a call that could reach an override compiles only when the * receiver's runtime class is statically exact. */ export function genericOverrideBelow(lowerer: Lowerer, info: ClassInfo, name: string): boolean { return info.subclasses.some( (s) => s.genericMethods?.has(name) === true || genericOverrideBelow(lowerer, s, name), ); } /** Discover overrides before choosing inferred method ABIs. Class collection * visits bases first, including imported bases and class expressions, so the * collected subclass graph cannot answer this question yet. The checker * hierarchy identifies the original declarations through aliases as well. */ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.SourceFile[]): void { // Record named prototype accesses before collecting method signatures. // Those slots need a callable ABI even when JavaScript would otherwise // specialize each call independently. for (const file of files) ts.walkPreorder(file, (node) => { if (!ts.isPropertyAccessExpression(node) && !ts.isElementAccessExpression(node)) return; const name = ts.isPropertyAccessExpression(node) ? node.name.text : ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null; if (name === null) return; const prototype = ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "prototype"; const assigned = ts.isBinaryExpression(node.parent) && node.parent.left === node && node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken; const symbol = !assigned ? undefined : ts.isPropertyAccessExpression(node) ? lowerer.checker.getSymbolAtLocation(node.name) : lowerer.checker.getPropertyOfType(lowerer.typeOf(node.expression), name); if (prototype || symbol && lowerer.checker.declarationsOf(symbol).some(ts.isMethodDeclaration)) { lowerer.prototypeMethodAccesses.set(name, node); } }); const visit = (node: ts.Node): void => { if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) { const members = node.members.filter((member): member is ts.MethodDeclaration => ts.isMethodDeclaration(member) && ts.isIdentifier(member.name) && !member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword), ); if (node.heritageClauses?.length && members.length > 0) { const symbol = lowerer.typeOf(node).getSymbol(); const instance = symbol ? lowerer.checker.getDeclaredTypeOfSymbol(symbol) : null; const seen = new Set(); const markBases = (type: ts.Type): void => { const target = type.isTypeReference() ? type.getTarget() : type; if (target === undefined || !target.isClassOrInterface() || seen.has(target)) return; seen.add(target); for (const base of lowerer.checker.getBaseTypes(target)) { for (const member of members) { if (!ts.isIdentifier(member.name)) continue; const property = lowerer.checker.getPropertyOfType(base, member.name.text); for (const declaration of property ? lowerer.checker.declarationsOf(property) : []) { if (ts.isMethodDeclaration(declaration) && isJsSourceFile(declaration.getSourceFile())) { lowerer.virtualJsMethods.add(declaration); if (isJsSourceFile(member.getSourceFile()) && !member.typeParameters && !member.asteriskToken && !member.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) && !bodyReadsArguments(member) && member.parameters.every((p) => !p.initializer && !p.dotDotDotToken)) { lowerer.virtualJsMethodArity.set(declaration, Math.max(lowerer.virtualJsMethodArity.get(declaration) ?? 0, member.parameters.length)); } } } } markBases(base); } }; if (instance) markBases(instance); } } }; for (const file of files) ts.walkPreorder(file, visit); } /** The receiver's EXACT runtime class, when the expression proves it: a * `new C(...)` expression directly, or a const binding initialized with * one (the binding can never be reassigned to a subclass instance). * The class is read off the mapped INITIALIZER type — a `const b: Base = * new D()` receiver is exactly D, not its annotation. Distinct from * exactClassOfReceiver, which answers for CLASS-VALUE receivers. */ export function exactInstanceClassOf(lowerer: Lowerer, expr: ts.Expression): ClassInfo | null { let e: ts.Expression = expr; while (ts.isParenthesizedExpression(e)) e = e.expression; const classOfNew = (n: ts.Expression): ClassInfo | null => { if (!ts.isNewExpression(n)) return null; const t = lowerer.mapTypeOf(lowerer.typeOf(n)); return t?.kind === "object" ? (lowerer.classes.get(t.className) ?? null) : null; }; const direct = classOfNew(e); if (direct) return direct; if (!ts.isIdentifier(e)) return null; const symbol = lowerer.resolveValueSymbol(e); const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; if ( !decl || !ts.isVariableDeclaration(decl) || decl.initializer === undefined || !ts.isVariableDeclarationList(decl.parent) || (decl.parent.flags & ts.NodeFlags.Const) === 0 ) { return null; } let init: ts.Expression = decl.initializer; while (ts.isParenthesizedExpression(init)) init = init.expression; return classOfNew(init); } /** `const r: Repo = new MemRepo()` where EVERY member of the annotation's * checker type is a generic-callable method (`interface Repo { get(id: * string): T }`): the record shape maps EMPTY — generic members are * excluded (no closure slot can hold a generic function) — so the * copy-reshape width coercion would DROP the exact class the method * calls monomorphize against, and in JS the binding IS the instance (no * copy exists). The binding keeps the initializer's class * representation instead: generic-method calls resolve like class-typed * receivers (exactInstanceClassOf reads the same const+new discipline), * and uses that want the empty record width-coerce at the use site. * Const + direct `new` only — a reassignable binding or a produced * value keeps today's record story and its fences. */ export function genericIfaceBindingKeepsClass(lowerer: Lowerer, decl: ts.VariableDeclaration, declaredType: IrType,): boolean { if (declaredType.kind !== "record") return false; if (!ts.isIdentifier(decl.name) || decl.initializer === undefined || decl.type === undefined) return false; if ((ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) === 0) return false; let init: ts.Expression = decl.initializer; while (ts.isParenthesizedExpression(init)) init = init.expression; if (!ts.isNewExpression(init)) return false; const shape = lowerer.shapes.get(declaredType.shapeId); if (!shape || shape.fields.length > 0 || shape.indexValue !== undefined || shape.tuple === true) return false; const annT = lowerer.typeOf(decl.name); const props = lowerer.checker.getPropertiesOfType(annT); if (props.length === 0) return false; return props.every((p) => isGenericCallableMemberType(lowerer.checker.getTypeOfSymbol(p), lowerer.checker)); } /** `recv.m(args)` — a GENERIC method call, dispatched STATICALLY: the * checker's resolved signature (type arguments substituted, inferred or * explicit) keys one instantiation of the nearest declarer's body, and * the call is a direct `call` of `%C.m%n` over the (up/down)cast * receiver. No per-instantiation vtable slots exist, so a receiver whose * runtime class could OVERRIDE the method (genericOverrideBelow) must be * statically exact (exactInstanceClassOf) — the override set then * resolves at compile time — or fences by name. */ export function lowerClassGenericMethodCall(lowerer: Lowerer, call: ts.CallExpression, access: ts.PropertyAccessExpression, recvInfo: ClassInfo, found: { declarer: ClassInfo; info: GenericFnInfo }, recvIr?: IrExpr,): IrExpr { const name = access.name.text; let { declarer, info } = found; if (genericOverrideBelow(lowerer, recvInfo, name)) { const exact = exactInstanceClassOf(lowerer, access.expression); const refound = exact ? findGenericMethodOn(lowerer, exact, name) : null; if (!refound) { lowerer.unsupported( "SC1090", call, `calling the generic method '${name}' through a receiver whose runtime class may override it (a subclass of '${recvInfo.def.name.replace(/^%|^%m\d+\./, "")}' redeclares it and generic methods dispatch statically — bind the receiver to a const initialized with its 'new' expression)`, ); } ({ declarer, info } = refound); } // Implicit-any methods instantiate over the call's ARGUMENT types // (there is no resolved generic signature — the untyped params are the // type parameters); everything else about the dispatch — static // resolution, the exactness rule above — is the generic story. const instance = info.implicitParams ? implicitCallInstance(lowerer, call, info) : genericCallInstance(lowerer, call, info); const receiver = recvIr ?? lowerer.lowerExpr(access.expression); const loc = locOf(call); // The declarer sits at/above the receiver's static class on the plain // path; the EXACT path can land below it (a base-typed const provably // holding the subclass) — that direction is the checker-grade downcast // (the exactness proof is static, stronger than an instanceof guard). const thisArg = receiver.type.kind === "object" && isSubclassOf(lowerer, declarer.def.name, receiver.type.className) ? { kind: "downcast" as const, value: receiver, type: { kind: "object" as const, className: declarer.def.name }, loc } : upcastTo(lowerer, receiver, declarer.def.name); const args = lowerer.completeArgs(call.arguments, instance.params, loc, call); return { kind: "call", callee: instance.name, args: [thisArg, ...args], type: instance.returnType, loc }; } /** Wraps a derived-class expression in an upcast when the target base * class differs (a no-op reinterpret at runtime; keeps IR types exact). */ export function upcastTo(lowerer: Lowerer, expr: IrExpr, className: string): IrExpr { if (expr.type.kind === "object" && expr.type.className !== className) { return { kind: "upcast", value: expr, type: { kind: "object", className }, loc: expr.loc }; } return expr; } /** Constructor and methods become module functions `%C.name` whose first * param is `this`. Field initializers run in declaration order at the top * of a base class's constructor; a derived class's run right after its * super() call returns (tsc/JS initialization order). Reachability gates * each member independently: an unreached method body never lowers and * never emits (pinned by the corpus), while every override a reachable * virtualCall can dispatch to was marked by the discovery pass. */ /** True when a MIXIN class's constructor is the pure forwarding shape: * exactly one rest parameter, `super(...args)` as the first statement, * and no other reference to the parameter — the one rest-constructor * form with an exact static story under monomorphization (the * instantiation adopts the base's ABI; see collectClassShapeInner). */ function mixinForwardingCtor(lowerer: Lowerer, ctor: ts.ConstructorDeclaration): boolean { if (ctor.parameters.length !== 1 || !ctor.body) return false; const p = ctor.parameters[0]!; if (!p.dotDotDotToken || !ts.isIdentifier(p.name)) return false; const paramName = p.name; const paramSym = lowerer.checker.getSymbolAtLocation(paramName); if (!paramSym) return false; const first = ctor.body.statements[0]; if (!first || !ts.isExpressionStatement(first) || !ts.isCallExpression(first.expression)) return false; const call = first.expression; if (call.expression.kind !== ts.SyntaxKind.SuperKeyword) return false; if (call.arguments.length !== 1) return false; const a = call.arguments[0]!; if (!ts.isSpreadElement(a) || !ts.isIdentifier(a.expression)) return false; const spreadIdent = a.expression; if (lowerer.checker.getSymbolAtLocation(spreadIdent) !== paramSym) return false; let extraRef = false; ts.walkPreorder(ctor.body, (n) => { if (n === spreadIdent) return undefined; if (ts.isIdentifier(n) && n.text === paramName.text && lowerer.checker.getSymbolAtLocation(n) === paramSym) { extraRef = true; return "stop"; } return undefined; }); return !extraRef; } export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction[] { // Local class bodies are lowered when their expression captures its // lexical environment. Type collection alone can discover a class in // an owner skipped by the coverage remainder; it must not relower the // constructor without that environment. if (info.localClass && info.localClass.context === null) { return [...info.localClass.bodies.values()]; } const out: IrFunction[] = []; const className = info.def.name; // Generic-class INSTANTIATIONS (and mixin instantiations) are // demand-driven like generic-fn instances, not reachability units: // they are never registered as units, so wantBody's name-keyed gate // cannot apply — every member of a demanded instantiation lowers. const always = info.genericInstance !== undefined || info.mixinInstance !== undefined || info.localClass !== undefined; // A FAMILY has no constructor function at all (nothing constructs it; // construction resolves to instantiations) and declares no instance // members — only its statics lower below. // A poison OUTSIDE the per-statement catches (a fenced parameter // default — declareParams lowers it before any statement-level catch // exists): the diagnostic is recorded — the member skips like a // signature-blocked function (lowerStaticMethod's rule) instead of // crashing the whole lowering. if (!info.generic && (always || lowerer.wantBody(`%${className}.constructor`))) { try { out.push(lowerer.lowerClassCtor(info)); } catch (e) { if (!(e instanceof PoisonError)) throw e; } } for (const { mName, member } of lowerer.classMethodMembers(info)) { if (!always && !lowerer.wantBody(`%${className}.${mName}`)) continue; try { const fn = lowerer.lowerClassMethodMember(info, member); if (fn) out.push(fn); } catch (e) { if (!(e instanceof PoisonError)) throw e; } } if (info.staticMethods) for (const [name] of info.staticMethods) { if (!lowerer.wantBody(`%${className}.static:${name}`)) continue; const fn = lowerStaticMethod(lowerer, info, name); if (fn) out.push(fn); } for (const prop of info.throwingSetters) { if (always || lowerer.wantBody(`%${className}.set:${prop}`)) out.push(lowerer.throwingSetterFn(info, prop)); } return out; } /** The constructor function `%C.constructor`. Synthesized when absent: a * base class runs just its field initializers; a derived class inherits * the base's signature — forward every param to super(), then run own * field initializers. */ export function lowerClassCtor(lowerer: Lowerer, info: ClassInfo): IrFunction { const cached = info.localClass?.bodies.get(`%${info.def.name}.constructor`); if (cached) return cached; return withInstanceBindings(lowerer, info, () => lowerClassCtorInner(lowerer, info)); } function classCaptureParams(info: ClassInfo, context: FnCtx): Pick { if (!info.localClass) return {}; const environment = info.localClass.context; if (!environment) throw new InternalCompilerError("local class lowered outside its lexical environment"); return { classCaptures: (context.captures ?? []).map((capture, index) => { if (!capture) throw new InternalCompilerError("missing local class capture"); return { ...capture, slot: environment.captures!.findIndex((entry) => entry.localId === context.captureSources[index]) }; }) }; } function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction { const className = info.def.name; const thisType: IrType = { kind: "object", className }; const prevClass = lowerer.currentClass; lowerer.currentClass = info; lowerer.fnStack.push(newFnCtx(info.localClass !== undefined, null, null, VOID)); try { const thisLocal = lowerer.declareThis(thisType); const params: IrParam[] = [{ localId: thisLocal.id, name: "this", type: thisType }]; const body: IrStmt[] = []; // The construction-relevant base: generic families are transparent // (an instantiation of a base-less generic class IS a base class — // its source has no super()). const ctorBase = superBaseOf(info); if (info.ctor && info.mixinInstance?.forwardingCtor) { // The mixin FORWARDING constructor: the declared rest parameter // never materializes — the ABI is the base's (synthetic params, // the synthesized-ctor rule), `super(...args)` forwards them // unchanged, and the remaining statements lower normally. const loc = locOf(info.ctor); const forward: IrExpr[] = info.ctorParams.map((shape, i) => { const local: IrLocal = { id: `arg${i}.0`, name: `arg${i}`, type: shape.type, mutable: false }; lowerer.ctx.locals.push(local); params.push({ localId: local.id, name: local.name, type: shape.type }); return { kind: "varRef", localId: local.id, type: shape.type, loc }; }); body.push(...lowerer.lowerDerivedCtorBody(info, thisLocal, forward)); } else if (info.ctor) { // The default-param prologue runs FIRST — before field initializers // and (in a derived class) before super(): JS evaluates parameter // defaults on entry, ahead of everything the body does. const declared = lowerer.declareParams(info.ctor.parameters, info.ctorParams); params.push(...declared.params); if (info.ctorParams.at(-1)?.mode === "arguments") { const local = lowerer.declareHiddenLocal("%arguments", DYN); params.push({ localId: local.id, name: local.name, type: DYN }); lowerer.ctx.argumentsLocal = local; } body.push(...declared.prologue); if (!ctorBase && !info.callableBase) { // Node's base-class order: field initializers run at the start // of construction, the parameter-property assignments open the // constructor body (probed — a field initializer reading a // parameter property sees undefined). body.push(...lowerer.fieldInitStmts(info, thisLocal)); body.push(...paramPropInitStmts(lowerer, info, thisLocal)); if (info.ctor.body) body.push(...lowerer.lowerStmts(info.ctor.body.statements)); } else if (info.ctor.body) { body.push(...lowerer.lowerDerivedCtorBody(info, thisLocal)); } } else { if (ctorBase || info.callableBase) { // Synthetic forwarding params (the inherited ABI signature). // Nothing references them by symbol — only the super call below, // which forwards the already-completed values UNCHANGED (defaults // apply in the base constructor's own prologue, never twice). const loc = locOf(info.decl!); const superArgs: IrExpr[] = info.ctorParams.map((shape, i) => { const local: IrLocal = { id: `arg${i}.0`, name: `arg${i}`, type: shape.type, mutable: false }; lowerer.ctx.locals.push(local); params.push({ localId: local.id, name: local.name, type: shape.type }); return { kind: "varRef", localId: local.id, type: shape.type, loc }; }); try { body.push(lowerer.superCallStmt(info, thisLocal, superArgs, loc)); } catch (e) { // A synthesized super() can fence (a stream base whose // underscore methods have no lowering): the diagnostic was // pushed; the half-initialized ctor stays out of the body. if (!(e instanceof PoisonError)) throw e; } } body.push(...lowerer.fieldInitStmts(info, thisLocal)); } return { name: `%${className}.constructor`, params, returnType: VOID, locals: lowerer.ctx.locals, ...classCaptureParams(info, lowerer.ctx), body, loc: locOf(info.ctor ?? info.decl!), }; } finally { lowerer.fnStack.pop(); lowerer.currentClass = prevClass; } } /** The lowered method-map name of a class member: identifier text, a * COMPUTED name that folds to one compile-time string * (foldedStringKeyOf — the object-literal computed-key machinery * applied to method positions; tsc late-bound the member under exactly * that name), or a reserved `sym:*` slot for the well-known symbols the * compiler consumes as language protocols (iterator and explicit * resource management). These names are unspellable as user identifiers, * like the accessor `get:x` convention. Null for genuinely * runtime-keyed names — the computed-member fences stay. */ export function classMemberNameOf(lowerer: Lowerer, name: ts.PropertyName): string | null { if (ts.isIdentifier(name)) return name.text; // #private methods key by their spelled name ('#m') — '#' is // unspellable in public identifiers, the accessor-colon precedent. if (ts.isPrivateIdentifier(name)) return name.text; if (!ts.isComputedPropertyName(name)) return null; let e = name.expression; while (ts.isParenthesizedExpression(e)) e = e.expression; if (ts.isPropertyAccessExpression(e)) { const member = lowerer.stdlibGlobalMember(e, "Symbol"); if ( member === "iterator" || member === "asyncIterator" || member === "dispose" || member === "asyncDispose" ) { return `sym:${member}`; } } return classSymbolKeyOf(lowerer, name.expression)?.fieldName ?? lowerer.foldedStringKeyOf(name.expression); } /** A class type's ITERATOR PROTOCOL shape, statically resolved: the * receiver declares (or inherits) `[Symbol.iterator]()` (the * "sym:iterator" method slot) returning a class whose zero-parameter * `next()` returns a record with a `value` field and an optional * boolean `done` field (`{ value, done: false }` — the self-iterator * idiom returns `this`, so the iterator class is usually the receiver * itself). A MISSING done field never terminates — exactly JS, where * `undefined` is falsy forever (Node loops forever too; corpus * iterators of that shape are deliberately infinite). Iterator classes * declaring `return`/`throw` members stay out — the desugars below * never call IteratorClose, and silently skipping a declared return() * would drop user cleanup. Null when the shape doesn't hold — callers * keep their fences. */ export interface ClassIteratorInfo { /** The `[Symbol.iterator]()` call's receiver class + its declarer. */ className: string; /** The iterator object's type (the method's return). */ iterT: IrType & { kind: "object" }; /** next()'s result record type. */ resultT: IrType & { kind: "record" }; valueT: IrType; /** False: no done field — the protocol never terminates. */ hasDone: boolean; } export function classIteratorOf(lowerer: Lowerer, t: IrType): ClassIteratorInfo | null { if (t.kind !== "object") return null; const info = lowerer.classes.get(t.className); if (!info) return null; const iter = findMethodOn(lowerer, info, "sym:iterator"); if (!iter || iter.sig.params.length !== 0 || iter.sig.abstract === true) return null; const iterT = iter.sig.ret; if (iterT.kind !== "object") return null; const itInfo = lowerer.classes.get(iterT.className); if (!itInfo) return null; // IteratorClose honesty: a declared return()/throw() would be called // by JS on abrupt completion; these desugars never close. if (findMethodOn(lowerer, itInfo, "return") || findMethodOn(lowerer, itInfo, "throw")) return null; const next = findMethodOn(lowerer, itInfo, "next"); if (!next || next.sig.params.length !== 0 || next.sig.abstract === true) return null; const resultT = next.sig.ret; if (resultT.kind !== "record") return null; const shape = lowerer.shapes.get(resultT.shapeId); const value = shape?.fields.find((f) => f.name === "value"); if (!shape || !value) return null; const done = shape.fields.find((f) => f.name === "done"); if (done && done.type.kind !== "bool") return null; return { className: t.className, iterT, resultT, valueT: value.type, hasDone: done !== undefined }; } /** The `it.next()` step of a class iterator as an ordinary (possibly * virtual) method call. */ export function classIteratorNextCall(lowerer: Lowerer, cit: ClassIteratorInfo, itRef: IrExpr, loc: SrcLoc): IrExpr { return accessorCall(lowerer, cit.iterT.className, "next", itRef, [], cit.resultT, loc); } /** `recv[Symbol.iterator]()` as an ordinary method call. */ export function classIteratorOpenCall(lowerer: Lowerer, cit: ClassIteratorInfo, recv: IrExpr, loc: SrcLoc): IrExpr { return accessorCall(lowerer, cit.className, "sym:iterator", recv, [], cit.iterT, loc); } /** `[...new C]` / `f(...new C)` over a CLASS ITERABLE: the eager drain — * an interned `%iter.drain.(recv)` lifted function running the * whole protocol into a fresh element array (a doneless iterator loops * forever, exactly Node's spread of an infinite iterator). `elemT` * (default: the iterator's own value type) is the DESTINATION element — * a spread into a union-element literal (`[...numbers, ...symbols]` as * `(number | symbol)[]`) pushes each value wrapped into its arm. Null * when the value isn't a recognized class iterable or the element * doesn't coerce — spread fences stay. */ export function classIteratorDrainCall(lowerer: Lowerer, src: IrExpr, loc: SrcLoc, elemT?: IrType): IrExpr | null { const cit = classIteratorOf(lowerer, src.type); if (!cit) return null; const outElem = elemT ?? cit.valueT; // Probe the element coercion purely: identical types, or an arm of a // union destination (the wrap coerceToExpected applies below). if (!typeEquals(outElem, cit.valueT)) { if (outElem.kind !== "union" || lowerer.armTag(outElem.unionId, cit.valueT) < 0) return null; } const outT = arrayOf(outElem); const key = `${cit.className}:${typeKey(outElem)}`; let name = lowerer.iterDrainHelpers.get(key); if (!name) { name = `%iter.drain.${lowerer.iterDrainHelpers.size}`; lowerer.iterDrainHelpers.set(key, name); const recvT: IrType = { kind: "object", className: cit.className }; const recvRef: IrExpr = { kind: "varRef", localId: "r.0", type: recvT, loc }; const itRef: IrExpr = { kind: "varRef", localId: "it.0", type: cit.iterT, loc }; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: outT, loc }; const resRef: IrExpr = { kind: "varRef", localId: "res.0", type: cit.resultT, loc }; const valueRead: IrExpr = { kind: "recordGet", obj: resRef, shapeId: cit.resultT.shapeId, field: "value", type: cit.valueT, loc }; const loop: IrStmt[] = [ { kind: "varDecl", localId: "res.0", init: classIteratorNextCall(lowerer, cit, itRef, loc), loc }, ...(cit.hasDone ? [ { kind: "if", cond: { kind: "recordGet", obj: resRef, shapeId: cit.resultT.shapeId, field: "done", type: BOOL, loc }, then: [{ kind: "return", value: outRef, loc }], else_: null, loc, } satisfies IrStmt, ] : []), { kind: "exprStmt", expr: { kind: "arrIntrinsic", method: "push", receiver: outRef, args: [lowerer.coerceToExpected(valueRead, outElem)], type: F64, loc, }, loc, }, ]; lowerer.liftedFns.push({ name, params: [{ localId: "r.0", name: "r", type: recvT }], returnType: outT, locals: [ { id: "r.0", name: "r", type: recvT, mutable: false }, { id: "it.0", name: "it", type: cit.iterT, mutable: false }, { id: "out.0", name: "out", type: outT, mutable: false }, { id: "res.0", name: "res", type: cit.resultT, mutable: true }, ], body: [ { kind: "varDecl", localId: "out.0", init: { kind: "arrayLit", elems: [], type: outT, loc }, loc }, { kind: "varDecl", localId: "it.0", init: classIteratorOpenCall(lowerer, cit, recvRef, loc), loc }, { kind: "while", cond: { kind: "boolLit", value: true, type: BOOL, loc }, body: loop, loc, }, // Doneless iterators never leave the loop; satisfies the // all-paths-return rule (the retag-helper convention). { kind: "throw", value: { kind: "strLit", value: "scriptc: internal error: iterator drain fell through", type: STRING, loc }, loc, }, ], loc, }); } return { kind: "call", callee: name, args: [src], type: outT, loc }; } /** The tail of a class iterable's protocol from an already-open ITERATOR * object (`var [a, ...rest] = new C` — the rest element drains whatever * next() still yields): the drain loop keyed by the iterator class. */ export function classIteratorRestDrainCall(lowerer: Lowerer, cit: ClassIteratorInfo, itVal: IrExpr, loc: SrcLoc): IrExpr { const outT = arrayOf(cit.valueT); const key = `it:${cit.iterT.className}`; let name = lowerer.iterDrainHelpers.get(key); if (!name) { name = `%iter.drain.${lowerer.iterDrainHelpers.size}`; lowerer.iterDrainHelpers.set(key, name); const itRef: IrExpr = { kind: "varRef", localId: "it.0", type: cit.iterT, loc }; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: outT, loc }; const resRef: IrExpr = { kind: "varRef", localId: "res.0", type: cit.resultT, loc }; const loop: IrStmt[] = [ { kind: "varDecl", localId: "res.0", init: classIteratorNextCall(lowerer, cit, itRef, loc), loc }, ...(cit.hasDone ? [ { kind: "if", cond: { kind: "recordGet", obj: resRef, shapeId: cit.resultT.shapeId, field: "done", type: BOOL, loc }, then: [{ kind: "return", value: outRef, loc }], else_: null, loc, } satisfies IrStmt, ] : []), { kind: "exprStmt", expr: { kind: "arrIntrinsic", method: "push", receiver: outRef, args: [{ kind: "recordGet", obj: resRef, shapeId: cit.resultT.shapeId, field: "value", type: cit.valueT, loc }], type: F64, loc, }, loc, }, ]; lowerer.liftedFns.push({ name, params: [{ localId: "it.0", name: "it", type: cit.iterT }], returnType: outT, locals: [ { id: "it.0", name: "it", type: cit.iterT, mutable: false }, { id: "out.0", name: "out", type: outT, mutable: false }, { id: "res.0", name: "res", type: cit.resultT, mutable: true }, ], body: [ { kind: "varDecl", localId: "out.0", init: { kind: "arrayLit", elems: [], type: outT, loc }, loc }, { kind: "while", cond: { kind: "boolLit", value: true, type: BOOL, loc }, body: loop, loc }, { kind: "throw", value: { kind: "strLit", value: "scriptc: internal error: iterator drain fell through", type: STRING, loc }, loc, }, ], loc, }); } return { kind: "call", callee: name, args: [itVal], type: outT, loc }; } /** One method or accessor body as its module function `%C.name` * (accessors are methods with property syntax: "get:x"/"set:x" entries — * see collectClassShape). */ export function lowerClassMethodMember(lowerer: Lowerer, info: ClassInfo, fnLike: ts.MethodDeclaration | ts.AccessorDeclaration,): IrFunction | null { if (info.localClass?.ready) { const member = classMemberNameOf(lowerer, fnLike.name); const name = ts.isMethodDeclaration(fnLike) ? member : `${ts.isGetAccessor(fnLike) ? "get" : "set"}:${member}`; return info.localClass.bodies.get(`%${info.def.name}.${name}`) ?? null; } return withInstanceBindings(lowerer, info, () => lowerClassMethodMemberInner(lowerer, info, fnLike)); } function lowerClassMethodMemberInner(lowerer: Lowerer, info: ClassInfo, fnLike: ts.MethodDeclaration | ts.AccessorDeclaration,): IrFunction | null { const className = info.def.name; const thisType: IrType = { kind: "object", className }; const memberName = classMemberNameOf(lowerer, fnLike.name); if (memberName === null) return null; const mName = ts.isMethodDeclaration(fnLike) ? memberName : `${ts.isGetAccessor(fnLike) ? "get" : "set"}:${memberName}`; const sig = info.methods.get(mName); if (!sig || !fnLike.body) return null; const entryFence = info.methodEntryFences?.get(mName); if (entryFence) { lowerer.runtimeFences.push(entryFence); return lowerer.deferToRuntimeFence(lowerer.diags.length, fnLike.name, { kind: "function", name: `%${className}.${mName}`, params: [ { localId: "this.0", name: "this", type: thisType }, ...sig.params.map((p, i) => ({ localId: `p.${i}`, name: `p${i}`, type: p.type })), ], returnType: sig.ret, fallback: entryFence, allowDiagSink: true, }); } const prevClass = lowerer.currentClass; lowerer.currentClass = info; // ASYNC methods: the module function is an async IrFunction — its // body returns the promise's INNER type (a `return v` fulfills with // v) and every call enters through the emitted fiber spawn wrapper // (callTargetC routes by fn.async; `this` rides as param 0 in the // spawn's argument pack). Dispatch is static by construction — the // override fence at collection keeps async methods out of vtables. const isAsync = sig.async === true; // GENERATOR methods: the module function returns the TReturn channel, // yields ride ctx.generator, and direct calls enter through the emitted // gen-spawn wrapper with `this` in the argument pack. const genCh = sig.gen !== undefined && sig.ret.kind === "generator" ? sig.gen : null; const bodyReturn = genCh !== null ? lowerer.genBodyReturnType(sig.ret) : isAsync && sig.ret.kind === "promise" ? sig.ret.inner : sig.ret; const fnCtx = newFnCtx(info.localClass !== undefined, null, null, bodyReturn); fnCtx.isAsync = isAsync; if (genCh !== null) fnCtx.generator = genCh; lowerer.fnStack.push(fnCtx); try { const thisLocal = lowerer.declareThis(thisType); const params: IrParam[] = [{ localId: thisLocal.id, name: "this", type: thisType }]; // `this` is declared first, so method parameter DEFAULTS may use it // (JS allows this in method defaults; it is param 0 here). const declared = lowerer.declareParams(fnLike.parameters, sig.params); params.push(...declared.params); // A shorter JS override still implements every inherited ABI slot. // These tail parameters have no source binding or default prologue. for (const shape of sig.params.slice(declared.params.length)) { const name = shape.mode === "arguments" ? "%arguments" : "%ignored"; const local = lowerer.declareHiddenLocal(name, shape.type); params.push({ localId: local.id, name, type: shape.type }); if (shape.mode === "arguments") fnCtx.argumentsLocal = local; } const body = [...declared.prologue, ...lowerer.lowerStmts(fnLike.body.statements)]; appendImplicitUndefinedReturn(lowerer, body, bodyReturn, locOf(fnLike)); const fn: IrFunction = { name: `%${className}.${mName}`, params, returnType: bodyReturn, locals: lowerer.ctx.locals, ...classCaptureParams(info, lowerer.ctx), body, loc: locOf(fnLike), }; if (isAsync) fn.async = true; if (genCh !== null) fn.generator = genCh; return fn; } finally { lowerer.fnStack.pop(); lowerer.currentClass = prevClass; } } /** One static method body as its module function `%C.static:m` — an * ordinary function with NO `this` param. `this` and `super` inside * name the RECEIVER class in JS (dynamic — `F.who()` sees F even when * who() is declared on E), which has no static story here: both are * named fences, with arrow functions transparent (they inherit the * method's `this`) and this-binding function forms opaque — the static- * block rule verbatim. */ export function lowerStaticMethod(lowerer: Lowerer, info: ClassInfo, name: string): IrFunction | null { const entry = info.staticMethods?.get(name); if (!entry?.member.body) return null; const isAsync = entry.member.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; const isGenerator = entry.member.asteriskToken !== undefined; if (isGenerator && (entry.ret.kind !== "generator" || (entry.ret.async === true) !== isAsync)) { lowerer.badType(entry.member.name, lowerer.typeOf(entry.member.name)); } if (isAsync && !isGenerator && entry.ret.kind !== "promise") { lowerer.badType(entry.member.name, lowerer.typeOf(entry.member.name)); } const genCh = isGenerator && entry.ret.kind === "generator" ? generatorMeta(lowerer, entry.ret) : null; const bodyReturn = genCh !== null ? lowerer.genBodyReturnType(entry.ret) : isAsync && entry.ret.kind === "promise" ? entry.ret.inner : entry.ret; const fnCtx = newFnCtx(false, null, null, bodyReturn); fnCtx.isAsync = isAsync; if (genCh !== null) fnCtx.generator = genCh; lowerer.fnStack.push(fnCtx); try { rejectStaticThis( lowerer, entry.member.body, (keyword) => `'${keyword}' in static methods (it names the RECEIVER class — a dynamic value; reference the class by name instead)`, ); const declared = lowerer.declareParams(entry.member.parameters, entry.params); if (entry.params.at(-1)?.mode === "arguments") { const local = lowerer.declareHiddenLocal("%arguments", DYN); declared.params.push({ localId: local.id, name: "%arguments", type: DYN }); fnCtx.argumentsLocal = local; } const body = [...declared.prologue, ...lowerer.lowerStmts(entry.member.body.statements)]; appendImplicitUndefinedReturn(lowerer, body, bodyReturn, locOf(entry.member)); const fn: IrFunction = { name: `%${info.def.name}.static:${name}`, params: declared.params, returnType: bodyReturn, locals: lowerer.ctx.locals, body, loc: locOf(entry.member), }; if (isAsync) fn.async = true; if (genCh !== null) fn.generator = genCh; return fn; } catch (e) { // A poison OUTSIDE the per-statement catches (the this/super fence, // a fenced parameter default): the diagnostic is recorded — the // method skips like a signature-blocked function (lowerFunction's // rule) instead of killing the whole analysis. if (!(e instanceof PoisonError)) throw e; return null; } finally { lowerer.fnStack.pop(); } } /** A synthesized throwing setter: a getter-only override shadows the * inherited pair (JS), so a base-typed write dispatches HERE and must * throw exactly like Node's TypeError — a real instance (a typed catch's * `e instanceof TypeError` matches), catchable, exit 1 uncaught (message * text is compiler-worded; stdout and exit code are the contract). */ export function throwingSetterFn(lowerer: Lowerer, info: ClassInfo, prop: string): IrFunction { const className = info.def.name; const thisType: IrType = { kind: "object", className }; const sig = info.methods.get(`set:${prop}`)!; const loc = locOf(info.decl!); const locals: IrLocal[] = [ { id: "this.0", name: "this", type: thisType, mutable: false }, { id: "v.0", name: "v", type: sig.params[0]!.type, mutable: false }, ]; return { name: `%${className}.set:${prop}`, params: locals.map((l) => ({ localId: l.id, name: l.name, type: l.type })), returnType: VOID, locals, body: [ { kind: "throw", value: { kind: "libCall", fn: "error.new", args: [ { kind: "strLit", value: `Cannot set property ${prop} which has only a getter`, type: STRING, loc, }, ], type: { kind: "object", className: "%TypeError" }, loc, }, loc, }, ], loc, }; } /** The base a constructor chain actually CALLS into: generic FAMILIES are * never constructed (no `%.constructor` exists), so an * instantiation's construction-relevant base is the family's own base — * null when the generic class extends nothing, exactly the source's * story (tsc forbids super() there). Ordinary classes answer their base * unchanged. */ function superBaseOf(info: ClassInfo): ClassInfo | null { const b = info.base; return b?.generic ? b.base : b; } /** The class's OWN field initializers as fieldSet statements (declaration * order) — a base constructor's prologue, a derived constructor's * super()-return continuation. */ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrLocal): IrStmt[] { const out: IrStmt[] = []; const thisType: IrType = { kind: "object", className: info.def.name }; for (const f of info.fieldOrder) { if (f.undefinedInitializer) { const loc = f.undefinedInitializer; out.push({ kind: "fieldSet", obj: { kind: "varRef", localId: thisLocal.id, type: thisType, loc }, className: info.def.name, field: f.name, value: dynUndefinedExpr(loc), loc, }); } if (!f.initializer) continue; lowerer.stats.statementsTotal++; lowerer.bumpFileStat(locOf(f.initializer).file, "total"); const diagsBefore = lowerer.diags.length; try { const value = lowerer.lowerExprExpecting(f.initializer, f.type); out.push({ kind: "fieldSet", obj: { kind: "varRef", localId: thisLocal.id, type: thisType, loc: locOf(f.initializer) }, className: info.def.name, field: f.name, value, loc: locOf(f.initializer), }); } catch (e) { if (!(e instanceof PoisonError)) throw e; lowerer.stats.statementsFailed++; lowerer.bumpFileStat(locOf(f.initializer).file, "failed"); if (isJsSourceFile(f.initializer.getSourceFile()) && lowerer.diagSink === null) { const fence = lowerer.deferToRuntimeFence(diagsBefore, f.initializer, { kind: "statement" }); if (fence) out.push(fence); } } } return out; } /** PARAMETER-PROPERTY assignments (`this.x = x`, synthesized): run AFTER * the field initializers — Node's transform defines the fields at the * top of the class body (undefined until assigned) and injects the * assignments at the start of the constructor body, i.e. after super() * and after the (native) field initializers ran (probed: a field * initializer reading `this.x` sees undefined; the body sees the value). * Each assignment reads the parameter's BODY local (defaults already * applied by the declareParams prologue), whose type the collection made * the field's type — slot-exact by construction. */ function paramPropInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrLocal): IrStmt[] { const out: IrStmt[] = []; const thisType: IrType = { kind: "object", className: info.def.name }; for (const pp of info.paramProps ?? []) { const loc = locOf(pp.param); const local = ts.isIdentifier(pp.param.name) ? lowerer.resolveLocal(pp.param.name) : null; if (!local || !typeEquals(local.type, pp.type)) { // Defensive: collection derived the field type from the same // paramShape the ctor's declareParams bound — they cannot diverge. lowerer.unsupported("SC1090", pp.param, "this parameter property form"); } out.push({ kind: "fieldSet", obj: { kind: "varRef", localId: thisLocal.id, type: thisType, loc }, className: info.def.name, field: pp.name, value: { kind: "varRef", localId: local!.id, type: local!.type, loc }, loc, }); } return out; } /** A derived constructor's body: statements lower as usual EXCEPT the * top-level `super(...)` statement, which becomes a direct call to the * base constructor over the same `this`, immediately followed by this * class's field initializers (JS runs them when super returns). tsc * guarantees a super call exists and runs before any this-use; the * supported form is a top-level expression statement — anywhere else * (conditionals, expression positions) is rejected, not misordered. */ export function lowerDerivedCtorBody(lowerer: Lowerer, info: ClassInfo, thisLocal: IrLocal, /** Mixin forwarding-constructor mode: `super(...args)` forwards these * pre-declared synthetic params directly (the spread never lowers — * the base's ABI is this constructor's ABI). */ forward?: IrExpr[],): IrStmt[] { const out: IrStmt[] = []; let superSeen = false; const statements = info.ctor!.body!.statements; const entry = { stmts: statements, index: 0, ctx: lowerer.ctx, frame: lowerer.scopes[lowerer.scopes.length - 1]!, out }; lowerer.activeStmtLists.push(entry); try { for (let index = 0; index < statements.length; index++) { entry.index = index; const stmt = statements[index]!; const superCall = ts.isExpressionStatement(stmt) && ts.isCallExpression(stmt.expression) && stmt.expression.expression.kind === ts.SyntaxKind.SuperKeyword ? stmt.expression : null; if (!superCall) { out.push(...lowerer.lowerStmts([stmt])); continue; } if (!lowerer.suppressStats) { lowerer.stats.statementsTotal++; lowerer.bumpFileStat(locOf(stmt).file, "total"); } try { if (superSeen) lowerer.unsupported("SC1090", stmt, "multiple super() calls"); superSeen = true; if (info.callableBase) { if (superCall.arguments.some(ts.isSpreadElement)) lowerer.unsupported("SC1090", superCall, "spreading explicit super arguments into an ordinary constructor"); const loc = locOf(stmt); const pack: IrExpr = { kind: "dynArrLit", elems: superCall.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc }; out.push(lowerer.superCallStmt(info, thisLocal, [pack], loc)); out.push(...lowerer.fieldInitStmts(info, thisLocal)); out.push(...paramPropInitStmts(lowerer, info, thisLocal)); continue; } const base = superBaseOf(info)!; if (base.builtinEmitter && superCall.arguments.length > 0) { // @types/node admits super({ captureRejections }) — no lowering. lowerer.unsupported("SC1090", superCall, "EventEmitter constructor options ('captureRejections')"); } if (base.builtinStream) { // super(options?) into a runtime stream base: the stream spoke // parses the options and binds overridden underscore methods. out.push(...lowerStreamSuperCall(lowerer, info, base, superCall.arguments, thisLocal, locOf(stmt), stmt)); out.push(...lowerer.fieldInitStmts(info, thisLocal)); out.push(...paramPropInitStmts(lowerer, info, thisLocal)); continue; } const surplusSpread = forward === undefined && !base.builtinError && !base.builtinEmitter && base.ctorParams.length === 0 && superCall.arguments.some(ts.isSpreadElement); if (surplusSpread) { // A parameterless base still evaluates and iterates every // supplied argument before its constructor starts. const evaluated = lowerer.completeArgs(superCall.arguments, [{ mode: "dynRest", type: DYN }], locOf(stmt), stmt); for (const value of evaluated) out.push({ kind: "exprStmt", expr: value, loc: locOf(stmt) }); } const args = surplusSpread ? [] : forward !== undefined ? forward : base.builtinError ? lowerer.errorConstructorArgs(superCall.arguments, locOf(stmt), stmt) : base.builtinEmitter ? [] : lowerer.completeArgs(superCall.arguments, base.ctorParams, locOf(stmt), stmt); out.push(lowerer.superCallStmt(info, thisLocal, args, locOf(stmt))); // super() returns → field initializers → parameter-property // assignments (Node's order, probed) → the rest of the body. out.push(...lowerer.fieldInitStmts(info, thisLocal)); out.push(...paramPropInitStmts(lowerer, info, thisLocal)); } catch (e) { if (!(e instanceof PoisonError)) throw e; if (!lowerer.suppressStats) { lowerer.stats.statementsFailed++; lowerer.bumpFileStat(locOf(stmt).file, "failed"); } } } } finally { lowerer.activeStmtLists.pop(); } if (!superSeen) { // tsc guarantees the call exists somewhere; if it wasn't a top-level // statement the per-site rejection above already fired — this is the // constructor-level backstop so a half-initialized ctor never emits. lowerer.pushDiag( unsupportedDiag( "SC1090", locOf(info.ctor!), "super() calls anywhere but as a top-level constructor statement", ), ); } return out; } /** `super(args)` → direct call of the base constructor with the SAME * `this` (upcast; retained by the varRef read — the callee owns and * releases its param per the universal convention). */ export function superCallStmt(lowerer: Lowerer, info: ClassInfo, thisLocal: IrLocal, args: IrExpr[], loc: SrcLoc,): IrStmt { const base = superBaseOf(info)!; const thisRef: IrExpr = { kind: "varRef", localId: thisLocal.id, type: { kind: "object", className: info.def.name }, loc, }; if (info.callableBase) return { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.classSuper", args: [info.localClass ? { kind: "libCall", fn: "dyn.getPrototype", args: [ { kind: "dynFrom", value: { kind: "fieldGet", obj: thisRef, className: info.def.name, field: `%classEnvironment:${info.def.name}`, type: { kind: "classval", className: info.def.name }, loc }, type: DYN, loc }, ], type: DYN, loc } : { kind: "varRef", localId: info.callableBase.constructorId, type: DYN, loc }, lowerer.coerceToExpected(thisRef, DYN), args[0]!], type: VOID, loc }, loc, }; if (base.builtinError) { // super(message) into the runtime-provided Error constructor: stamps // name/message on the (already-allocated) object. Receiver + message // are BORROWED by the libCall — no ownership transfer, unlike the // call form below. return { kind: "exprStmt", expr: { kind: "libCall", fn: "error.ctorOptions", args: [lowerer.upcastTo(thisRef, base.def.name), ...args], type: VOID, loc, }, loc, }; } if (base.builtinEmitter) { // super() into the runtime-provided EventEmitter: the emitted // allocation already initialized the prefix (registry NULL, display // name stamped), so the call is a placeholder site. Receiver // borrowed, like error.ctor. return { kind: "exprStmt", expr: { kind: "libCall", fn: "emitter.ctor", args: [lowerer.upcastTo(thisRef, base.def.name)], type: VOID, loc, }, loc, }; } if (base.builtinStream) { // The SYNTHESIZED constructor of a ctor-less stream subclass: // super() with default options (underscore methods still bind; a // construction passing options requires a declared constructor — // lowerNew fences that). Zero options ⇒ exactly one init stmt. return lowerStreamSuperCall(lowerer, info, base, [], thisLocal, loc, info.decl ?? info.ctor!)[0]!; } lowerer.noteEdge(`%${base.def.name}.constructor`); return { kind: "exprStmt", expr: { kind: "call", callee: `%${base.def.name}.constructor`, args: [lowerer.upcastTo(thisRef, base.def.name), ...args], type: VOID, loc, }, loc, }; } /** `super.method(args)`: the base chain's implementation, called * DIRECTLY over this method's own `this` (upcast to the declarer) — * super dispatch is static in JS too, never through the dynamic class. */ export function lowerSuperMethodCall(lowerer: Lowerer, call: ts.CallExpression, access: ts.PropertyAccessExpression,): IrExpr { const cls = lowerer.currentClass; if (!cls?.base) { // tsc rejects super outside derived-class bodies first; defensive. lowerer.unsupported("SC1090", access, "'super' outside a derived class"); } // An emit-override SPECIALIZATION body's forward — `super.emit(event, // ...args)` — carries no literal event name; the specialization's own // context answers it (matched before any lookup: neither identifier // resolves through the ordinary lowering). const forward = emitSpecSuperForward(lowerer, call, access); if (forward) return forward; const found = lowerer.findMethodOn(cls.base, access.name.text); if (!found) { // `super.m(...)` of a GENERIC method: super dispatch is static in JS // too, so the base chain's declaration answers unconditionally — the // ordinary instantiation route over this method's own `this`. const gfound = findGenericMethodOn(lowerer, cls.base, access.name.text); if (gfound) { const thisL = lowerer.resolveThis(); if (!thisL) lowerer.unsupported("SC1080", access); const instance = genericCallInstance(lowerer, call, gfound.info); const loc = locOf(call); const thisRef: IrExpr = { kind: "varRef", localId: thisL.id, type: thisL.type, loc }; const args = lowerer.completeArgs(call.arguments, instance.params, loc, call); return { kind: "call", callee: instance.name, args: [lowerer.upcastTo(thisRef, gfound.declarer.def.name), ...args], type: instance.returnType, loc, }; } // The runtime-provided emitter surface through `super` — // `super.emit('x', v)`, `super.on(...)`: Node's prototype-chain rule // is STATIC dispatch above the lexical class, which the emitter // spoke lowers with this method's own `this` as the receiver (an // emit override at-or-below `cls` never answers; the nearest one // strictly above does). if (EMITTER_API_MEMBERS.has(access.name.text) && emitterRooted(lowerer, cls.base)) { const viaEmitter = lowerEmitterSuperCall(lowerer, call, access, cls); if (viaEmitter) return viaEmitter; } lowerer.unsupported("SC1090", access, `'super.${access.name.text}' (no base class declares it)`); } // tsc rejects super-access of abstract members (TS2513); defensive — // no function exists behind an abstract declaration. if (found.sig.abstract === true) { lowerer.unsupported("SC1090", access, `'super.${access.name.text}' of an abstract method`); } const thisLocal = lowerer.resolveThis(); if (!thisLocal) lowerer.unsupported("SC1080", access); const loc = locOf(call); const thisRef: IrExpr = { kind: "varRef", localId: thisLocal.id, type: thisLocal.type, loc }; if (lowerer.prototypeMethodAccesses.has(access.name.text)) { const prototype = classPrototypeData(lowerer, cls.base, loc); if (prototype) { const callee: IrExpr = { kind: "dynKeyGet", value: prototype, key: { kind: "strLit", value: access.name.text, type: STRING, loc }, type: DYN, loc }; const value: IrExpr = { kind: "dynCall", callee, receiver: lowerer.coerceToExpected(thisRef, DYN), calleeName: access.getText(), args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc }; return found.sig.ret.kind === "void" ? value : lowerer.coerceToExpected(value, found.sig.ret); } } if (found.declarer.builtinError) { return { kind: "libCall", fn: "error.toString", args: [lowerer.upcastTo(thisRef, "%Error")], type: STRING, loc }; } lowerer.noteEdge(`%${found.declarer.def.name}.${access.name.text}`); const args = lowerer.completeArgs(call.arguments, found.sig.params, loc, call); return { kind: "call", callee: `%${found.declarer.def.name}.${access.name.text}`, args: [lowerer.upcastTo(thisRef, found.declarer.def.name), ...args], type: found.sig.ret, loc, }; } /** The `this` reference for super accessor reads/writes, with the shared * validity checks (derived-class body, resolvable this). */ export function superThisRef(lowerer: Lowerer, access: ts.PropertyAccessExpression): { thisRef: IrExpr; base: ClassInfo } { const cls = lowerer.currentClass; if (!cls?.base) { lowerer.unsupported("SC1090", access, "'super' outside a derived class"); } const thisLocal = lowerer.resolveThis(); if (!thisLocal) lowerer.unsupported("SC1080", access); const loc = locOf(access); return { thisRef: { kind: "varRef", localId: thisLocal.id, type: thisLocal.type, loc }, base: cls.base, }; } /** `super.x` read: a DIRECT call of the base chain's getter over this * method's own `this` (upcast to the declarer) — like super.method(), * never through the vtable. */ export function lowerSuperAccessorRead(lowerer: Lowerer, access: ts.PropertyAccessExpression): IrExpr { const { thisRef, base } = lowerer.superThisRef(access); const name = access.name.text; const found = lowerer.findMethodOn(base, `get:${name}`); if (!found) { lowerer.unsupported( "SC1090", access, lowerer.findMethodOn(base, name) ? `bound method references through 'super' (call 'super.${name}(...)' directly)` : `'super.${name}' (only base-class methods and getter properties are readable through 'super')`, ); } // tsc rejects super-access of abstract members (TS2513); defensive. if (found.sig.abstract === true) { lowerer.unsupported("SC1090", access, `'super.${name}' of an abstract accessor`); } lowerer.noteEdge(`%${found.declarer.def.name}.get:${name}`); return { kind: "call", callee: `%${found.declarer.def.name}.get:${name}`, args: [lowerer.upcastTo(thisRef, found.declarer.def.name)], type: found.sig.ret, loc: locOf(access), }; } /** `super.x = v`: a DIRECT call of the base chain's setter (same * static-dispatch rule as every super member access). */ export function lowerSuperAccessorWrite(lowerer: Lowerer, access: ts.PropertyAccessExpression, rhs: ts.Expression, loc: SrcLoc,): IrStmt { const { thisRef, base } = lowerer.superThisRef(access); const name = access.name.text; const found = lowerer.findMethodOn(base, `set:${name}`); if (!found) { lowerer.unsupported( "SC1090", access, `assignment to 'super.${name}' (no base class declares a setter for it)`, ); } // tsc rejects super-access of abstract members (TS2513); defensive. if (found.sig.abstract === true) { lowerer.unsupported("SC1090", access, `assignment to 'super.${name}' of an abstract accessor`); } lowerer.noteEdge(`%${found.declarer.def.name}.set:${name}`); const value = lowerer.lowerExprExpecting(rhs, found.sig.params[0]!.type); return { kind: "exprStmt", expr: { kind: "call", callee: `%${found.declarer.def.name}.set:${name}`, args: [lowerer.upcastTo(thisRef, found.declarer.def.name), value], type: VOID, loc, }, loc, }; } /** True when `info`'s EFFECTIVE constructor — its own, or the one * inherited through ctor-less bases — is a builtin error class's. Such * classes construct with the error argument rule, and their synthesized * constructors forward both checked-dynamic values to error.ctorOptions. */ export function inheritsBuiltinErrorCtor(lowerer: Lowerer, info: ClassInfo): boolean { for (let c: ClassInfo | null = info; c; c = c.base) { if (c.builtinError) return true; if (c.ctor) return false; } return false; } /** The EventEmitter twin: a ctor-less chain into the emitter base * inherits `new C()` — zero arguments (the options bag fences). */ export function inheritsBuiltinEmitterCtor(lowerer: Lowerer, info: ClassInfo): boolean { for (let c: ClassInfo | null = info; c; c = c.base) { if (c.builtinStream) return false; // the stream story owns the chain if (c.builtinEmitter) return true; if (c.ctor) return false; } return false; } /** The stream twin: a ctor-less chain into a runtime stream base * inherits `new C()` — zero arguments (the synthesized constructor runs * super() with default options; passing options through an inherited * constructor would need the literal at the new-site to plumb, so it * asks for a declared constructor instead). */ function inheritsBuiltinStreamCtor(lowerer: Lowerer, info: ClassInfo): boolean { for (let c: ClassInfo | null = info; c; c = c.base) { if (c.builtinStream) return true; if (c.ctor) return false; } return false; } /** Preserve argument evaluation order and option presence through builtin * and inherited Error constructors. Checked-dynamic conversion retains * Error identity and distinguishes a missing cause from undefined. */ export function errorConstructorArgs(lowerer: Lowerer, args: readonly ts.Expression[], loc: SrcLoc, blame: ts.Node): IrExpr[] { if (args.length > 2) lowerer.unsupported("SC1090", blame, "Error constructors with more than two arguments"); return [0, 1].map((i) => { const arg = args[i]; if (!arg) return dynUndefinedExpr(loc); return lowerer.lowerExprExpecting(arg, DYN); }); } /** `new C(...)` of a registered PROGRAM class — the shared tail of the * identifier and namespace-qualified construction forms. */ /** `new Box(1)` / `new Box("s")` — construction of a GENERIC * class resolves to the INSTANTIATION the expression's checker type names * (inference and explicit type arguments both land there; defaults apply). * The identity function for ordinary classes. */ function genericNewTarget(lowerer: Lowerer, expr: ts.NewExpression, info: ClassInfo): ClassInfo { if (!info.generic) return info; const t = lowerer.typeOf(expr); const mapped = lowerer.mapTypeOf(t); const instInfo = mapped?.kind === "object" ? lowerer.classes.get(mapped.className) : undefined; // Unmappable type arguments (or a poisoned instantiation): the site // reports the type it cannot compile — the instantiation's own // diagnostic (context-tagged) already told the deeper story. if (!instInfo || instInfo.generic) lowerer.badType(expr, t); return instInfo; } /** A class whose decoration provably throws has no reachable VALUE form: * the binding never initializes (the %init ReferenceError unwinds first), * so `new`, the class as a value, and `extends` all fence — reaching one * in compiled code would require executing past the throw. */ function fenceDecorationThrows(lowerer: Lowerer, info: ClassInfo, blame: ts.Node): void { if (info.decorationThrows === undefined) return; lowerer.unsupported( "SC1090", blame, `using the class '${info.def.jsName || info.def.name}' whose decoration provably throws ('${info.decorationThrows.name}' is an ambient name nothing defines — the class statement crashes before the binding exists)`, ); } function lowerProgramClassNew(lowerer: Lowerer, expr: ts.NewExpression, declaredInfo: ClassInfo, loc: SrcLoc): IrExpr { const info = genericNewTarget(lowerer, expr, declaredInfo); fenceDecorationThrows(lowerer, info, expr); if (info.localClass) lowerer.unsupported("SC1090", expr, "construction through a function-local class's internal name"); lowerer.noteEdge(`%${info.def.name}.constructor`); // A ctor-less chain into an EventEmitter base inherits `new C()` — // zero arguments (the options bag fences, like the super() form). // Stream subclasses come first: their chain roots at the emitter // too, but the message should name the stream story. if (inheritsBuiltinStreamCtor(lowerer, info) && (expr.arguments ?? []).length > 0) { lowerer.noLowering( `new ${info.def.name.replace(/^%/, "")} with arguments through an inherited stream constructor`, expr.arguments![0]!, "declare a constructor that passes an inline options object to super(...)", ); } if (lowerer.inheritsBuiltinEmitterCtor(info) && (expr.arguments ?? []).length > 0) { lowerer.unsupported("SC1090", expr.arguments![0]!, "EventEmitter constructor options ('captureRejections')"); } // A ctor-less chain into a builtin error base inherits both arguments; // each synthesized constructor forwards the same pair to its base. const args = lowerer.inheritsBuiltinErrorCtor(info) ? lowerer.errorConstructorArgs(expr.arguments ?? [], loc, expr) : lowerer.completeArgs(expr.arguments ?? [], info.ctorParams, loc, expr); for (let current: ClassInfo | null = info; current; current = current.base) { if (current.def.baseValueGlobal) return { kind: "newValue", callee: classValueRef(lowerer, info, expr), args, type: { kind: "object", className: info.def.name }, loc }; } return { kind: "new", className: info.def.name, args, type: { kind: "object", className: info.def.name }, loc, }; } function assignedThisFieldType(lowerer: Lowerer, expr: ts.NewExpression): IrType | null { const parent = expr.parent; if ( !ts.isBinaryExpression(parent) || parent.operatorToken.kind !== ts.SyntaxKind.EqualsToken || parent.right !== expr || !ts.isPropertyAccessExpression(parent.left) || parent.left.expression.kind !== ts.SyntaxKind.ThisKeyword ) { return null; } return lowerer.currentClass?.fields.get(parent.left.name.text) ?? null; } export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const loc = locOf(expr); const instanceConstructor = lowerInstanceConstructorNew(lowerer, expr); if (instanceConstructor) return instanceConstructor; const objectFactory = lowerObjectFactoryNew(lowerer, expr); if (objectFactory) return objectFactory; let selected: ts.Expression = expr.expression; while (ts.isParenthesizedExpression(selected)) selected = selected.expression; if (ts.isConditionalExpression(selected)) { const yes = exactClassOfReceiver(lowerer, selected.whenTrue); const no = exactClassOfReceiver(lowerer, selected.whenFalse); if (yes && no && !yes.classDecorators && !no.classDecorators) { const yesType: IrType = { kind: "object", className: yes.def.name }; const noType: IrType = { kind: "object", className: no.def.name }; const type: IrType = yes === no ? yesType : { kind: "union", unionId: lowerer.unions.intern([yesType, noType].sort((a, b) => typeKey(a) < typeKey(b) ? -1 : 1)) }; return { kind: "ternary", cond: lowerer.lowerCondition(selected.condition), then: lowerer.coerceInto(expr, lowerProgramClassNew(lowerer, expr, yes, loc), type), else_: lowerer.coerceInto(expr, lowerProgramClassNew(lowerer, expr, no, loc), type), type, loc }; } } if (ts.isPropertyAccessExpression(expr.expression) && expr.expression.name.text === "Segmenter" && lowerer.isStdlibGlobal(expr.expression.expression, "Intl")) { const args = expr.arguments ?? []; const undefinedArg = (arg: ts.Expression): boolean => (lowerer.typeOf(arg).flags & ts.TypeFlags.Undefined) !== 0; if (args.length > 2 || (args[0] && !undefinedArg(args[0]))) { lowerer.noLowering("Intl.Segmenter locale negotiation", expr, "omit the locale or pass undefined for default Unicode grapheme segmentation"); } const options = args[1]; if (options && !undefinedArg(options) && !(ts.isObjectLiteralExpression(options) && options.properties.every((property) => ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) && property.name.text === "granularity" && ts.isStringLiteral(property.initializer) && property.initializer.text === "grapheme"))) { lowerer.noLowering("Intl.Segmenter options", options, 'the native segmenter supports default options and { granularity: "grapheme" }; word and sentence segmentation remain unsupported'); } // Explicit undefined expressions can carry effects; retain argument order. const stmts: IrStmt[] = args.map((arg) => { const value = lowerer.lowerExpr(arg); return { kind: "exprStmt", expr: value.type.kind === "void" ? value : lowerer.coerceInto(arg, value, DYN), loc: locOf(arg) }; }); const result: IrExpr = { kind: "libCall", fn: "intl.segmenterNew", args: [], type: DYN, loc }; return stmts.length ? { kind: "seqExpr", stmts, result, type: DYN, loc } : result; } const consoleCtor = ts.isIdentifier(expr.expression) ? lowerer.builtinImportOf(expr.expression) : ts.isPropertyAccessExpression(expr.expression) && lowerer.builtinNamespaceModuleOf(expr.expression.expression) === "console" ? { module: "console", member: expr.expression.name.text } : null; if (consoleCtor?.module === "console" && consoleCtor.member === "Console") { lowerer.noLowering("new console.Console", expr, builtinFenceHintOf("console", "Console")); } // `new X(...)` where X is a package-declared class, in a static build: // the per-package requires-dynamic diagnostic (the constructor runs in // the embedded engine). Under --dynamic, X is jsval-typed and lowers // to the construct op below. if (!lowerer.dynamic) { const pkg = ts.isIdentifier(expr.expression) ? lowerer.npmPackageOfSymbol(lowerer.resolveValueSymbol(expr.expression) ?? undefined) : null; if (pkg) { lowerer.pushDiag(requiresDynamicPackageDiag(pkg, loc)); throw new PoisonError(); } } // Island construction: a jsval-typed callee (a package-declared class, // or any 'any'-typed constructor value) runs JS_CallConstructor — // `new Command()` is the npm entry point. Arguments marshal in; the // instance stays an island handle. if (lowerer.isIslandExpr(expr.expression)) { const callee = lowerer.lowerExpr(expr.expression); const args = (expr.arguments ?? []).map((a) => lowerer.jsvalIn(lowerer.lowerExpr(a), a)); return { kind: "jsOp", op: "construct", args: [callee, ...args], type: JSVAL, loc }; } // `new events.EventEmitter()` — the namespace-member (and CJS // `require('events').EventEmitter`) construction form: the property's // symbol resolves to the same ambient class as the named import. if (ts.isPropertyAccessExpression(expr.expression) && ts.isIdentifier(expr.expression.name)) { const memberSym = lowerer.checker.getSymbolAtLocation(expr.expression.name); const resolved = memberSym && memberSym.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(memberSym) : memberSym; const emitterInfo = lowerer.builtinEmitterInfoOf(resolved); if (emitterInfo) { if ((expr.arguments ?? []).length > 0) { lowerer.unsupported("SC1090", expr.arguments![0]!, "EventEmitter constructor options ('captureRejections')"); } return { kind: "libCall", fn: "emitter.new", args: [], type: { kind: "object", className: RUNTIME_EMITTER_CLASS }, loc, }; } // `new stream.Readable({...})` — the namespace-member (and CJS // `require('stream').Readable`) construction form. const streamInfoNs = builtinStreamInfoOf(lowerer, resolved); if (streamInfoNs) return lowerStreamNew(lowerer, expr, streamInfoNs); // `new N.C(...)` / `new a.Point(...)` — construction through a // USER namespace qualifier (import= alias chains included): the // member resolves to the registered program class, guarded by the // namespace source-order fences (lower-namespaces.ts). if (!expr.expression.questionDotToken && nsMemberIdentOf(lowerer, expr.expression)) { if (memberSym) fenceEarlyNsMemberRef(lowerer, expr.expression, memberSym); // resolveValueSymbol (not the bare alias chase): the reference // must flush deferred collection diagnostics like any other. const classSym = lowerer.resolveValueSymbol(expr.expression.name); const info = classSym ? lowerer.classBySymbol.get(classSym) : undefined; // Qualified spellings of a rebindable decorated class (an import= // alias chain landing on it) cannot construct the declaration // directly — the decoration result decides. The bare-name path // routes through the class VALUE; the qualified one fences. if (info?.classDecorators?.valueGlobalId !== undefined) { lowerer.unsupported( "SC1090", expr, "constructing a decorated class through a qualified name (a replacing decorator rebinds the class name — construct through the bare name)", ); } if (info && !info.localClass) return lowerProgramClassNew(lowerer, expr, info, loc); // A namespace export can hold a class returned by a factory. // Let the class-value path below preserve that constructor's // lexical environment instead of requiring a class declaration. } // `new B.C()` where B is an AMBIENT namespace (fundule merges // included): Node evaluates the callee first and throws // ReferenceError before any argument runs — undefRead reproduces it // exactly. if (!expr.expression.questionDotToken) { const ambientRoot = ambientNsRootOf(lowerer, expr.expression.expression); if (ambientRoot !== null) { const t = ambientUndefReadType(lowerer, expr); if (t) return nsUndefRead(lowerer, ambientRoot.text, expr, t); } } // Construction through a CJS export member tsgo types `any` // (expando members — `new module.exports.Sub()` / `new // exports.Sub()` in-file, `new C.Sub()` through the require // binding): the member IS its pre-registered export global — // construction dispatches through the class VALUE, the classval // path's newValue with the global as the callee. Resolution is the // member-export symbol's; the class collects on demand (a body // lowering ahead of the assignment statement). if ( !expr.expression.questionDotToken && ((isCjsJsFile(expr.getSourceFile(), lowerer.program) && (isModuleExportsAccess(expr.expression.expression) || (ts.isIdentifier(expr.expression.expression) && expr.expression.expression.text === "exports" && !lowerer.peekLocal(expr.expression.expression) && !lowerer.globalOf(expr.expression.expression)))) || lowerer.cjsLocalModuleBindingOf(expr.expression.expression)) ) { // Candidate symbols for the export global: the member symbol as // spelled, its alias-chased resolution (resolveValueSymbol carries // the dep-module fallback tsgo needs at member-use sites), and the // in-file module-export symbol. const candidates = [ memberSym, resolved, lowerer.resolveValueSymbol(expr.expression.name) ?? undefined, lowerer.cjsModuleExportSymbol(expr.getSourceFile(), expr.expression.name.text), ]; const exportSym = candidates.find((s) => s !== undefined); const g = candidates .map((s) => (s ? lowerer.globalsBySymbol.get(s) : undefined)) .find((x) => x !== undefined); if (g && g.type.kind === "classval") { const info = lowerer.classes.get(g.type.className) ?? propertyAssignedClassInfoOf(lowerer, exportSym) ?? lowerer.classes.get(g.type.className); if (info && !info.generic) { lowerer.noteEdge(`%${info.def.name}.constructor`); const below = (c: ClassInfo): void => { for (const s of c.subclasses) { lowerer.noteEdge(`%${s.def.name}.constructor`); below(s); } }; below(info); const callee: IrExpr = { kind: "varRef", localId: g.id, type: g.type, loc }; const args = lowerer.completeArgs(expr.arguments ?? [], info.ctorParams, loc, expr); return { kind: "newValue", callee, args, type: { kind: "object", className: info.def.name }, loc, }; } } } } // `new http.Server([options][, handler])` — the constructor spelling // of http.createServer (Node's Server class IS the factory's // product); routed to lower-server ahead of the stdlib-ctor fences. { const httpServer = lowerHttpServerNew(lowerer, expr); if (httpServer) return httpServer; } // `new http.Agent(opts?)` / `new https.Agent(opts?)` — the Agent // handle (lower-server): getName/destroy/counters through the dyn // handle ops, requests thread it via the agent option. { const agent = lowerHttpAgentNew(lowerer, expr); if (agent) return agent; } if (ts.isIdentifier(expr.expression)) { // `import C = N.C; new C()` — the alias's own source-order guards // (a no-op for every non-import= binding). fenceEarlyAliasUse(lowerer, expr.expression, expr); const symbol = lowerer.resolveValueSymbol(expr.expression); if (isNativeProxyInitializer(lowerer, expr)) return lowerNativeProxy(lowerer, expr); // `new Error(msg?)` and its standard subclasses: the // runtime-provided classes construct through one libCall — the result // TYPE names which builtin, and the message completes to "" exactly // like Node's message property default. const errInfo = lowerer.builtinErrorInfoOf(symbol); // `new DOMException(message?, nameOrOptions?)`: both arguments cross // as dyn values (absent → the dyn undefined), and the runtime owns // WebIDL's resolution — ToString of the message ("" for undefined), // name from a string / an options object's `name` member (with the // `cause` own-property record) / "Error" for absent, and the legacy // numeric code from the name table. if (errInfo && errInfo.def.name === "%DOMException") { const args = expr.arguments ?? []; if (args.length > 2) { lowerer.noLowering(`new DOMException with ${args.length} arguments`, expr); } const toDynArg = (a: ts.Expression | undefined): IrExpr => { if (!a) return dynUndefinedExpr(loc); const v = lowerer.lowerExpr(a); if (v.type.kind === "dyn") return v; if (v.kind === "unitLit" || (v.type.kind !== "jsval" && lowerer.dynConvertible(v.type))) { return { kind: "dynFrom", value: v, type: DYN, loc }; } lowerer.noLowering( `new DOMException with a '${lowerer.fmt(v.type)}' argument`, a, "message strings and string/options-object names lower (Node ToStrings other values — convert explicitly)", ); }; const msgArg = toDynArg(args[0]); const nameArg = toDynArg(args[1]); return { kind: "libCall", fn: "error.newDom", args: [msgArg, nameArg], type: { kind: "object", className: "%DOMException" }, loc, }; } if (errInfo) { const args = lowerer.errorConstructorArgs(expr.arguments ?? [], loc, expr); return { kind: "libCall", fn: "error.newOptions", args, type: { kind: "object", className: errInfo.def.name }, loc, }; } // `new EventEmitter()`: the runtime-provided emitter constructs // through one libCall. Zero arguments — the options bag // (@types/node's captureRejections) has no lowering. const emitterInfo = lowerer.builtinEmitterInfoOf(symbol); if (emitterInfo) { if ((expr.arguments ?? []).length > 0) { lowerer.unsupported("SC1090", expr.arguments![0]!, "EventEmitter constructor options ('captureRejections')"); } return { kind: "libCall", fn: "emitter.new", args: [], type: { kind: "object", className: RUNTIME_EMITTER_CLASS }, loc, }; } // `new Readable({...})` and the other stream classes: the options // object parses structurally in the stream spoke. const streamInfo = builtinStreamInfoOf(lowerer, symbol); if (streamInfo) return lowerStreamNew(lowerer, expr, streamInfo); // `new URL(input)`: the WHATWG URL class (stdlib/@types provenance — // a user's own `class URL` resolves through classBySymbol below). // Invalid input or base throws a catchable TypeError ("Invalid URL"). // `new RegExp(pattern, flags?)`: runtime construction over the same // libregexp engine the literals ride. The pattern compiles EAGERLY, // so bad input throws Node's catchable SyntaxError at construction. // Checked arguments retain native regexes and perform runtime coercion. if (symbol && symbol.name === "RegExp" && lowerer.isStdlibSymbol(symbol)) { const args = expr.arguments ?? []; if (args.length > 2) { lowerer.noLowering(`new RegExp with ${args.length} arguments`, expr); } if (args.some(ts.isSpreadElement)) lowerer.noLowering("new RegExp with spread arguments", expr); const values = args.map((arg) => lowerer.lowerExpr(arg)); if (values.some((value) => value.type.kind === "dyn" || value.type.kind === "regex")) { const checked = [0, 1].map((index): IrExpr => values[index] ? lowerer.coerceInto(args[index]!, values[index]!, DYN) : { kind: "dynFrom", value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, type: DYN, loc }); return { kind: "libCall", fn: "regex.newChecked", args: checked, type: { kind: "regex" }, loc }; } const strArg = (a: ts.Expression | undefined, what: string): IrExpr => { if (!a) return { kind: "strLit", value: "", type: STRING, loc }; const v = values[args.indexOf(a)]!; const empty: IrExpr = { kind: "strLit", value: "", type: STRING, loc }; if (v.type.kind === "undefinedT") { if (v.kind === "unitLit") return empty; return { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: lowerer.coerceToExpected(v, DYN), loc: locOf(a) }], result: empty, type: STRING, loc }; } if (v.type.kind === "union" && lowerer.unions.get(v.type.unionId)?.arms.every( (arm) => arm.kind === "string" || arm.kind === "undefinedT", )) { return { kind: "nullish", left: v, right: empty, type: STRING, loc: locOf(a) }; } if (v.type.kind !== "string") { lowerer.noLowering( `new RegExp with a '${lowerer.fmt(v.type)}' ${what}`, a, "string or undefined arguments are the lowered form (a RegExp copy or ToString coercion has no lowering)", ); } return v; }; const pattern = strArg(args[0], "pattern"); const flags = strArg(args[1], "flags argument"); return { kind: "libCall", fn: "regex.new", args: [pattern, flags], type: { kind: "regex" }, loc }; } if (symbol && symbol.name === "URL" && lowerer.isStdlibSymbol(symbol)) { return lowerUrlNew(lowerer, expr); } // `new URLSearchParams(init?)`: the WHATWG list (stdlib provenance — // see lowerSearchParamsNew for the lowered init shapes). if (symbol && symbol.name === "URLSearchParams" && lowerer.isStdlibSymbol(symbol)) { return lowerSearchParamsNew(lowerer, expr, loc); } // Date's read-only value slice: store the constructor's TimeClip'd // epoch milliseconds as the scalar date kind. That is sufficient // for every getter and toISOString; identity and setters stay // fenced, so copying the scalar cannot create an observable lie. if (symbol && symbol.name === "Date" && lowerer.isStdlibSymbol(symbol)) { const args = expr.arguments ?? []; if (args.some(ts.isSpreadElement) || args.length > 1) { lowerer.noLowering( `new Date with ${args.length} arguments`, expr, "new Date(), new Date(milliseconds), and new Date(dateString) are supported; the local-time year/month field constructor has no lowering", symbol, ); } if (isJsSourceFile(expr.getSourceFile())) { return { kind: "libCall", fn: "date.nativeNew", args: [ { kind: "dynArrLit", elems: args.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc }, ], type: DYN, loc }; } if (args.length === 0) { return { kind: "libCall", fn: "date.newNow", args: [], type: DATE_T, loc }; } const arg = lowerer.lowerExpr(args[0]!); if (arg.type.kind === "f64") { return { kind: "libCall", fn: "date.newMs", args: [arg], type: DATE_T, loc }; } if (arg.type.kind === "string") { return { kind: "libCall", fn: "date.newString", args: [arg], type: DATE_T, loc }; } if (arg.type.kind === "date") { return arg; } lowerer.noLowering( `new Date of '${lowerer.fmt(arg.type)}' values`, args[0]!, "pass milliseconds, a date string, or another Date value", symbol, ); } // `new StringDecoder(encoding?)` (node:string_decoder): the decoder // is a two-field record — the CANONICAL encoding name (aliases fold // at compile time, exactly what `.encoding` answers in Node) and // the packed-f64 pending state starting at 0 (nothing buffered). // The encoding must be a literal (Node's alias set); omitted means // utf8, Node's default. if (symbol && symbol.name === "StringDecoder" && lowerer.isStdlibSymbol(symbol)) { const args = expr.arguments ?? []; if (args.length > 1) { lowerer.noLowering("new StringDecoder with 2 arguments", expr, undefined, symbol); } const encName = args.length === 1 ? bufEncoding(lowerer, "new StringDecoder", args[0]!) : "utf8"; const decT = lowerer.mapTypeOf(lowerer.typeOf(expr)); if (decT?.kind !== "record") lowerer.badType(expr, lowerer.typeOf(expr)); return { kind: "recordLit", fields: [ { name: "%enc", value: { kind: "strLit", value: encName, type: STRING, loc } }, { name: "%pending", value: { kind: "numLit", value: 0, type: F64, loc } }, ], type: decT, loc, }; } if (symbol && (symbol.name === "TextDecoder" || symbol.name === "TextEncoder") && lowerer.isStdlibSymbol(symbol)) { return lowerTextCodecNew(lowerer, expr, symbol.name); } // `new Uint8Array(...)` / `new Uint32Array(...)` / `new // Float32Array(...)` / `new Float64Array(...)`: the typed-array // constructors with a runtime representation (stdlib provenance — see lowerBytesNew for the // lowered argument shapes; a user's own class with one of the names // resolves through classBySymbol below). const bytesNew = lowerer.lowerBytesNew(expr, symbol); if (bytesNew) return bytesNew; const info = (symbol ? lowerer.classBySymbol.get(symbol) : undefined) ?? // `const C = require('./x'); new C()` over `module.exports = // class {…}`: the binding aliases the expression's own symbol — // the declaration story, collected on demand. propertyAssignedClassInfoOf(lowerer, symbol) ?? undefined; // A rebindable decorated name constructs through its VALUE (the // classval-typed path below — newValue through the decoration // result's construct thunk), never the declaration directly. if (info && !info.localClass && info.classDecorators?.valueGlobalId === undefined) { return lowerProgramClassNew(lowerer, expr, info, loc); } // `new Map()`: the lib Map constructor. The SEEDED forms: an // entries ARRAY LITERAL of PAIR LITERALS at the construction site // (`new Map([[k, v], ...])`) — each pair's key/value lower as // ordinary K/V-typed expressions and the backend set()s them in // order, so the tuple array never exists as a value — and a // `[K, V][]`-typed tuple-array VALUE (lowerMapSeedNew: a // construct-and-set loop, pairs in array order, duplicates // overwrite). Matching Maps copy their entries; nullish seeds make // empty Maps. General iterable seeds retain a fence. Unsupported key/value // types get their half named specifically instead of the component // fence (SC2009, which names Map slots at value positions elsewhere). // Both Array() and new Array() share the elements/count lowering. // `new Object()` — the spec's OrdinaryObjectCreate, exactly what the // `{}` literal builds (fresh reference identity, no own properties) — // lowers as the empty record. The ARGUMENT form is Object(x): it // returns its argument for objects and BOXES primitives — the wrapper // story with no lowering — so it keeps the constructor fence. if ( symbol?.name === "Object" && lowerer.isStdlibSymbol(symbol) && (expr.arguments ?? []).length === 0 ) { return { kind: "recordLit", fields: [], type: { kind: "record", shapeId: lowerer.shapes.intern([]) }, loc, }; } if (symbol?.name === "Array" && lowerer.isStdlibSymbol(symbol)) { return lowerArrayConstructor(lowerer, expr, expr.arguments ?? []); } if (symbol?.name === "Map" && lowerer.isStdlibSymbol(symbol)) { const seedArg = (expr.arguments?.length ?? 0) === 1 ? expr.arguments![0]! : null; const isPairLit = (el: ts.Expression): el is ts.ArrayLiteralExpression => ts.isArrayLiteralExpression(el) && el.elements.length === 2 && !el.elements.some(ts.isSpreadElement); const entriesLit = seedArg && ts.isArrayLiteralExpression(seedArg) && seedArg.elements.every(isPairLit) ? seedArg.elements.filter(isPairLit) : null; let tsType = lowerer.typeOf(expr); let mapped = lowerer.mapTypeOf(tsType); if (mapped === null && isJsSourceFile(expr.getSourceFile())) mapped = lowerer.irTypeOf(expr); const fieldType = assignedThisFieldType(lowerer, expr); if (mapped?.kind !== "map" && fieldType?.kind === "map") mapped = fieldType; // A fresh map built from pair literals can use its destination's // layout directly. Inference may otherwise split record values // into incompatible shapes (for example a nullable field), even // though every seed fits the declared value type. Existing map or // tuple-array values retain their own representation. let parent = expr.parent; while (ts.isParenthesizedExpression(parent)) parent = parent.parent; // `satisfies` checks a view but retains the expression's own type. const contextualSeed = !ts.isSatisfiesExpression(parent) && (expr.typeArguments?.length ?? 0) === 0 && (entriesLit !== null || (expr.arguments?.length ?? 0) === 0); // JavaScript's `new Map()` has no type-argument syntax: the no-arg // constructor overload pins Map whatever the JSDoc says // (`@type` on the declaration types the VARIABLE, not this // expression). The CONTEXTUAL type carries the annotation — adopt // it when it is a supported map. TS type arguments keep winning: // their expression type already maps. if (mapped?.kind !== "map" || contextualSeed) { const ctx = lowerer.checker.getContextualType(expr); const ctxMapped = ctx ? lowerer.mapTypeOf(ctx) : null; if (ctx && ctxMapped?.kind === "map") { tsType = ctx; mapped = ctxMapped; } } if (seedArg && !entriesLit && mapped?.kind === "map") { const seeded = lowerMapSeedNew(lowerer, seedArg, mapped); if (seeded) return seeded; } if ((expr.arguments?.length ?? 0) > 0 && !entriesLit) { lowerer.noLowering( "new Map(entries)", expr, "supported seeds: an array literal of [key, value] pair literals, or a " + "[K, V][]-typed tuple-array value, a matching Map, or null/undefined", ); } if (mapped?.kind === "map") { if (!entriesLit) return { kind: "mapNew", type: mapped, loc }; const seed = entriesLit.map((pair) => ({ key: lowerer.lowerCollectionKey(pair.elements[0]!, mapped.key), value: mapped.value.kind === "dyn" ? lowerer.lowerCollectionKey(pair.elements[1]!, mapped.value) : lowerer.lowerExprExpecting(pair.elements[1]!, mapped.value), })); return { kind: "mapNew", seed, type: mapped, loc }; } const targs = lowerer.checker.getTypeArguments(tsType as ts.TypeReference); // JAVASCRIPT `new Map()` whose arguments never resolved past // Map (no annotation, no contextual type, no seed): the // The VALUE lowers as an opaque dyn object // (identity and truthiness are real), and every reached METHOD use // meets its own per-site fence at runtime. The formatter's // config-cache shape: module init constructs the caches // unconditionally; the format path never touches them. TypeScript // keeps the compile fence. if ( isJsSourceFile(expr.getSourceFile()) && (expr.arguments?.length ?? 0) === 0 && targs.length > 0 && targs.every((t) => (t.flags & ts.TypeFlags.Any) !== 0) ) { return { kind: "dynObjLit", type: DYN, loc }; } const keyIr = targs[0] ? lowerer.mapTypeOf(targs[0]) : null; if (targs[0] && (!keyIr || !isSupportedMapKey(keyIr, keyIr.kind === "union" ? lowerer.unions.get(keyIr.unionId)?.arms : undefined))) { lowerer.unsupported( "SC1090", expr, `Map keys of type '${lowerer.checker.typeToString(targs[0])}' ` + `(Map keys must be numbers, strings, identity references, or unions of identity references)`, ); } if (targs[1]) { lowerer.unsupported( "SC1090", expr, `Map values of type '${lowerer.checker.typeToString(targs[1])}' ` + `(Map values must be number, string, boolean, records, class instances, ` + `arrays, promises, Maps, Sets, or unions of those — not functions, 'unknown', or 'any')`, ); } lowerer.badType(expr, tsType); } // Weak containers share checked native key identities and lifetime hooks. if ((symbol?.name === "WeakMap" || symbol?.name === "WeakSet") && lowerer.isStdlibSymbol(symbol)) { const args = expr.arguments ?? []; if (args.length > 1 || args.some(ts.isSpreadElement)) { lowerer.unsupported("SC1090", expr, `${symbol.name} construction with surplus or spread arguments`); } if (lowerer.dynamic) { const ctor: IrExpr = { kind: "jsOp", op: "globalGet", name: symbol.name, args: [], type: JSVAL, loc }; return { kind: "jsOp", op: "construct", args: [ctor, ...args.map((arg) => lowerer.jsvalIn(lowerer.lowerExpr(arg), arg))], type: JSVAL, loc }; } return { kind: "libCall", fn: symbol.name === "WeakMap" ? "weakMap.new" : "weakSet.new", args: [args[0] ? lowerer.lowerExprExpecting(args[0], DYN) : dynUndefinedExpr(loc)], type: DYN, loc, }; } // `new Set()`: construct + bulk add from native arrays, tuples, // or strings. Duplicates collapse in insertion order. if (symbol?.name === "Set" && lowerer.isStdlibSymbol(symbol)) { const tsType = lowerer.typeOf(expr); let mapped = lowerer.mapTypeOf(tsType); const fieldType = assignedThisFieldType(lowerer, expr); if (mapped?.kind !== "set" && fieldType?.kind === "set") mapped = fieldType; if (mapped?.kind === "set" && (expr.arguments?.length ?? 0) === 1) { const seeded = lowerSetSeedNew(lowerer, expr.arguments![0]!, mapped); if (seeded) return seeded; } // JavaScript's identity-Set idiom: `new Set([setTimeout, atob, // ...])` — the element TYPE (a union of stdlib signatures) has no // mapping, but the element VALUES all lower to identity tokens // (interned strings — see the JS token stance in lower-exprs), so // the honest construction is a Set of those scalars. if ( !mapped && isJsSourceFile(expr.getSourceFile()) && (expr.arguments?.length ?? 0) === 1 && ts.isArrayLiteralExpression(expr.arguments![0]!) && !(expr.arguments![0] as ts.ArrayLiteralExpression).elements.some(ts.isSpreadElement) ) { const lit = expr.arguments![0] as ts.ArrayLiteralExpression; const elems = lit.elements.map((el) => lowerer.lowerExpr(el)); const first = elems[0]; if ( first !== undefined && (first.type.kind === "string" || first.type.kind === "f64") && elems.every((e) => e.type.kind === first.type.kind) ) { const setT: IrType = { kind: "set", elem: first.type }; const seed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(first.type), loc }; return { kind: "setNew", seed, type: setT, loc }; } } if ((expr.arguments?.length ?? 0) > 0) { lowerer.noLowering( "new Set(values)", expr, "construct the Set empty and add() each value — only nullish values or a matching Set, string, array, or fixed tuple of " + "already-legal elements (numbers, strings, or identity references) seeds a Set", ); } if (mapped?.kind === "set") return { kind: "setNew", type: mapped, loc }; const targs = lowerer.checker.getTypeArguments(tsType as ts.TypeReference); if (targs[0]) { lowerer.unsupported( "SC1090", expr, `Set elements of type '${lowerer.checker.typeToString(targs[0])}' ` + `(Set elements must be numbers, strings, identity references, or unions of identity references)`, ); } lowerer.badType(expr, tsType); } // `new AsyncLocalStorage()` (node:async_hooks): a fresh store id — // an f64 handle (type-mapper.ts), the Channel story. Construction options // ({ defaultValue, name }) have no lowering yet. if (symbol?.name === "AsyncLocalStorage" && lowerer.isStdlibSymbol(symbol)) { if ((expr.arguments?.length ?? 0) > 0) { lowerer.noLowering( "new AsyncLocalStorage(options)", expr, "the zero-argument constructor is the supported form (defaultValue/name options have no lowering yet)", ); } return { kind: "libCall", fn: "als.new", args: [], type: F64, loc }; } // `new Promise((resolve) => ...)`: the ambient Promise constructor. if (symbol?.name === "Promise" && lowerer.isStdlibSymbol(symbol)) { const type = lowerer.irTypeOf(expr); if (type.kind !== "promise") lowerer.badType(expr, lowerer.typeOf(expr)); const args = expr.arguments ?? []; if (args.length !== 1) { lowerer.unsupported("SC1090", expr, "Promise construction without an executor"); } // `new Promise(setImmediate)` (the Node-suite early-exit shape): // the executor IS the stdlib setImmediate, so resolve rides the // immediate queue — a dedicated runtime constructor arms an // immediate that fulfills with the undefined dyn value. { const a0 = args[0]!; if (ts.isIdentifier(a0) && a0.text === "setImmediate") { const sym = lowerer.checker.getSymbolAtLocation(a0); const decls = sym ? lowerer.checker.declarationsOf(sym) : []; if (decls.length > 0 && decls.every((d) => lowerer.isStdlibFile(d.getSourceFile()))) { // The settled value is the undefined dyn value — the result // is promise whatever T the checker inferred for the // unusual executor (Promise in the suite's shape). return { kind: "libCall", fn: "timers.immediatePromise", args: [], type: { kind: "promise", inner: DYN }, loc, }; } } } // Executors bind resolve alone or (resolve, reject): reject is a // real closure rejecting the promise with an Error reason (the // ambient override pins `reason: Error` — rejection payloads share // the thrown-value representation, and the OBJ kind keeps // catch-side instanceof and the uncaught printer working). First // settle wins, exactly JS: reject-after-resolve and double-reject // are no-ops, and an executor throw after any settle is swallowed. const executor = lowerer.lowerExpr(args[0]!); if (executor.type.kind !== "func") lowerer.badType(args[0]!, lowerer.typeOf(args[0]!)); if (executor.type.params.length > 1) { const rj = executor.type.params[1]!; if ( executor.type.params.length > 2 || rj.kind !== "func" || rj.ret.kind !== "void" || rj.params.length !== 1 || rj.params[0]!.kind !== "object" || rj.params[0]!.className !== "%Error" ) { // A non-contextually-typed executor VALUE whose second param // isn't the pinned (reason: Error) => void shape. lowerer.unsupported( "SC1090", args[0]!, "Promise executors whose reject parameter is not '(reason: Error) => void'", ); } } return { kind: "newPromise", executor, type, loc }; } // The lib fence's CONSTRUCTOR chokepoint: `new` of any other // stdlib-declared constructor (Date, WeakMap, Proxy, // ArrayBuffer, RegExp, ... — and @types/node's URL, AbortController, // TextEncoder, ...) typechecks and reports SC2020 here. The named // families carry pointed hints: each states WHY no honest static // lowering exists (or what to use instead). if (lowerer.isStdlibSymbol(symbol ?? undefined)) { // The deprecated `new Buffer(string, encoding?)` ctor's string arm // with a NON-STRING first argument and a string second: Node // throws ERR_INVALID_ARG_TYPE synchronously (and DEP0005 never // fires on this throwing path, so the compiled silence matches). // Every other new Buffer form keeps the fence below — the // constructing forms would owe the deprecation warning. if ( expr.expression.text === "Buffer" && expr.arguments?.length === 2 && !expr.arguments.some(ts.isSpreadElement) && lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[0]!))?.kind !== "string" && lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[1]!))?.kind === "string" ) { const first = lowerer.lowerExpr(expr.arguments[0]!); if (first.kind === "unitLit" || first.type.kind === "dyn" || lowerer.dynConvertible(first.type)) { const got: IrExpr = first.type.kind === "dyn" ? first : { kind: "dynFrom", value: first, type: DYN, loc }; // The encoding argument still evaluates in Node before the // throw only via the ctor body's later reads — it does NOT // observe it before throwing, so dropping it is exact for // effect-free operands; effectful ones keep the fence. const enc = lowerer.lowerExpr(expr.arguments[1]!); if (enc.kind === "strLit" || isSafeToRepeat(enc)) { return { kind: "libCall", fn: "buffer.newStringFail", args: [got], type: bytesOf("u8"), loc }; } } } const ctorHints: Record = { RegExp: "use a regex literal (/pattern/flags) — constructed regexes have no lowering", String: "boxed wrapper objects have no lowering — use the string primitive (the box is only distinguishable via typeof/identity, which nothing here can honor)", Number: "boxed wrapper objects have no lowering — use the number primitive", Boolean: "boxed wrapper objects have no lowering — use the boolean primitive", WeakRef: "deref()-after-collect exposes GC timing — genuinely dynamic; hold a strong reference instead", FinalizationRegistry: "finalization callbacks expose GC timing — genuinely dynamic; release resources explicitly instead", SharedArrayBuffer: "no shared-memory threads exist in a compiled program — Uint8Array is the byte storage", ArrayBuffer: "native ArrayBuffer construction supports fixed-length storage; resizable buffers remain unsupported", Proxy: "native Proxy construction requires checked-native plain targets and handlers", Function: "runtime code generation cannot be compiled ahead of time (the eval stance) — write the function", }; lowerer.noLowering( `new ${expr.expression.text}`, expr, ctorHints[expr.expression.text], symbol, ); } } // `new crypto.X509Certificate(data)` — the Dirent-style data record: // the certificate's lowered members (fingerprint — the SHA-1 of the // DER, uppercase colon-separated — plus the validFrom/validTo // validity window in Node's ASN1_TIME_print shape) compute AT // CONSTRUCTION, when Node parses too, so unparseable input throws // Node's exact PEM error here (ERR_OSSL_PEM_NO_START_LINE) and the // handle never exists. Both import forms (`crypto.X509Certificate` // through the namespace, named `X509Certificate`); Buffer input only // — the readFileSync idiom. mapType interns the matching record, so // locals and the composed member reads all flow. The data argument // feeds THREE field computations, so construction goes through an // interned helper whose parameter evaluates it exactly once. { const callee = expr.expression; const isX509 = (ts.isPropertyAccessExpression(callee) && callee.name.text === "X509Certificate" && lowerer.builtinNamespaceModuleOf(callee.expression) === "crypto") || (ts.isIdentifier(callee) && (() => { const bi = lowerer.builtinImportOf(callee); return bi?.module === "crypto" && bi.member === "X509Certificate"; })()); if (isX509) { const args: readonly ts.Expression[] = expr.arguments ?? []; if (args.length !== 1) { lowerer.noLowering( "X509Certificate with this argument shape", expr, "the supported form is new X509Certificate(readFileSync(path))", ); } const data = lowerer.lowerExpr(args[0]!); const isBytes = data.type.kind === "bytes" && data.type.elem === "u8"; if (!isBytes && data.type.kind !== "string") { lowerer.noLowering( `X509Certificate over '${lowerer.fmt(data.type)}' data`, args[0]!, "pass the certificate Buffer or PEM string (an fs.readFileSync result)", ); } const t = lowerer.mapTypeOf(lowerer.typeOf(expr)); if (t?.kind !== "record") lowerer.badType(expr, lowerer.typeOf(expr)); const key = `x509.record:${isBytes ? "bytes" : "str"}`; let helper = lowerer.widthHelpers.get(key); if (!helper) { helper = `%x509.record.${lowerer.widthHelpers.size}`; lowerer.widthHelpers.set(key, helper); const dataT = data.type; const dRef: IrExpr = { kind: "varRef", localId: "d.0", type: dataT, loc }; const field = ( name: string, fn: "crypto.x509Fingerprint" | "crypto.x509FingerprintStr" | "crypto.x509ValidFrom" | "crypto.x509ValidFromStr" | "crypto.x509ValidTo" | "crypto.x509ValidToStr", ): { name: string; value: IrExpr } => ({ name, value: { kind: "libCall", fn, args: [dRef], type: STRING, loc }, }); lowerer.liftedFns.push({ name: helper, params: [{ localId: "d.0", name: "d", type: dataT }], returnType: t, locals: [{ id: "d.0", name: "d", type: dataT, mutable: false }], body: [ { kind: "return", value: { kind: "recordLit", fields: [ field("fingerprint", isBytes ? "crypto.x509Fingerprint" : "crypto.x509FingerprintStr"), field("validFrom", isBytes ? "crypto.x509ValidFrom" : "crypto.x509ValidFromStr"), field("validTo", isBytes ? "crypto.x509ValidTo" : "crypto.x509ValidToStr"), ], type: t, loc, }, loc, }, ], loc, }); } return { kind: "call", callee: helper, args: [data], type: t, loc }; } } // `new X(...)` through a class VALUE (a classval-typed binding, array // element, map read, param): the newValue dispatch through the class // object's construct thunk. Arguments complete against the STATIC // class's one constructor signature — exact for every value legally // in the slot (the classval widening rule pins the ABI). tsc typed // the site against the slot's construct signature; a UNION-typed // callee (unannotated heterogeneous registries) keeps a pointed // fence — annotate the slot with the common constructor type. { const calleeT = storedClassValueType(lowerer, expr.expression); if (calleeT?.kind === "classval") { let info = lowerer.classes.get(calleeT.className); if (!info && ts.isPropertyAccessExpression(expr.expression) && ts.isIdentifier(expr.expression.name)) { // A property-assigned class expression not collected yet (this // body lowers ahead of the assignment statement — hoisted // functions): collect it on demand and retry, keeping the // dynamic newValue dispatch below (the runtime field value // decides, exactly Node under reassignment-through-aliases). propertyAssignedClassInfoOf(lowerer, lowerer.checker.getSymbolAtLocation(expr.expression.name)); info = lowerer.classes.get(calleeT.className); } if (!info) { // The TYPE world names a class the lowering never registered — // a fenced class expression, an abstract/deferred declaration: // flush its own diagnostics (they tell the real story) and // poison this construction site, never an ICE. lowerer.flushDeferredClass(calleeT.className); lowerer.unsupported( "SC1090", expr, "constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic)", ); } // A classval of a generic FAMILY (`new () => Box` slots): no // single constructor ABI exists to complete against. No producer // can fill such a slot (family values and widenings both fence), // so the construction site is the honest place to name it. if (info.generic) { lowerer.unsupported( "SC1090", expr, "constructing through a class value of an uninstantiated generic type (annotate the slot with a concrete instantiation — e.g. 'new (v: number) => Box')", ); } const callee = lowerer.lowerExpr(expr.expression); if (callee.type.kind !== "classval") lowerer.badType(expr.expression, lowerer.typeOf(expr.expression)); lowerer.noteEdge(`%${info.def.name}.constructor`); // Every constructor a value in this slot can dispatch to is a // descendant's — mark them reachable like a virtual edge. const below = (c: ClassInfo): void => { for (const s of c.subclasses) { lowerer.noteEdge(`%${s.def.name}.constructor`); below(s); } }; below(info); const args = lowerer.completeArgs(expr.arguments ?? [], info.ctorParams, loc, expr); return { kind: "newValue", callee, args, type: { kind: "object", className: info.def.name }, loc, }; } if (calleeT?.kind === "union") { const def = lowerer.unions.get(calleeT.unionId); if (def?.arms.some((a) => a.kind === "classval")) { lowerer.unsupported( "SC1090", expr, "constructing through a union of class values (annotate the slot with the common constructor type — e.g. `new () => Base` — or narrow first)", ); } } } // Declared-but-unlowered stdlib classes (fallback surface: the // http/https Agent): the fence points at the lowered shapes instead // of the generic construction rejection. { const STDLIB_CTOR_HINTS: Record = { Agent: "constructing an http Agent through an indirect class binding (spell the construction on the module binding — new http.Agent(...)/new https.Agent(...) or the named Agent import — which lowers to the Agent handle)", }; const ctorName = ts.isIdentifier(expr.expression) ? expr.expression : ts.isPropertyAccessExpression(expr.expression) && ts.isIdentifier(expr.expression.name) ? expr.expression.name : null; if (ctorName !== null) { const raw = lowerer.checker.getSymbolAtLocation(ctorName); const sym = raw && raw.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(raw) : raw; const hint = sym && lowerer.isStdlibSymbol(sym) ? own(STDLIB_CTOR_HINTS, sym.name) : undefined; if (hint !== undefined) { lowerer.unsupported("SC1090", expr, hint); } } } // Checked construction supports emitted classes and builtin typed arrays. const callee = tryLowerExpression(lowerer, expr.expression); if (callee?.type.kind === "dyn" && !(expr.arguments ?? []).some(ts.isSpreadElement)) { return checkedClassConstruction(lowerer, callee, (expr.arguments ?? []).map((arg) => lowerer.lowerExprExpecting(arg, DYN)), loc); } lowerer.unsupported("SC1090", expr, "constructing values other than classes declared in the program"); } /** A getter/setter invocation over an accessor target's receiver — the * same whole-program devirtualization as method calls: a virtualCall * when some strict subclass of the receiver's static class overrides * this HALF of the accessor (get and set devirtualize independently), * a direct call of the nearest declaration otherwise. */ export function accessorCall(lowerer: Lowerer, className: string, member: string, obj: IrExpr, extraArgs: IrExpr[], ret: IrType, loc: SrcLoc,): IrExpr { const info = lowerer.classes.get(className); if (!info) throw new InternalCompilerError(`lowerer bug: accessor call on unknown class ${className}`); const found = lowerer.findMethodOn(info, member); if (!found) throw new InternalCompilerError(`lowerer bug: no ${member} on ${className}`); // The abstract direct-call fence, accessor form (see // lowerObjectMethodCall): an abstract accessor with no concrete // override below has no implementation for a direct call to target. if (found.sig.abstract === true && !lowerer.overrideBelow(info, member)) { lowerer.pushDiag( unsupportedDiag( "SC1090", loc, `${member.startsWith("get:") ? "reads" : "writes"} of the abstract accessor '${member.slice(4)}' with no concrete implementation below the receiver's static class`, ), ); throw new PoisonError(); } if (lowerer.overrideBelow(info, member)) lowerer.noteVirtualEdge(info, member); else lowerer.noteEdge(`%${found.declarer.def.name}.${member}`); if (lowerer.overrideBelow(info, member)) { return { kind: "virtualCall", className: info.def.name, method: member, args: [lowerer.upcastTo(obj, info.def.name), ...extraArgs], type: ret, loc, }; } return { kind: "call", callee: `%${found.declarer.def.name}.${member}`, args: [lowerer.upcastTo(obj, found.declarer.def.name), ...extraArgs], type: ret, loc, }; } /** The generic function-like INITIALIZER behind a class FIELD — * `time = async (...) => {...}` or `= function g(...) {...}` * (parens stripped): bindingGenericFnNodeOf's shape rule, member form. * Null when the field isn't that shape. */ function genericFieldFnNodeOf(member: ts.PropertyDeclaration): ts.FunctionExpression | ts.ArrowFunction | null { if (member.initializer === undefined) return null; let init: ts.Expression = member.initializer; while (ts.isParenthesizedExpression(init)) init = init.expression; if ( (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) && init.typeParameters !== undefined && init.body !== undefined ) { return init; } return null; }