mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
* Expand static Effect support across common APIs * Preserve compiler parity across expanded Effect patterns * Fix compiler regressions exposed by Effect coverage * Bound shared type conversion traversal * Clarify conversion contracts and tidy fixtures * Run every Effect fixture in CI shards * Preserve inferred array assignments in concurrent Effect programs * Record inferred array fixture compiler ordering
6616 lines
346 KiB
TypeScript
6616 lines
346 KiB
TypeScript
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<IrFunction["generator"]>;
|
|
}
|
|
|
|
export interface ClassInfo {
|
|
def: IrClassDef;
|
|
runtimeStatics?: readonly ts.ClassElement[];
|
|
runtimePrototypeMembers?: readonly ts.ClassElement[];
|
|
localPrototypeData?: true;
|
|
prototypeMethodValues?: Map<string, IrExpr>;
|
|
prototypeReflectionReady?: true;
|
|
prototypeReflectionRefusal?: string;
|
|
localClass?: {
|
|
context: FnCtx | null;
|
|
bodies: Map<string, IrFunction>;
|
|
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<string, IrType>;
|
|
/** 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<string, ClassMethodSignature>;
|
|
/** Own JS overrides whose signature cannot use the inherited ABI. The slot
|
|
* retains that ABI, but its implementation throws before executing. */
|
|
methodEntryFences?: Map<string, ScrDiagnostic>;
|
|
/** OWN GENERIC instance methods (own type parameters — `m<T>(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<string, GenericFnInfo>;
|
|
/** 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<string, GenericFnInfo>;
|
|
/** 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<string>;
|
|
/** 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:<event>`
|
|
* (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.<C>.<name>`), 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<string, { params: ParamShape[]; ret: IrType; member: ts.MethodDeclaration; receiverInfo?: GenericFnInfo }>;
|
|
/** `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<ts.Symbol, string>;
|
|
/** Stable symbol keys for computed instance methods. */
|
|
symbolMethods?: Map<ts.Symbol, string>;
|
|
/** GENERIC class FAMILY (`class Box<T>` 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<number>`): 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<ts.Symbol, IrType>; typeArgsText: string; ordinal: number };
|
|
/** MIXIN instantiation (`%mx<start>.<name>` 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): T`)
|
|
* — Node erases it, so evaluating the decorator expression throws the
|
|
* ReferenceError; the program compiles to exactly that crash. */
|
|
shapes?: (
|
|
| { kind: "call"; funcType: Extract<IrType, { kind: "func" }>; 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<T> | 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<string, { name: string; info: ClassInfo | null; poisoned?: boolean }>;
|
|
}
|
|
|
|
/** 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<ts.Symbol, string>,
|
|
fields: ReadonlyMap<string, IrType>,
|
|
): 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: "<builtin>", 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<string, { params: ParamShape[]; ret: IrType; abstract?: true; async?: true; gen?: NonNullable<IrFunction["generator"]> }>([["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: "<builtin>", 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: "<builtin>", 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<string, IrType>();
|
|
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<string> = 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): 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<ts.Symbol, IrType>; 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<ts.Symbol, IrType>; 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<T> extends Box<T>`) 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<ts.Symbol>();
|
|
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<number>` — 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<string, IrType>(base ? base.fields : []);
|
|
const errorRooted = (() => {
|
|
for (let c = base; c; c = c.base) if (c.builtinError) return true;
|
|
return false;
|
|
})();
|
|
const symbolFields = new Map<ts.Symbol, string>();
|
|
if (base?.symbolFields) for (const [symbol, name] of base.symbolFields) symbolFields.set(symbol, name);
|
|
const symbolMethods = new Map<ts.Symbol, string>();
|
|
if (base?.symbolMethods) for (const [symbol, name] of base.symbolMethods) symbolMethods.set(symbol, name);
|
|
const fieldOrder: ClassInfo["fieldOrder"] = [];
|
|
const methods = new Map<string, { params: ParamShape[]; ret: IrType; abstract?: true; async?: true; gen?: NonNullable<IrFunction["generator"]> }>();
|
|
const methodEntryFences = new Map<string, ScrDiagnostic>();
|
|
// Own accessor declarations ("get:x"/"set:x" → node), for the
|
|
// partial-override analysis below (diagnostics need the node).
|
|
const accessorNodes = new Map<string, ts.AccessorDeclaration>();
|
|
/** 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<string, ts.MethodDeclaration>();
|
|
/** 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<ClassInfo["paramProps"]> = [];
|
|
|
|
const staticFields: ClassInfo["staticFields"] = [];
|
|
const staticMethods: NonNullable<ClassInfo["staticMethods"]> = 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<string, GenericFnInfo>();
|
|
const genericStatics = new Map<string, GenericFnInfo>();
|
|
// Accepts generic METHODS and instance FIELDS initialized with a
|
|
// generic arrow/function expression (`time = async <T>(...) => {...}`
|
|
// — 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 <T>(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<string>(base?.deferredInitFields ?? []);
|
|
if (unguardedFields.length > 0) {
|
|
const topAssigned = new Set<string>();
|
|
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<ts.Symbol, ts.Symbol>();
|
|
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<number>`) names — registered on first demand. The
|
|
* instance's NAME reserves its key before the shape collects, so
|
|
* self-referential layouts (`next: Box<T> | 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<any>` under a static build, wilder arguments
|
|
// no instantiation can carry (`X<<T>() => 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<any> = 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<T>` 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<ts.Symbol, IrType>();
|
|
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<T>(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<ClassDecorationInfo["shapes"]> = [];
|
|
for (const d of cd.nodes) {
|
|
// An ambient declaration NOTHING defines (`declare let dec: any`,
|
|
// `declare function dec<T>(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<IrType, { kind: "func" }>; 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<number>`, `new (v: number) => Box<number>` slots).
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
blame,
|
|
`generic classes as values ('typeof ${display}' keeps the type parameter — instantiation expressions ('${display}<number>') 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 `… = <this class expression>` 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<IrFunction["generator"]> } } | 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<ts.Type>();
|
|
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<T>(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<T>(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<IrFunction, "classCaptures"> {
|
|
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.<n>(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 `%<family>.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<string>("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<K, V>()`: 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<any, any> 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<any, any> (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<T>()`: 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<T>((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<dyn> whatever T the checker inferred for the
|
|
// unusual executor (Promise<unknown> 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<string, string | undefined> = {
|
|
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<any>` 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<number>')",
|
|
);
|
|
}
|
|
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<string, string | undefined> = {
|
|
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 <T>(...) => {...}` or `= function g<T>(...) {...}`
|
|
* (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;
|
|
}
|