mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-09 10:50:02 +08:00
11950 lines
506 KiB
TypeScript
11950 lines
506 KiB
TypeScript
import { lowerAsyncLocalStorageOptions } from "./builtins/async-context.js";
|
|
import { shapeOverlayAnnotated } from "../shape-inference/index.js";
|
|
import {
|
|
DATE_TIME_FIELD,
|
|
dateRootOf,
|
|
dateSuperCall,
|
|
isDatePrototypeMethod,
|
|
lowerDateSuperMethodCall,
|
|
} from "./builtins/date-classes.js";
|
|
import { predeclareForwardClass } from "./lower-stmts.js";
|
|
import {
|
|
arrayClassBase,
|
|
arrayClassFieldsFn,
|
|
arrayClassSpeciesFn,
|
|
arraySuperCall,
|
|
refuseArrayClassSpecies,
|
|
lowerArraySuperCall,
|
|
} from "./builtins/array-classes.js";
|
|
import { lowerDateStaticCall } from "./builtins/dates.js";
|
|
import { keyedHelperName } from "./stable-names.js";
|
|
import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js";
|
|
import { adaptPromiseExecutor } from "./promise-executor-adapter.js";
|
|
import { dynUndefinedExpr, nodeThrowExpr, strLit, varRef } from "../../ir/build.js";
|
|
import {
|
|
constructedFromValue,
|
|
constructedValue,
|
|
superIntoResultSlot,
|
|
} from "./constructor-returns.js";
|
|
import { checkedIterableSpread } from "./checked-iterable-spread.js";
|
|
import { lowerReadlineInterfaceNew, lowerReadlineUiNew } from "./builtins/readline.js";
|
|
import {
|
|
builtinMemberBindingTarget,
|
|
builtinMemberRequireDecl,
|
|
} from "./builtins/module-bindings.js";
|
|
import { checkedClassUnionAssertion } from "./class-assertions.js";
|
|
import { collectClassMethodMutations } from "./class-method-mutations.js";
|
|
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 { fsStreamClassOf, lowerFsStreamNew } from "./builtins/fs-streams.js";
|
|
import { codeGenerationCalleeOf, lowerCodeGenerationCall } from "./code-generation.js";
|
|
import { TEMPORAL_GLOBAL } from "../builtin-modules.js";
|
|
import * as ts from "../ts7/adapter.js";
|
|
import { lowerZlibStreamNew, zlibClassName, zlibStreamModeOf } from "./builtins/compression.js";
|
|
import { everyStmtList } from "../../ir/traverse.js";
|
|
import {
|
|
type BuiltinClassRef,
|
|
builtinClassAliasOf,
|
|
builtinClassRefOf,
|
|
} from "./builtins/class-values.js";
|
|
import type { FnCtx, Lowerer } from "./lowerer.js";
|
|
import { inlineNamespaceRequireOf, lowerWithInlineRequireInit } from "./cjs-inline-require.js";
|
|
import {
|
|
BOOL,
|
|
BYTES_U8,
|
|
canDynCheckTo,
|
|
DATE_T,
|
|
DYN,
|
|
F64,
|
|
type IrClassDef,
|
|
type IrExpr,
|
|
type IrFunction,
|
|
type IrLibFn,
|
|
type IrLocal,
|
|
type IrParam,
|
|
type IrStmt,
|
|
type IrType,
|
|
RUNTIME_EMITTER_CLASS,
|
|
RUNTIME_ERROR_CLASSES,
|
|
RUNTIME_STREAM_CLASSES,
|
|
STRING,
|
|
type SrcLoc,
|
|
classValueConstructor,
|
|
UNDEFINED_T,
|
|
VOID,
|
|
arrayOf,
|
|
isSupportedMapKey,
|
|
isSupportedMapValue,
|
|
isSupportedSetElem,
|
|
isUnitType,
|
|
typeEquals,
|
|
} from "../../ir/ir.js";
|
|
import {
|
|
genericCallInstance,
|
|
implicitAnyParamSymbolsOf,
|
|
implicitCallInstance,
|
|
implicitMonoFile,
|
|
genericLocalClassResult,
|
|
type GenericFnInfo,
|
|
} from "./generic-functions.js";
|
|
import {
|
|
extendInstantiationPath,
|
|
MAX_INSTANTIATION_RECURSION,
|
|
type InstantiationPath,
|
|
} from "./instantiation-path.js";
|
|
import { appendImplicitUndefinedReturn, hasExplicitJsDocReturn } from "./function-returns.js";
|
|
import { bodyReadsArguments } from "../arguments-usage.js";
|
|
import { errorNameAfterField, errorViewFields } from "./error-name-order.js";
|
|
import {
|
|
checkedJsDocumentedParameter,
|
|
documentedParameter,
|
|
generatorMeta,
|
|
type ParamShape,
|
|
} from "./call-signatures.js";
|
|
import { omittedArgFor } from "./call-arguments.js";
|
|
import {
|
|
checkerTypeDescription,
|
|
isGenericCallableMemberType,
|
|
jsOpenObjectType,
|
|
typeKey,
|
|
withUnitArm,
|
|
X509_DER_FIELD,
|
|
X509_PUBLIC_KEY_FIELD,
|
|
} from "../type-mapper.js";
|
|
import {
|
|
canonicalBuiltinModule,
|
|
cjsClassExprWholeExportOf,
|
|
isCjsJsFile,
|
|
isJsSourceFile,
|
|
isModuleExportsAccess,
|
|
locOf,
|
|
requireSpecOf,
|
|
} from "../program.js";
|
|
import { PoisonError, dynFallbackType, jsFuncNameOf, newFnCtx, own } from "./lowerer.js";
|
|
import { declarationGroupKey } from "./type-failure.js";
|
|
import { isAssertClassNew } from "./assert-instances.js";
|
|
import { isCjsRequireGlobal } from "./lower-node-module.js";
|
|
import { lowerArrayConstructor } from "./containers/array-construction.js";
|
|
import { initializedCollectionType } from "./js-collection-types.js";
|
|
import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js";
|
|
import { lowerObjectConstructor } from "./containers/indexed-objects.js";
|
|
import { knownBufEncoding, lowerBufferConstruct } from "./containers/bytes.js";
|
|
import { lowerSearchParamsNew } from "./builtins/url.js";
|
|
import { lowerTextCodecNew } from "./builtins/text-codecs.js";
|
|
import {
|
|
lowerBroadcastChannelNew,
|
|
lowerMessageChannelNew,
|
|
lowerWorkerNew,
|
|
} from "./builtins/workers.js";
|
|
import {
|
|
noLoweringDiag,
|
|
unsupportedPackageValueDiag,
|
|
unsupportedDiag,
|
|
type ScrDiagnostic,
|
|
} from "../../diagnostics/diagnostic.js";
|
|
import {
|
|
STREAM_API_MEMBERS,
|
|
STREAM_PROP_MEMBERS,
|
|
STREAM_COMPLETION_CALLBACK_INDEX,
|
|
UNDERSCORE_METHODS,
|
|
streamExtraCallbacks,
|
|
lowerStreamNew,
|
|
lowerStreamSuperCall,
|
|
streamCtorShape,
|
|
} from "./lower-stream.js";
|
|
import {
|
|
builtinEventTargetBaseOf,
|
|
eventTargetApiMembers,
|
|
eventTargetApiMethod,
|
|
eventTargetRootOf,
|
|
eventTargetSuperCall,
|
|
lowerEventTargetMethodCall,
|
|
} from "./lower-event-target.js";
|
|
import {
|
|
emitOverrideShapeReason,
|
|
emitSpecSuperForward,
|
|
emitterRooted,
|
|
lowerEmitterSuperCall,
|
|
type EmitOverrideRec,
|
|
} from "./lower-event-emitter.js";
|
|
import { declSymbolOf } from "./lower-modules.js";
|
|
import {
|
|
INSPECT_CUSTOM_MEMBER,
|
|
classSymbolKeyOf,
|
|
classSymbolKeyOfSymbol,
|
|
symbolFieldDisplayName,
|
|
} from "./symbol-fields.js";
|
|
import { lowerHttpAgentNew, lowerHttpServerNew, lowerTlsSocketNew } from "./lower-server.js";
|
|
import {
|
|
ambientNsRootOf,
|
|
ambientUndefReadType,
|
|
ambientUndefVarRootOf,
|
|
ambientUndefinedFnSymbolOf,
|
|
contextualUndefReadType,
|
|
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 {
|
|
accessorInterceptsBase,
|
|
accessorNames,
|
|
construction,
|
|
constructionPrefix,
|
|
} from "./js-class-fields.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, reflectClassPrototype } from "./class-prototypes.js";
|
|
import { constructionEscape } from "./construction-escapes.js";
|
|
import { checkedClassConstruction, checkedClassConstructionPacked } from "./class-construction.js";
|
|
import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js";
|
|
import {
|
|
reflectedClassMethodValue,
|
|
reflectedClassStaticMethodValue,
|
|
} from "./class-method-values.js";
|
|
import { identifierReadSymbol } from "./surfaces.js";
|
|
|
|
export function returnsOnlyThis(member: ts.MethodDeclaration): boolean {
|
|
const body = member.body;
|
|
const last = body?.statements?.nodes.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;
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
ts.forEachChild(body, visit);
|
|
return valid;
|
|
}
|
|
|
|
/** returnsOnlyThis, also accepting a comma expression whose last operand
|
|
* is `this` (the minifier's `return a(), this.x = v, this`, jose's fluent
|
|
* claim setters). Used only to narrow a call's result back to the
|
|
* receiver's class. */
|
|
export function returnsOnlyThisValue(member: ts.MethodDeclaration): boolean {
|
|
const body = member.body;
|
|
const last = body?.statements?.nodes.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;
|
|
for (;;) {
|
|
while (value && ts.isParenthesizedExpression(value)) value = value.expression;
|
|
if (
|
|
value &&
|
|
ts.isBinaryExpression(value) &&
|
|
value.operatorToken.kind === ts.SyntaxKind.CommaToken
|
|
) {
|
|
value = value.right;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
if (!value || value.kind !== ts.SyntaxKind.ThisKeyword) valid = false;
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
ts.forEachChild(body, visit);
|
|
return valid;
|
|
}
|
|
|
|
export 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,
|
|
visiting = new Set<ts.Node>(),
|
|
): IrType | null {
|
|
if (visiting.has(expression)) return null;
|
|
visiting.add(expression);
|
|
if (ts.isParenthesizedExpression(expression))
|
|
return storedClassValueType(lowerer, expression.expression, visiting);
|
|
if (
|
|
ts.isClassExpression(expression) &&
|
|
!functionLocalClass(expression) &&
|
|
!lowerer.collectingExprClasses.has(expression)
|
|
) {
|
|
const info = lowerer.lowerClassExpressionInfo(expression);
|
|
return { kind: "classval", className: info.def.name };
|
|
}
|
|
if (ts.isPropertyAccessExpression(expression) && !expression.questionDotToken) {
|
|
const member = nsMemberIdentOf(lowerer, expression);
|
|
if (member) return storedClassValueType(lowerer, member, visiting);
|
|
}
|
|
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" && lowerer.classes.has(stored.type.className))
|
|
return stored.type;
|
|
const symbol = lowerer.resolveValueSymbol(expression);
|
|
const declaration =
|
|
symbol &&
|
|
symbol.declarations.find(
|
|
(decl) =>
|
|
ts.isVariableDeclaration(decl) &&
|
|
decl.initializer &&
|
|
(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0,
|
|
);
|
|
if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
const value = storedClassValueType(lowerer, declaration.initializer, visiting);
|
|
if (value?.kind === "classval") return value;
|
|
}
|
|
}
|
|
// A generic factory's own local class has one class per instance.
|
|
if (ts.isCallExpression(expression)) {
|
|
const local = genericLocalClassResult(lowerer, expression);
|
|
if (local?.kind === "classval") return local;
|
|
}
|
|
const type = lowerer.typeOf(expression);
|
|
const mapped = lowerer.mapTypeOf(type);
|
|
if (mapped?.kind === "classval" && !lowerer.classes.has(mapped.className)) {
|
|
for (const signature of lowerer.checker.getConstructSignatures(type)) {
|
|
const constructor = signature.declaration;
|
|
const declaration =
|
|
constructor && ts.isConstructorDeclaration(constructor) ? constructor.parent : undefined;
|
|
if (
|
|
declaration &&
|
|
ts.isClassExpression(declaration) &&
|
|
!functionLocalClass(declaration) &&
|
|
!lowerer.collectingExprClasses.has(declaration)
|
|
) {
|
|
const info = lowerer.lowerClassExpressionInfo(declaration);
|
|
return { kind: "classval", className: info.def.name };
|
|
}
|
|
}
|
|
}
|
|
if (mapped?.kind === "classval" || !ts.isCallExpression(expression)) return mapped;
|
|
let callee = expression.expression;
|
|
let calls = 1;
|
|
const aliases = new Set<ts.Node>();
|
|
for (;;) {
|
|
if (aliases.has(callee)) break;
|
|
aliases.add(callee);
|
|
if (ts.isParenthesizedExpression(callee)) {
|
|
callee = callee.expression;
|
|
continue;
|
|
}
|
|
if (ts.isCallExpression(callee)) {
|
|
calls++;
|
|
callee = callee.expression;
|
|
continue;
|
|
}
|
|
if (ts.isPropertyAccessExpression(callee)) {
|
|
const member = nsMemberIdentOf(lowerer, callee);
|
|
if (member) {
|
|
callee = member;
|
|
continue;
|
|
}
|
|
}
|
|
if (ts.isIdentifier(callee)) {
|
|
const symbol = lowerer.resolveValueSymbol(callee);
|
|
const declaration =
|
|
symbol &&
|
|
symbol.declarations.find(
|
|
(decl) =>
|
|
ts.isVariableDeclaration(decl) &&
|
|
decl.initializer &&
|
|
(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0,
|
|
);
|
|
if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
callee = declaration.initializer;
|
|
continue;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
if (
|
|
(ts.isArrowFunction(callee) || ts.isFunctionExpression(callee)) &&
|
|
isJsSourceFile(ts.getSourceFileOfNode(callee)) &&
|
|
!callee.typeParameters?.nodes.length &&
|
|
!lowerer.preparingClassFactories.has(callee)
|
|
) {
|
|
const signature = lowerer.checker.getSignatureFromDeclaration(callee);
|
|
let result = signature && lowerer.checker.getReturnTypeOfSignature(signature);
|
|
for (let i = 1; result && i < calls; i++) {
|
|
const signatures = lowerer.checker.getCallSignatures(result);
|
|
result =
|
|
signatures.length === 1
|
|
? lowerer.checker.getReturnTypeOfSignature(signatures[0]!)
|
|
: undefined;
|
|
}
|
|
if (result && lowerer.checker.getConstructSignatures(result).length > 0) {
|
|
const returned = (value: IrExpr): IrType | null => {
|
|
let type = value.type;
|
|
for (let i = 0; i < calls && type.kind === "func"; i++) type = type.ret;
|
|
return type.kind === "classval" ? type : null;
|
|
};
|
|
const home = callee.parent;
|
|
const generic =
|
|
ts.isVariableDeclaration(home) && ts.isIdentifier(home.name)
|
|
? lowerer.genericFnOf(home.name)
|
|
: null;
|
|
if (generic?.implicitParams && ts.isVariableDeclaration(home) && ts.isIdentifier(home.name)) {
|
|
const factory = lowerer.lowerGenericFnValue(home.name, generic);
|
|
const type = returned(factory);
|
|
if (type) return type;
|
|
}
|
|
lowerer.preparingClassFactories.add(callee);
|
|
const collecting = lowerer.collecting,
|
|
sink = lowerer.diagSink;
|
|
lowerer.collecting = false;
|
|
lowerer.diagSink = null;
|
|
try {
|
|
const factory = lowerer.preparedClassFactories.get(callee) ?? lowerer.lowerLambda(callee);
|
|
lowerer.preparedClassFactories.set(callee, factory);
|
|
const type = returned(factory);
|
|
if (type) return type;
|
|
} finally {
|
|
lowerer.collecting = collecting;
|
|
lowerer.diagSink = sink;
|
|
lowerer.preparingClassFactories.delete(callee);
|
|
}
|
|
}
|
|
}
|
|
// 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 = signature.declaration;
|
|
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(ts.getSourceFileOfNode(factory)) ||
|
|
factory.typeParameters?.nodes.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;
|
|
expressionInitializationEmitted?: true;
|
|
runtimeStatics?: readonly ts.ClassElement[];
|
|
/** Direct calls of runtime static methods (directRuntimeStaticCall), by
|
|
* member name. */
|
|
runtimeStaticCalls?: Map<string, GenericFnInfo>;
|
|
runtimePrototypeMembers?: readonly ts.ClassElement[];
|
|
localPrototypeData?: true;
|
|
prototypeMethodValues?: Map<string, IrExpr>;
|
|
prototypeReflectionReady?: true;
|
|
/** The global that records at run time that the prototype view was
|
|
* populated by reflection, so it holds its own `constructor` until the
|
|
* program deletes it (prototypeReflectedFlag). */
|
|
prototypeReflectedFlag?: string;
|
|
prototypeObservedMethodsReady?: true;
|
|
/** The globals holding each observed method's original prototype value
|
|
* (set when the prototype data is built), by method name. A typed call
|
|
* whose slot still holds that value calls the method natively. */
|
|
prototypeOriginals?: Map<string, string>;
|
|
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;
|
|
errorCause?: 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 TypeScript overrides whose declared result differs from the
|
|
* inherited ABI's but converts into it (`Promise<void>` over `any`, a
|
|
* union narrowed to its inherited arm): the slot function `%C.m` keeps the
|
|
* inherited ABI and converts the result of the body, which lowers as
|
|
* `%C.m$own` with the declared result (async when declared so). */
|
|
returnAdapters?: Map<string, { params: ParamShape[]; ret: IrType; async: boolean }>;
|
|
/** 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>;
|
|
/** True when construction may hand the instance to other code (a method
|
|
* call, an argument, a callback, or a base constructor that does) — a
|
|
* subclass's own fields can then be read before their assignment. */
|
|
constructionEscapes?: true;
|
|
/** 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";
|
|
/** Runtime-provided EventTarget ("target") or Event ("event") base of
|
|
* compiled subclasses (lower-event-target.ts): no bodies lower, super()
|
|
* becomes an event.* libCall, and the inherited members lower through
|
|
* lower-event-target.ts over any class rooted here. Subclass structs
|
|
* embed the runtime prefix. */
|
|
builtinEventTarget?: "target" | "event";
|
|
/** A class declared `extends Date` (builtins/date-classes.ts): its first
|
|
* field is the hidden `%time` value, super() sets it with the Date
|
|
* constructor's rules, and the Date methods it does not declare lower
|
|
* over that value. Subclasses inherit the field. */
|
|
extendsDate?: true;
|
|
/** A class declared `extends Array` (builtins/array-classes.ts): its
|
|
* instances are arrays of `elem` whose own fields follow the array
|
|
* header, and its TypeScript type maps to `elem[]`. */
|
|
extendsArray?: { elem: IrType };
|
|
/** The synthetic LifetimeRegion class (@scriptc/experimental,
|
|
* lifetime-regions.ts): its constructor is synthesized with the class,
|
|
* and its methods lower at their call sites. */
|
|
lifetimeRegion?: true;
|
|
/** The synthetic InternTable class (@scriptc/experimental,
|
|
* intern-tables.ts): synthesized like lifetimeRegion. */
|
|
internTable?: true;
|
|
/** The synthetic ThreadSlots class (@scriptc/experimental,
|
|
* thread-slots.ts): synthesized like lifetimeRegion. */
|
|
threadSlots?: true;
|
|
/** 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[];
|
|
/** A runtime-provided class whose values construct (`const kinds =
|
|
* [Readable, Writable]`, `(C: typeof EventEmitter) => new C()`): the
|
|
* parameters of its `%C.construct` function (classValueConstructor).
|
|
* Absent for runtime classes that are not values. */
|
|
valueCtorParams?: ParamShape[];
|
|
base: ClassInfo | null;
|
|
/** Family heritage resolves its type arguments separately for each instance. */
|
|
genericBase?: ts.ExpressionWithTypeArguments;
|
|
/** 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 };
|
|
factoryBaseExpression?: ts.Expression;
|
|
/** 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;
|
|
/** Indexed sources can be absent despite the declared static type. */
|
|
runtimeOptional?: true;
|
|
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;
|
|
}
|
|
>;
|
|
staticMethodValues?: Map<string, IrExpr>;
|
|
/** `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?: {
|
|
path: InstantiationPath;
|
|
family: ClassInfo;
|
|
bindings: Map<ts.Symbol, IrType>;
|
|
/** The type arguments as checker types, for the resolutions that need
|
|
* literal identity (a conditional type over a literal argument). */
|
|
tsBindings: Map<ts.Symbol, ts.Type>;
|
|
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 type parameters a conditional type in the class depends on
|
|
* (literalSensitiveTypeParams), computed on first instantiation. */
|
|
literalSensitive?: Set<ts.Symbol>;
|
|
}
|
|
|
|
/** 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 p.declarations) {
|
|
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(ts.getSourceFileOfNode(fnLike))) return null;
|
|
if (fnLike.asteriskToken !== undefined) return null;
|
|
if (fnLike.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null;
|
|
const stmts = fnLike.body.statements.nodes;
|
|
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);
|
|
ts.forEachChild(n, visit);
|
|
};
|
|
ts.forEachChild(fnLike.body, 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 error classes whose values construct through error.newOptions. */
|
|
const ERROR_CLASS_VALUES: ReadonlySet<string> = new Set([
|
|
"%Error",
|
|
"%TypeError",
|
|
"%RangeError",
|
|
"%SyntaxError",
|
|
"%ReferenceError",
|
|
"%EvalError",
|
|
"%URIError",
|
|
]);
|
|
|
|
/** 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 },
|
|
{ name: "%systemErrno", type: F64 },
|
|
{ name: "%systemCall", type: STRING },
|
|
{ name: "%systemPath", type: STRING },
|
|
{ name: "%systemDest", type: STRING },
|
|
{ name: "%systemMessage", type: STRING },
|
|
{ name: "%systemCode", type: STRING },
|
|
{ name: "%nativePlain", type: BOOL },
|
|
{ name: "%errnoOrder", type: BOOL },
|
|
],
|
|
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" },
|
|
],
|
|
// The ECMAScript error constructors take (message?, options?);
|
|
// DOMException, AggregateError, AssertionError and SuppressedError
|
|
// have their own construction and are not values.
|
|
...(ERROR_CLASS_VALUES.has(irName)
|
|
? {
|
|
valueCtorParams: [
|
|
{ type: DYN, mode: "omittable" as const },
|
|
{ type: DYN, mode: "omittable" as const },
|
|
],
|
|
}
|
|
: {}),
|
|
base,
|
|
subclasses: [],
|
|
throwingSetters: [],
|
|
staticFields: [],
|
|
};
|
|
if (base) {
|
|
base.subclasses.push(info);
|
|
lowerer.subclassLinks++;
|
|
}
|
|
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: [],
|
|
valueCtorParams: [],
|
|
base: null,
|
|
subclasses: [],
|
|
throwingSetters: [],
|
|
staticFields: [],
|
|
};
|
|
lowerer.classes.set(RUNTIME_EMITTER_CLASS, info);
|
|
}
|
|
|
|
/** `class Bus extends require("events") {}` in a CommonJS module: the
|
|
* events module is the EventEmitter class itself, so the base is the
|
|
* runtime emitter, exactly as through a `require("events")` binding. */
|
|
export function builtinRequireBaseOf(lowerer: Lowerer, node: ts.Expression): ClassInfo | null {
|
|
let expr = node;
|
|
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
|
|
if (!ts.isCallExpression(expr) || !isJsSourceFile(expr.getSourceFile())) return null;
|
|
const spec = requireSpecOf(expr);
|
|
if (spec === null || canonicalBuiltinModule(spec) !== "events") return null;
|
|
if (!isCjsRequireGlobal(lowerer, expr.expression)) return null;
|
|
return lowerer.classes.get(RUNTIME_EMITTER_CLASS) ?? null;
|
|
}
|
|
|
|
/** 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: [],
|
|
// Construction through a class value builds the stream without
|
|
// options (lowerNew reports options passed through a value).
|
|
valueCtorParams: [],
|
|
base,
|
|
subclasses: [],
|
|
throwingSetters: [],
|
|
staticFields: [],
|
|
};
|
|
if (base) {
|
|
base.subclasses.push(info);
|
|
lowerer.subclassLinks++;
|
|
}
|
|
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). A const binding of
|
|
* one (`const R = Readable`) names the same class (builtinClassAliasOf). */
|
|
export function builtinStreamInfoOf(
|
|
lowerer: Lowerer,
|
|
symbol: ts.Symbol | null | undefined,
|
|
): ClassInfo | null {
|
|
if (!symbol) return null;
|
|
const alias = builtinClassAliasOf(lowerer, symbol);
|
|
if (alias !== null) return alias.kind === "stream" ? alias.info : 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).
|
|
// A `var`/`let` binding of the inline require member
|
|
// (`var Transform = require("stream").Transform`) is the same alias
|
|
// plumbing as builtinMemberRequireDecl's: its declaration owns no
|
|
// storage either way.
|
|
const decl = symbol.valueDeclaration;
|
|
if (
|
|
decl &&
|
|
ts.isVariableDeclaration(decl) &&
|
|
((ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0 ||
|
|
builtinMemberRequireDecl(decl.name, decl.initializer)) &&
|
|
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 === ts.symbolName(symbol)) irName = name;
|
|
}
|
|
if (!irName) return null;
|
|
// Both the bundled declarations and @types/node declare the classes
|
|
// inside `declare module "stream"` (@types/node nests them in the module's
|
|
// `internal` namespace).
|
|
const declared = symbol.declarations.some((d) => {
|
|
if (!ts.isClassDeclaration(d)) 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 {
|
|
if (ts.isIdentifier(expression)) {
|
|
const symbol = lowerer.checker.getSymbolAtLocation(expression);
|
|
if (
|
|
symbol &&
|
|
symbol.declarations.some(
|
|
(decl) =>
|
|
ts.isParameter(decl) &&
|
|
((ts.isConstructorDeclaration(decl.parent) &&
|
|
!checkedJsDocumentedParameter(lowerer, decl)) ||
|
|
(lowerer.typeOf(expression).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0),
|
|
)
|
|
)
|
|
return DYN;
|
|
}
|
|
// Shape inference's overlay types a field as a record or array only when
|
|
// the analysis proves its values never reach code that could observe a
|
|
// copy, so those fields keep their native layout.
|
|
if (
|
|
(type.kind === "array" || type.kind === "record") &&
|
|
!shapeOverlayAnnotated(expression.parent?.parent ?? expression)
|
|
)
|
|
return DYN;
|
|
// JavaScript documentation cannot guarantee that a call or an indexed
|
|
// read returns a present number. Numeric fields receiving those values
|
|
// must retain undefined, so later arithmetic performs JS coercion (NaN).
|
|
if (type.kind === "f64" && isJsSourceFile(ts.getSourceFileOfNode(expression))) {
|
|
const assignment = expression.parent;
|
|
const left = ts.isBinaryExpression(assignment) ? assignment.left : null;
|
|
const field =
|
|
left &&
|
|
ts.isPropertyAccessExpression(left) &&
|
|
left.expression.kind === ts.SyntaxKind.ThisKeyword
|
|
? left.name.text
|
|
: null;
|
|
let owner: ts.Node | undefined = expression.parent;
|
|
while (owner && !ts.isClassDeclaration(owner) && !ts.isClassExpression(owner))
|
|
owner = owner.parent;
|
|
if (field && owner) {
|
|
let acceptsUnchecked = false;
|
|
const scan = (node: ts.Node): void => {
|
|
if (
|
|
acceptsUnchecked ||
|
|
(node !== owner && (ts.isClassDeclaration(node) || ts.isClassExpression(node)))
|
|
)
|
|
return;
|
|
if (
|
|
ts.isBinaryExpression(node) &&
|
|
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
ts.isPropertyAccessExpression(node.left) &&
|
|
node.left.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
|
node.left.name.text === field
|
|
) {
|
|
let rhs = node.right;
|
|
while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression;
|
|
if (ts.isCallExpression(rhs) || ts.isElementAccessExpression(rhs))
|
|
acceptsUnchecked = true;
|
|
}
|
|
ts.forEachChild(node, scan);
|
|
};
|
|
scan(owner);
|
|
if (acceptsUnchecked) return DYN;
|
|
}
|
|
}
|
|
let node = expression;
|
|
while (ts.isParenthesizedExpression(node)) node = node.expression;
|
|
if (ts.isCallExpression(node) && type.kind === "bytes") {
|
|
let prototypeArgument = false;
|
|
for (const argument of node.arguments.nodes)
|
|
ts.walkPreorder(argument, (part) => {
|
|
if (
|
|
!ts.isPropertyAccessExpression(part) ||
|
|
part.expression.kind !== ts.SyntaxKind.ThisKeyword
|
|
)
|
|
return;
|
|
const symbol = lowerer.checker.getSymbolAtLocation(part.name);
|
|
if (
|
|
symbol &&
|
|
symbol.declarations.some((decl) => {
|
|
const member = ts.isBinaryExpression(decl) ? decl.left : decl;
|
|
return (
|
|
ts.isPropertyAccessExpression(member) &&
|
|
ts.isPropertyAccessExpression(member.expression) &&
|
|
member.expression.name.text === "prototype"
|
|
);
|
|
})
|
|
)
|
|
prototypeArgument = true;
|
|
// JS checkers can synthesize a property declaration for prototype
|
|
// assignments. Keep the source ownership proof when that happens.
|
|
let owner: ts.Node | undefined = node.parent;
|
|
while (owner && !(ts.isClassDeclaration(owner) || ts.isClassExpression(owner)))
|
|
owner = owner.parent;
|
|
if (owner && (ts.isClassDeclaration(owner) || ts.isClassExpression(owner)) && owner.name) {
|
|
const className = owner.name.text;
|
|
for (const statement of ts.getSourceFileOfNode(owner).statements.nodes) {
|
|
if (
|
|
!ts.isExpressionStatement(statement) ||
|
|
!ts.isBinaryExpression(statement.expression)
|
|
)
|
|
continue;
|
|
const left = statement.expression.left;
|
|
if (
|
|
statement.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
ts.isPropertyAccessExpression(left) &&
|
|
left.name.text === part.name.text &&
|
|
ts.isPropertyAccessExpression(left.expression) &&
|
|
left.expression.name.text === "prototype" &&
|
|
ts.isIdentifier(left.expression.expression) &&
|
|
left.expression.expression.text === className
|
|
)
|
|
prototypeArgument = true;
|
|
}
|
|
}
|
|
});
|
|
// A prototype-selected conversion can return different container kinds
|
|
// in subclasses even when JSDoc describes the base class's default.
|
|
if (prototypeArgument) return DYN;
|
|
}
|
|
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);
|
|
const scalar =
|
|
(inferred.flags & ts.TypeFlags.BooleanLike) !== 0
|
|
? "bool"
|
|
: (inferred.flags & ts.TypeFlags.NumberLike) !== 0
|
|
? "f64"
|
|
: (inferred.flags & ts.TypeFlags.StringLike) !== 0
|
|
? "string"
|
|
: (inferred.flags & ts.TypeFlags.BigIntLike) !== 0
|
|
? "bigint"
|
|
: (inferred.flags & ts.TypeFlags.ESSymbolLike) !== 0
|
|
? "symbol"
|
|
: null;
|
|
if (
|
|
scalar &&
|
|
type.kind !== "dyn" &&
|
|
type.kind !== scalar &&
|
|
!(
|
|
type.kind === "union" &&
|
|
lowerer.unions.get(type.unionId)?.arms.some((arm) => arm.kind === scalar)
|
|
)
|
|
)
|
|
return DYN;
|
|
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;
|
|
}
|
|
|
|
function inheritedJsFieldType(
|
|
lowerer: Lowerer,
|
|
decl: ts.ClassDeclaration | ts.ClassExpression,
|
|
name: string,
|
|
type: IrType,
|
|
): IrType {
|
|
for (const expression of lowerer.inheritedJsFieldWrites.get(decl)?.get(name) ?? []) {
|
|
// Keep one shared layout, including sibling subclasses. A descendant
|
|
// initializer that exceeds the base slot needs checked JS storage.
|
|
if (!typeEquals(type, jsFieldInitializerType(lowerer, type, expression))) return DYN;
|
|
}
|
|
return type;
|
|
}
|
|
|
|
function returnsWidenedJsField(
|
|
lowerer: Lowerer,
|
|
decl: ts.ClassDeclaration | ts.ClassExpression,
|
|
member: ts.MethodDeclaration,
|
|
fields: ReadonlyMap<string, IrType>,
|
|
ret: IrType,
|
|
): boolean {
|
|
if (
|
|
!isJsSourceFile(ts.getSourceFileOfNode(member)) ||
|
|
member.type ||
|
|
member.asteriskToken ||
|
|
hasExplicitJsDocReturn(member)
|
|
)
|
|
return false;
|
|
const widened = (expression: ts.Expression): boolean => {
|
|
if (ts.isParenthesizedExpression(expression)) return widened(expression.expression);
|
|
if (ts.isConditionalExpression(expression))
|
|
return widened(expression.whenTrue) || widened(expression.whenFalse);
|
|
if (ts.isCallExpression(expression)) {
|
|
const callee = expression.expression;
|
|
const symbol = ts.isPropertyAccessExpression(callee)
|
|
? lowerer.checker.getSymbolAtLocation(callee.name)
|
|
: ts.isElementAccessExpression(callee)
|
|
? lowerer.checker.getPropertyOfType(
|
|
lowerer.typeOf(callee.expression),
|
|
lowerer.foldedStringKeyOf(callee.argumentExpression) ?? "",
|
|
)
|
|
: undefined;
|
|
if (
|
|
symbol &&
|
|
symbol.declarations.some(
|
|
(declaration) =>
|
|
ts.isMethodDeclaration(declaration) && lowerer.checkedVirtualJsMethods.has(declaration),
|
|
)
|
|
)
|
|
return true;
|
|
}
|
|
const name = ts.isPropertyAccessExpression(expression)
|
|
? expression.name.text
|
|
: ts.isElementAccessExpression(expression)
|
|
? lowerer.foldedStringKeyOf(expression.argumentExpression)
|
|
: null;
|
|
if (ts.isPropertyAccessExpression(expression)) {
|
|
const symbol = lowerer.checker.getSymbolAtLocation(expression.name);
|
|
if (
|
|
symbol &&
|
|
symbol.declarations.some(
|
|
(declaration) =>
|
|
(ts.isGetAccessorDeclaration(declaration) ||
|
|
ts.isSetAccessorDeclaration(declaration)) &&
|
|
lowerer.checkedVirtualJsAccessors.has(declaration),
|
|
)
|
|
)
|
|
return true;
|
|
}
|
|
if (
|
|
name !== null &&
|
|
(ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression)) &&
|
|
expression.expression.kind === ts.SyntaxKind.ThisKeyword
|
|
) {
|
|
return (
|
|
fields.get(name)?.kind === "dyn" ||
|
|
inheritedJsFieldType(lowerer, decl, name, ret).kind === "dyn"
|
|
);
|
|
}
|
|
return false;
|
|
};
|
|
let result = false;
|
|
const scan = (node: ts.Node): void => {
|
|
if (result || ts.isFunctionLike(node)) return;
|
|
if (ts.isReturnStatement(node) && node.expression && widened(node.expression)) result = true;
|
|
ts.forEachChild(node, scan);
|
|
};
|
|
if (member.body) scan(member.body);
|
|
return result;
|
|
}
|
|
|
|
/** 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 = p.declarations[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 p.declarations) {
|
|
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 === "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. A
|
|
* const binding of it (`const E = EventEmitter`) names the same class
|
|
* (builtinClassAliasOf). */
|
|
export function builtinEmitterInfoOf(
|
|
lowerer: Lowerer,
|
|
symbol: ts.Symbol | null | undefined,
|
|
): ClassInfo | null {
|
|
if (!symbol) return null;
|
|
const alias = builtinClassAliasOf(lowerer, symbol);
|
|
if (alias !== null) return alias.kind === "emitter" ? alias.info : 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 = symbol.valueDeclaration;
|
|
if (
|
|
decl !== undefined &&
|
|
ts.isVariableDeclaration(decl) &&
|
|
((ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0 ||
|
|
builtinMemberRequireDecl(decl.name, decl.initializer)) &&
|
|
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 (ts.symbolName(symbol) !== "EventEmitter") return null;
|
|
const declared = symbol.declarations.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. A const binding
|
|
* of one (`const E = TypeError`) names the same class
|
|
* (builtinClassAliasOf). */
|
|
export function builtinErrorInfoOf(
|
|
lowerer: Lowerer,
|
|
symbol: ts.Symbol | null | undefined,
|
|
): ClassInfo | null {
|
|
if (!symbol) return null;
|
|
const alias = builtinClassAliasOf(lowerer, symbol);
|
|
if (alias !== null) return alias.kind === "error" ? alias.info : null;
|
|
if (!lowerer.isStdlibSymbol(symbol)) return null;
|
|
for (const [irName, rec] of RUNTIME_ERROR_CLASSES) {
|
|
if (rec.lib === ts.symbolName(symbol)) 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?: ts.NodeArray<ts.Node> }).modifiers?.nodes ?? []) 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?.nodes.find((c) => c.token === ts.SyntaxKind.ExtendsKeyword)
|
|
?.types?.nodes[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.nodes) {
|
|
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 {
|
|
if (
|
|
lowerer.classes.has(lowerer.classNamer(decl)) ||
|
|
lowerer.collectingClassDeclarations.has(decl)
|
|
)
|
|
return;
|
|
lowerer.collectingClassDeclarations.add(decl);
|
|
try {
|
|
collectClassShapeDeferring(lowerer, decl);
|
|
} finally {
|
|
lowerer.collectingClassDeclarations.delete(decl);
|
|
}
|
|
}
|
|
|
|
function collectClassShapeDeferring(lowerer: Lowerer, decl: ts.ClassDeclaration): void {
|
|
const symbol = lowerer.collectDeferring(
|
|
() => declSymbolOf(lowerer, decl),
|
|
() => {
|
|
// A forward type reference can demand this class before collection
|
|
// reaches its earlier base declaration. Collect that dependency first;
|
|
// a later same-file base remains subject to the runtime order boundary.
|
|
const heritage = decl.heritageClauses?.nodes.find(
|
|
(clause) => clause.token === ts.SyntaxKind.ExtendsKeyword,
|
|
)?.types?.nodes[0]?.expression;
|
|
const reference =
|
|
heritage && ts.isPropertyAccessExpression(heritage) ? heritage.name : heritage;
|
|
const baseSymbol =
|
|
reference && ts.isIdentifier(reference) ? lowerer.resolveValueSymbol(reference) : undefined;
|
|
const baseDeclaration = baseSymbol
|
|
? baseSymbol.declarations.find(ts.isClassDeclaration)
|
|
: undefined;
|
|
if (
|
|
baseDeclaration &&
|
|
!lowerer.isStdlibSymbol(baseSymbol ?? undefined) &&
|
|
!ts.getSourceFileOfNode(baseDeclaration).isDeclarationFile &&
|
|
(ts.getSourceFileOfNode(baseDeclaration) !== ts.getSourceFileOfNode(decl) ||
|
|
ts.tokenStartOfNode(baseDeclaration) < ts.tokenStartOfNode(decl))
|
|
)
|
|
lowerer.collectClassShape(baseDeclaration);
|
|
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 dropping the
|
|
// declaration without a diagnostic would drop observable side effects (the
|
|
// classStaticBlock13/28 miscompiles).
|
|
const hasDeclTimeCode = (n: ts.Node): boolean =>
|
|
ts.isClassStaticBlockDeclaration(n) ||
|
|
((n as { modifiers?: ts.NodeArray<ts.Node> }).modifiers?.nodes ?? []).some(
|
|
(m) => m.kind === ts.SyntaxKind.Decorator,
|
|
);
|
|
if (symbol && (hasDeclTimeCode(decl) || decl.members.nodes.some(hasDeclTimeCode))) {
|
|
lowerer.reportDeferred(symbol);
|
|
}
|
|
// 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?.nodes
|
|
.find((h) => h.token === ts.SyntaxKind.ExtendsKeyword)
|
|
?.types?.nodes.map((t) => t.expression)
|
|
.filter(ts.isIdentifier)[0];
|
|
const baseSym = baseIdent ? lowerer.resolveValueSymbol(baseIdent) : null;
|
|
if (baseSym) lowerer.reportDeferred(baseSym);
|
|
}
|
|
}
|
|
|
|
function inferredEmptyCollectionFieldType(
|
|
lowerer: Lowerer,
|
|
decl: ts.ClassLikeDeclaration,
|
|
field: string,
|
|
initializer: ts.Expression,
|
|
): IrType | null {
|
|
if (
|
|
!ts.isNewExpression(initializer) ||
|
|
(initializer.arguments?.nodes.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);
|
|
// Inferred JS collection fields keep functions in checked slots.
|
|
if (
|
|
!mapped ||
|
|
mapped.kind === "dyn" ||
|
|
mapped.kind === "void" ||
|
|
mapped.kind === "func" ||
|
|
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.nodes.length === 2
|
|
) {
|
|
const key = mappedArgument(node.arguments.nodes[0]!);
|
|
const value = mappedArgument(node.arguments.nodes[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.nodes.length === 1
|
|
) {
|
|
const element = mappedArgument(node.arguments.nodes[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.nodes) {
|
|
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?: {
|
|
path: InstantiationPath;
|
|
family: ClassInfo;
|
|
name: string;
|
|
bindings: Map<ts.Symbol, IrType>;
|
|
tsBindings: Map<ts.Symbol, ts.Type>;
|
|
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.nodes.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(ts.getSourceFileOfNode(decl))) {
|
|
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?.nodes.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
|
|
|
|
// Collection follows module order for runtime heritage. Ordinary
|
|
// instances use the family as their storage base; generic heritage
|
|
// resolves a concrete base under each instance's type bindings.
|
|
const computedBase = lowerer.computedClassBases.get(lowerer.classNamer(decl));
|
|
let base: ClassInfo | null = inst
|
|
? inst.family
|
|
: mixin
|
|
? mixin.base
|
|
: (computedBase?.classInfo ?? null);
|
|
let genericBase: ts.ExpressionWithTypeArguments | undefined;
|
|
let callableBase: ClassInfo["callableBase"];
|
|
let extendsDate = false;
|
|
let extendsArray: { elem: IrType } | undefined;
|
|
let factoryBaseExpression: ts.Expression | undefined;
|
|
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 ? signatures[0]!.declaration : undefined;
|
|
if (
|
|
!source ||
|
|
!isJsSourceFile(ts.getSourceFileOfNode(source)) ||
|
|
!(ts.isFunctionDeclaration(source) || ts.isFunctionExpression(source))
|
|
)
|
|
return false;
|
|
callableBase = {
|
|
expression,
|
|
constructorId: `%g.${className}.base`,
|
|
prototypeId: `%g.${className}.basePrototype`,
|
|
};
|
|
return true;
|
|
};
|
|
const ownGenericBase = inst?.family.genericBase;
|
|
if (inst && ownGenericBase) {
|
|
const mapped = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(ownGenericBase));
|
|
base = mapped?.kind === "object" ? (lowerer.classes.get(mapped.className) ?? null) : null;
|
|
if (!base || base.generic)
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
ownGenericBase,
|
|
"a generic base whose type arguments have no concrete layout",
|
|
);
|
|
}
|
|
for (const clause of inst || mixin || computedBase ? [] : (decl.heritageClauses?.nodes ?? [])) {
|
|
// `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.nodes[0];
|
|
if (familyMode && t) {
|
|
const reference = ts.isPropertyAccessExpression(t.expression)
|
|
? t.expression.name
|
|
: t.expression;
|
|
const symbol = ts.isIdentifier(reference)
|
|
? lowerer.resolveValueSymbol(reference)
|
|
: undefined;
|
|
const familyBase = symbol ? lowerer.classBySymbol.get(symbol) : undefined;
|
|
if (familyBase?.generic) {
|
|
if (symbol && ts.isPropertyAccessExpression(t.expression))
|
|
fenceEarlyNsMemberRef(lowerer, t.expression, symbol);
|
|
base = familyBase;
|
|
genericBase = t;
|
|
continue;
|
|
}
|
|
}
|
|
// `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" &&
|
|
globalError.def.name !== "%AggregateError"
|
|
) {
|
|
if (t.typeArguments?.nodes) 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?.nodes) 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 (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)`,
|
|
);
|
|
}
|
|
if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes");
|
|
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?.nodes)
|
|
lowerer.unsupported("SC1090", t, "extending generic classes");
|
|
const baseDecl = propBase.decl;
|
|
if (
|
|
baseDecl != null &&
|
|
ts.getSourceFileOfNode(baseDecl) === ts.getSourceFileOfNode(decl) &&
|
|
ts.tokenStartOfNode(baseDecl) > ts.tokenStartOfNode(decl)
|
|
) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
t,
|
|
`extending '${ts.getTextOfNode(t.expression)}' 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 &&
|
|
memberSym.declarations.filter(
|
|
(d) => ts.isBinaryExpression(d) && d.operatorToken.kind === ts.SyntaxKind.EqualsToken,
|
|
).length > 1;
|
|
if (rebinds) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
t,
|
|
`extending the reassigned property '${ts.getTextOfNode(t.expression)}' (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?.nodes) 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?.nodes) {
|
|
const requiredBase = builtinRequireBaseOf(lowerer, t.expression);
|
|
if (requiredBase) {
|
|
base = requiredBase;
|
|
continue;
|
|
}
|
|
const mixinBase = lowerer.mixinCallClassInfoOf(t.expression);
|
|
if (mixinBase) {
|
|
base = mixinBase;
|
|
continue;
|
|
}
|
|
const stored = storedClassValueType(lowerer, t.expression);
|
|
const factoryBase =
|
|
stored?.kind === "classval" ? lowerer.classes.get(stored.className) : undefined;
|
|
if (factoryBase) {
|
|
base = factoryBase;
|
|
factoryBaseExpression = t.expression;
|
|
continue;
|
|
}
|
|
}
|
|
if (!t || !ts.isIdentifier(t.expression)) {
|
|
if (t && adoptCallableBase(t.expression)) continue;
|
|
lowerer.unsupported("SC1090", clause, "extending computed expressions");
|
|
}
|
|
if ((familyMode || inst) && lowerer.isStdlibGlobal(t.expression, "Array"))
|
|
lowerer.unsupported("SC1090", t, "a generic class that extends Array");
|
|
if (!familyMode && !inst && !mixin && lowerer.isStdlibGlobal(t.expression, "Array")) {
|
|
extendsArray = arrayClassBase(lowerer, t);
|
|
refuseArrayClassSpecies(lowerer, decl);
|
|
continue;
|
|
}
|
|
if (!familyMode && !inst && !mixin && lowerer.isStdlibGlobal(t.expression, "Date")) {
|
|
if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes");
|
|
extendsDate = true;
|
|
continue;
|
|
}
|
|
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)",
|
|
);
|
|
}
|
|
// `extends AggregateError`: the errors list is the same kind of
|
|
// hidden runtime slot.
|
|
if (lowerer.builtinErrorInfoOf(symbol)?.def.name === "%AggregateError") {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
t,
|
|
"extending AggregateError (its runtime layout carries a hidden errors slot a subclass would overlap — extend Error and keep the list in a field)",
|
|
);
|
|
}
|
|
if (lowerer.builtinErrorInfoOf(symbol)?.def.name === "%AssertionError") {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
t,
|
|
"extending AssertionError (its constructor takes an options object the subclass constructor would have to forward — extend Error, or construct AssertionError directly)",
|
|
);
|
|
}
|
|
if (lowerer.builtinErrorInfoOf(symbol)?.def.name === "%SuppressedError") {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
t,
|
|
"extending SuppressedError (extend Error and keep error and suppressed in fields)",
|
|
);
|
|
}
|
|
const named =
|
|
(symbol && lowerer.classBySymbol.get(symbol)) ??
|
|
lowerer.builtinErrorInfoOf(symbol) ??
|
|
lowerer.builtinEmitterInfoOf(symbol) ??
|
|
builtinStreamInfoOf(lowerer, symbol) ??
|
|
builtinEventTargetBaseOf(lowerer, t.expression) ??
|
|
// 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?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes");
|
|
if (named?.extendsArray)
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
t,
|
|
`extending '${t.expression.text}', a class that extends Array (one level of Array subclass compiles)`,
|
|
);
|
|
base = named;
|
|
if (!base) {
|
|
if (adoptCallableBase(t.expression)) continue;
|
|
// A base whose own collection deferred can still register later
|
|
// (a flush retry with dynamic slots): the derived class retries
|
|
// with it (Lowerer.retryDeferred).
|
|
const diag = unsupportedDiag(
|
|
"SC1090",
|
|
locOf(t),
|
|
`extending classes not declared in the program ('${t.expression.text}')`,
|
|
);
|
|
lowerer.retriableDiags.add(diag);
|
|
// A base the program declares but rejected: this rejection is its
|
|
// cascade (localized failure, report-only).
|
|
const rejectedBase = rejectedProgramClassGroup(lowerer, symbol);
|
|
if (rejectedBase !== null) diag.cause = [rejectedBase];
|
|
lowerer.pushDiag(diag);
|
|
throw new PoisonError();
|
|
}
|
|
}
|
|
|
|
if (base?.localClass && !functionLocalClass(decl) && !computedBase && !factoryBaseExpression) {
|
|
const heritage = decl.heritageClauses?.nodes.find(
|
|
(clause) => clause.token === ts.SyntaxKind.ExtendsKeyword,
|
|
)?.types?.nodes[0];
|
|
if (heritage) factoryBaseExpression = heritage.expression;
|
|
else
|
|
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.builtinEventTarget !== 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>();
|
|
const returnAdapters = new Map<string, { params: ParamShape[]; ret: IrType; async: boolean }>();
|
|
// 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[] = [];
|
|
// Public JS accessors need live constructor descriptors, including
|
|
// receiver-sensitive getters and setters inherited by subclasses.
|
|
const checkedStatics =
|
|
isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
|
|
((base && hasRuntimeStatics(base)) ||
|
|
definedThroughReflection(lowerer, decl) ||
|
|
decl.members.nodes.some(
|
|
(member) =>
|
|
ts.isAccessor(member) &&
|
|
!ts.isPrivateIdentifier(member.name) &&
|
|
member.modifiers?.nodes.some(
|
|
(modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword,
|
|
),
|
|
)) &&
|
|
!decl.members.nodes.some(
|
|
(member) =>
|
|
member.name &&
|
|
ts.isPrivateIdentifier(member.name) &&
|
|
ts
|
|
.getModifiers(member)
|
|
?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword),
|
|
);
|
|
// 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!.nodes) {
|
|
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.nodes) {
|
|
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.nodes) {
|
|
if (
|
|
(functionLocalClass(decl) || checkedStatics) &&
|
|
(isJsSourceFile(ts.getSourceFileOfNode(decl)) ||
|
|
(functionLocalClass(decl) && ts.isMethodDeclaration(member))) &&
|
|
(ts.isClassStaticBlockDeclaration(member) ||
|
|
ts
|
|
.getModifiers(member)
|
|
?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword))
|
|
) {
|
|
runtimeStatics.push(member);
|
|
continue;
|
|
}
|
|
if (
|
|
functionLocalClass(decl) &&
|
|
isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
|
|
(ts.isMethodDeclaration(member) || ts.isAccessor(member)) &&
|
|
member.body &&
|
|
!ts.isPrivateIdentifier(member.name)
|
|
) {
|
|
runtimePrototypeMembers.push(member);
|
|
if (
|
|
ts.isComputedPropertyName(member.name) &&
|
|
classMemberNameOf(lowerer, member.name) === null
|
|
)
|
|
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 initializerSymbol = ts.isIdentifier(member.initializer)
|
|
? lowerer.resolveValueSymbol(member.initializer)
|
|
: null;
|
|
// Enum namespace aliases are erased; member reads fold through the checker.
|
|
if (initializerSymbol && (initializerSymbol.flags & ts.SymbolFlags.Enum) !== 0) continue;
|
|
const type = lowerer.irTypeOf(member.name);
|
|
if (type.kind === "void") lowerer.badType(member.name, lowerer.typeOf(member.name));
|
|
if (
|
|
type.kind === "dyn" &&
|
|
!lowerer.liftDynFieldPolicy &&
|
|
!isJsSourceFile(ts.getSourceFileOfNode(member)) &&
|
|
!lowerer.classFieldStoresDyn(lowerer.typeOf(member.name)) &&
|
|
(member.type !== undefined ||
|
|
(lowerer.typeOf(member.name).flags & ts.TypeFlags.Any) === 0)
|
|
) {
|
|
lowerer.dynFieldPolicyFence(
|
|
member.name,
|
|
`${dynFieldTypeSpelling(lowerer, member.name)}-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(ts.getSourceFileOfNode(decl)) &&
|
|
!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") {
|
|
if (
|
|
ts.isPropertyDeclaration(member) &&
|
|
!member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.StaticKeyword)
|
|
) {
|
|
// A TypeScript redeclaration is the JavaScript field: Node
|
|
// defines the Error's own `cause` (undefined without an
|
|
// initializer) after super() returns. `declare` and `abstract`
|
|
// declarations are erased and define nothing. Every `.cause`
|
|
// read of an Error-rooted class is the checked-dynamic property
|
|
// (fieldTarget's errorCause container), so only types that read
|
|
// back unchanged from it are accepted.
|
|
if (!isJsSourceFile(ts.getSourceFileOfNode(member))) {
|
|
if (
|
|
modifiers?.some(
|
|
(m) =>
|
|
m.kind === ts.SyntaxKind.DeclareKeyword ||
|
|
m.kind === ts.SyntaxKind.AbstractKeyword,
|
|
)
|
|
)
|
|
continue;
|
|
if (!lowerer.classFieldStoresDyn(lowerer.typeOf(member.name)))
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
memberName,
|
|
"redeclaring Error.cause with a type other than 'unknown' or 'object' (declare it 'cause?: unknown' and narrow at the read)",
|
|
);
|
|
}
|
|
fieldOrder.push({
|
|
name: "cause",
|
|
type: DYN,
|
|
initializer: member.initializer,
|
|
...(member.initializer ? {} : { undefinedInitializer: locOf(member) }),
|
|
redeclared: true,
|
|
errorCause: true,
|
|
});
|
|
continue;
|
|
}
|
|
lowerer.unsupported("SC1090", memberName, "Error.cause accessors");
|
|
}
|
|
// #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 instead of compiling to split behavior — 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?.nodes.some((m) => m.kind === ts.SyntaxKind.StaticKeyword)
|
|
) {
|
|
const reason = emitOverrideShapeReason(lowerer, member);
|
|
if (reason === null) {
|
|
const eventSym = lowerer.checker.getSymbolAtLocation(member.parameters.nodes[0]!.name);
|
|
const restSym = lowerer.checker.getSymbolAtLocation(member.parameters.nodes[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 inherited EventTarget/Event members read the engine's own
|
|
// state: dispatch never calls a subclass override, and the engine
|
|
// never reads an own field that shadows an accessor (`type`).
|
|
if (
|
|
memberName &&
|
|
ts.isIdentifier(memberName) &&
|
|
!(ts.getCombinedModifierFlags(member as ts.Declaration) & ts.ModifierFlags.Static) &&
|
|
eventTargetApiMembers(base)?.has(memberName.text)
|
|
) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
memberName,
|
|
`overriding the ${eventTargetRootOf(base) === "target" ? "EventTarget" : "Event"} member '${memberName.text}' (the runtime owns the inherited 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` on a Readable, 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" && !streamExtraCallbacks(streamBase.def.name).includes("writev")) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
memberName,
|
|
`declaring '_writev' on a ${streamBase.def.name.slice(1)} subclass (a Readable has no writable side)`,
|
|
);
|
|
}
|
|
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("/")})`,
|
|
);
|
|
}
|
|
}
|
|
// A completion callback declared with the broad `Function` type
|
|
// (`cb: Function`) takes the checked-dynamic callable the runtime
|
|
// passes implicitly typed callbacks; calls through it are
|
|
// dynamic calls of that callable.
|
|
const callbackIndex = STREAM_COMPLETION_CALLBACK_INDEX.get(name);
|
|
const callbackParam =
|
|
ts.isMethodDeclaration(member) && callbackIndex !== undefined
|
|
? member.parameters.nodes[callbackIndex]
|
|
: undefined;
|
|
if (
|
|
callbackParam !== undefined &&
|
|
ts.isIdentifier(callbackParam.name) &&
|
|
!callbackParam.dotDotDotToken &&
|
|
!callbackParam.questionToken &&
|
|
!callbackParam.initializer &&
|
|
lowerer.checker.typeToString(lowerer.typeOf(callbackParam.name)) === "Function"
|
|
)
|
|
lowerer.checkedCallbackParams.add(callbackParam);
|
|
}
|
|
}
|
|
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 resolve the concrete instance's
|
|
// property through checked class dispatch.
|
|
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) ||
|
|
ts.isStringLiteralLike(member.name) ||
|
|
ts.isNumericLiteral(member.name)
|
|
) {
|
|
const symbolKey = ts.isComputedPropertyName(member.name)
|
|
? classSymbolKeyOf(lowerer, member.name.expression)
|
|
: null;
|
|
const stringKey = ts.isComputedPropertyName(member.name)
|
|
? lowerer.foldedStringKeyOf(member.name.expression)
|
|
: ts.isNumericLiteral(member.name)
|
|
? String(Number(member.name.text))
|
|
: member.name.text;
|
|
const fieldName = symbolKey?.fieldName ?? stringKey;
|
|
// A TypeScript field without an initializer takes its declared
|
|
// type and the identifier fields' definite-assignment rule (an
|
|
// unconditional `this[key] = ...` at the constructor's top
|
|
// level), checked with the unguarded fields below.
|
|
const declaredOnly =
|
|
!member.initializer && !isJsSourceFile(ts.getSourceFileOfNode(member)) && !!member.type;
|
|
if (
|
|
fieldName === null ||
|
|
(!member.initializer &&
|
|
!isJsSourceFile(ts.getSourceFileOfNode(member)) &&
|
|
!declaredOnly)
|
|
) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
member,
|
|
"computed class fields without an initializer and a stable string, number, or 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 || declaredOnly
|
|
? (lowerer.mapTypeOf(inferred) ?? dynFallbackType(lowerer, member, inferred))
|
|
: DYN;
|
|
let type =
|
|
mapped?.kind === "record" &&
|
|
isJsSourceFile(ts.getSourceFileOfNode(member)) &&
|
|
!member.type
|
|
? DYN
|
|
: mapped
|
|
? jsOpenObjectType(member, mapped, lowerer.shapes, lowerer.unions)
|
|
: null;
|
|
if (
|
|
!type ||
|
|
type.kind === "void" ||
|
|
(type.kind === "dyn" &&
|
|
!isJsSourceFile(ts.getSourceFileOfNode(member)) &&
|
|
!lowerer.classFieldStoresDyn(inferred))
|
|
)
|
|
lowerer.badType(member, inferred);
|
|
if (isJsSourceFile(ts.getSourceFileOfNode(member))) {
|
|
type = inheritedJsFieldType(lowerer, decl, fieldName, type);
|
|
if (base?.fields.get(fieldName)?.kind === "dyn") type = DYN;
|
|
if (base && !fields.has(fieldName) && lowerer.dynConvertible(type)) type = DYN;
|
|
}
|
|
const undefinedInitializer =
|
|
!member.initializer && (!declaredOnly || type.kind === "dyn")
|
|
? locOf(member)
|
|
: undefined;
|
|
if (
|
|
declaredOnly &&
|
|
type.kind !== "dyn" &&
|
|
!(
|
|
type.kind === "union" &&
|
|
lowerer.unions.get(type.unionId)?.arms.some((a) => a.kind === "undefinedT")
|
|
)
|
|
) {
|
|
unguardedFields.push({
|
|
node: member.name,
|
|
name: fieldName,
|
|
why: `initializer-less computed fields not assigned at the constructor's top level (nothing guarantees '${symbolKey ? symbolFieldDisplayName(fieldName) : fieldName}' is assigned before a read — assign it unconditionally at the top of the constructor, give it an initializer, or include undefined in its type)`,
|
|
});
|
|
}
|
|
const previous = fields.get(fieldName);
|
|
if (previous) {
|
|
if (symbolKey && !symbolFields.has(symbolKey.identity)) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
member.name,
|
|
`distinct keys sharing the printable name '${symbolFieldDisplayName(fieldName)}' in one class`,
|
|
);
|
|
}
|
|
if (!typeEquals(previous, type)) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
member.name,
|
|
"redeclaring computed fields at a different type",
|
|
);
|
|
}
|
|
fieldOrder.push({
|
|
name: fieldName,
|
|
type,
|
|
initializer: member.initializer,
|
|
...(undefinedInitializer ? { undefinedInitializer } : {}),
|
|
redeclared: true,
|
|
});
|
|
} else {
|
|
fields.set(fieldName, type);
|
|
fieldOrder.push({
|
|
name: fieldName,
|
|
type,
|
|
initializer: member.initializer,
|
|
...(undefinedInitializer ? { undefinedInitializer } : {}),
|
|
});
|
|
}
|
|
if (symbolKey) symbolFields.set(symbolKey.identity, 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);
|
|
if (isJsSourceFile(ts.getSourceFileOfNode(member)) && !member.initializer) {
|
|
const constructor = decl.members.nodes.find(
|
|
(part): part is ts.ConstructorDeclaration =>
|
|
ts.isConstructorDeclaration(part) && !!part.body,
|
|
);
|
|
for (const statement of constructor?.body?.statements?.nodes ?? []) {
|
|
if (
|
|
!ts.isExpressionStatement(statement) ||
|
|
!ts.isBinaryExpression(statement.expression) ||
|
|
statement.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken
|
|
)
|
|
continue;
|
|
const left = statement.expression.left;
|
|
if (
|
|
!ts.isPropertyAccessExpression(left) ||
|
|
left.expression.kind !== ts.SyntaxKind.ThisKeyword ||
|
|
left.name.text !== member.name.text
|
|
)
|
|
continue;
|
|
type =
|
|
statement.expression.right.kind === ts.SyntaxKind.NullKeyword
|
|
? DYN
|
|
: jsFieldInitializerType(lowerer, type, statement.expression.right);
|
|
}
|
|
}
|
|
// Unannotated JavaScript callback slots retain the original
|
|
// callable and its receiver instead of adapting its signature.
|
|
if (
|
|
isJsSourceFile(ts.getSourceFileOfNode(member)) &&
|
|
!member.type &&
|
|
!/@type\b/.test(
|
|
ts.getSourceFileOfNode(member).text.slice(member.pos, ts.tokenStartOfNode(member)),
|
|
) &&
|
|
(type.kind === "func" || type.kind === "record") &&
|
|
lowerer.dynConvertible(type)
|
|
)
|
|
type = DYN;
|
|
// JS permits reads before a constructor's first assignment even
|
|
// when its inferred field type omits undefined.
|
|
if (
|
|
isJsSourceFile(ts.getSourceFileOfNode(member)) &&
|
|
!member.initializer &&
|
|
type.kind !== "dyn"
|
|
) {
|
|
type = lowerer.withUndefinedArmOf(type) ?? DYN;
|
|
}
|
|
if (isJsSourceFile(ts.getSourceFileOfNode(member))) {
|
|
type = inheritedJsFieldType(lowerer, decl, member.name.text, type);
|
|
if (base?.fields.get(member.name.text)?.kind === "dyn") type = DYN;
|
|
if (base && !fields.has(member.name.text) && lowerer.dynConvertible(type)) 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. In
|
|
// TypeScript the refusal is a policy fence that a flush retry lifts
|
|
// (Lowerer.liftDynFieldPolicy).
|
|
if (
|
|
type.kind === "dyn" &&
|
|
!lowerer.liftDynFieldPolicy &&
|
|
!isJsSourceFile(ts.getSourceFileOfNode(member)) &&
|
|
!lowerer.classFieldStoresDyn(lowerer.typeOf(member.name))
|
|
) {
|
|
lowerer.dynFieldPolicyFence(
|
|
member.name,
|
|
`${dynFieldTypeSpelling(lowerer, member.name)}-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 === "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.nodes) {
|
|
const isParamProp = p.modifiers?.nodes.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?.nodes.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 as a parameter property (declare 'cause?: unknown' and assign it in the constructor)",
|
|
);
|
|
}
|
|
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.liftDynFieldPolicy &&
|
|
!lowerer.classFieldStoresDyn(lowerer.typeOf(p))
|
|
) {
|
|
lowerer.dynFieldPolicyFence(
|
|
p.name,
|
|
`${dynFieldTypeSpelling(lowerer, p)}-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)`,
|
|
);
|
|
}
|
|
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,
|
|
overrideReturnAbi(lowerer, ret, overridden.sig.ret, className, base),
|
|
))
|
|
) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
member.name,
|
|
"overriding a method with a different signature (parameter types must match the base declaration exactly; returns may narrow to a subclass)",
|
|
);
|
|
}
|
|
const unionReturn =
|
|
overridden?.sig.ret.kind === "union" && !typeEquals(ret, overridden.sig.ret);
|
|
if (unionReturn) lowerer.unionReturnOverrides.add(member);
|
|
methods.set(mName, {
|
|
params: shapes,
|
|
ret: unionReturn ? overridden.sig.ret : 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 generic-functions.ts). 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(ts.getSourceFileOfNode(decl)) &&
|
|
!functionLocalClass(decl) &&
|
|
ts.isIdentifier(member.name) &&
|
|
!lowerer.virtualJsMethods.has(member) &&
|
|
!lowerer.checkedJsOverrideMethods.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);
|
|
const fieldRet = ft.ret.kind === "promise" ? ft.ret.inner : ft.ret;
|
|
if (returnsWidenedJsField(lowerer, decl, member, fields, fieldRet)) {
|
|
ft.ret = ft.ret.kind === "promise" ? { ...ft.ret, inner: DYN } : DYN;
|
|
}
|
|
if (lowerer.checkedVirtualJsMethods.has(member)) {
|
|
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" || lowerer.checkedVirtualJsValueReturns.has(member))
|
|
ft.ret = DYN;
|
|
}
|
|
// JS inference may retain only a symbol sentinel while a method
|
|
// also returns values from an untyped mutable field or local.
|
|
if (
|
|
isJsSourceFile(ts.getSourceFileOfNode(member)) &&
|
|
!member.type &&
|
|
!hasExplicitJsDocReturn(member) &&
|
|
ft.ret.kind === "symbol"
|
|
)
|
|
ft.ret = DYN;
|
|
if (
|
|
isJsSourceFile(ts.getSourceFileOfNode(member)) &&
|
|
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 inherited 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 parameter slot sound. Nominal covariant returns share the
|
|
// base pointer ABI while retaining their declared result type. 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(ts.getSourceFileOfNode(member))) {
|
|
// 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.nodes[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(ts.getSourceFileOfNode(member)) &&
|
|
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" })),
|
|
);
|
|
}
|
|
// A TypeScript override of a JavaScript method (`class T extends
|
|
// Agent` over an npm-static package) keeps the base's checked-value
|
|
// ABI: each untyped base slot stays dyn, and the override's typed
|
|
// parameter narrows from it at entry with a checked conversion. An
|
|
// untyped base return takes the override's value boxed. Calls on
|
|
// a statically typed receiver see the same ABI.
|
|
if (
|
|
overridden &&
|
|
!isJsSourceFile(ts.getSourceFileOfNode(member)) &&
|
|
overridden.declarer.decl !== null &&
|
|
isJsSourceFile(ts.getSourceFileOfNode(overridden.declarer.decl)) &&
|
|
member.asteriskToken === undefined &&
|
|
!overridden.sig.async &&
|
|
!overridden.sig.gen &&
|
|
shapes.length <= overridden.sig.params.length &&
|
|
shapes.every((p, i) => {
|
|
const inherited = overridden.sig.params[i]!;
|
|
return (
|
|
(p.mode === "required" || p.mode === "omittable") &&
|
|
p.bodyType === undefined &&
|
|
(inherited.mode === "required" || inherited.mode === "omittable") &&
|
|
(typeEquals(p.type, inherited.type) ||
|
|
(inherited.type.kind === "dyn" && lowerer.dynConvertible(p.type)))
|
|
);
|
|
}) &&
|
|
overridden.sig.params
|
|
.slice(shapes.length)
|
|
.every(
|
|
(p) => p.type.kind === "dyn" && (p.mode === "required" || p.mode === "omittable"),
|
|
)
|
|
) {
|
|
for (let i = 0; i < shapes.length; i++) {
|
|
const own = shapes[i]!;
|
|
if (overridden.sig.params[i]!.type.kind === "dyn" && own.type.kind !== "dyn")
|
|
shapes[i] = { type: DYN, mode: own.mode, entryNarrow: own.type };
|
|
}
|
|
shapes.push(
|
|
...overridden.sig.params
|
|
.slice(shapes.length)
|
|
.map((p): ParamShape => ({ type: p.type, mode: "omittable" })),
|
|
);
|
|
if (
|
|
overridden.sig.ret.kind === "dyn" &&
|
|
ft.ret.kind !== "dyn" &&
|
|
ft.ret.kind !== "void" &&
|
|
lowerer.dynConvertible(ft.ret)
|
|
)
|
|
ft.ret = DYN;
|
|
ft.params = shapes.map((shape) => shape.type);
|
|
}
|
|
if (
|
|
overridden &&
|
|
member.asteriskToken === undefined &&
|
|
(overridden.sig.params.length !== shapes.length ||
|
|
!overridden.sig.params.every((p, i) => typeEquals(p.type, shapes[i]!.type)) ||
|
|
!typeEquals(
|
|
overridden.sig.ret,
|
|
overrideReturnAbi(lowerer, ft.ret, overridden.sig.ret, className, base),
|
|
))
|
|
) {
|
|
const fencedJsOverride =
|
|
isJsSourceFile(ts.getSourceFileOfNode(member)) &&
|
|
overridden.declarer.decl !== null &&
|
|
isJsSourceFile(ts.getSourceFileOfNode(overridden.declarer.decl)) &&
|
|
overridden.sig.gen === undefined;
|
|
// A TypeScript override that only changes the result, into a
|
|
// type that converts into the inherited one (`Promise<void>` over
|
|
// an `any` base, `string | undefined` over `string`): the slot
|
|
// keeps the inherited ABI and converts the body's result at the
|
|
// boundary (lowerClassMethodMemberInner).
|
|
const ownAsync =
|
|
member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true;
|
|
if (
|
|
!fencedJsOverride &&
|
|
!functionLocalClass(decl) &&
|
|
overridden.sig.gen === undefined &&
|
|
// An async base's slot answers its promise: a Promise type, or
|
|
// the checked-dynamic value of an untyped async method.
|
|
(overridden.sig.async !== true ||
|
|
overridden.sig.ret.kind === "promise" ||
|
|
overridden.sig.ret.kind === "dyn") &&
|
|
!overridden.sig.abstract &&
|
|
overridden.sig.params.length === shapes.length &&
|
|
overridden.sig.params.every(
|
|
(p, i) =>
|
|
p.mode === shapes[i]!.mode &&
|
|
(p.mode === "required" || p.mode === "omittable") &&
|
|
// Each inherited argument converts into the override's
|
|
// parameter: equal, a checked extraction from an `any`
|
|
// slot, or a widening (a contravariant parameter).
|
|
(typeEquals(p.type, shapes[i]!.type) ||
|
|
(p.type.kind === "dyn" &&
|
|
canDynCheckTo(
|
|
shapes[i]!.type,
|
|
(id) => lowerer.shapes.get(id),
|
|
(id) => lowerer.unions.get(id),
|
|
)) ||
|
|
lowerer.coercibleValue(p.type, shapes[i]!.type)),
|
|
) &&
|
|
(ownAsync ? ft.ret.kind === "promise" : true) &&
|
|
(typeEquals(ft.ret, overridden.sig.ret) ||
|
|
(overridden.sig.ret.kind === "dyn"
|
|
? ft.ret.kind !== "void" && lowerer.dynConvertible(ft.ret)
|
|
: lowerer.coercibleValue(ft.ret, overridden.sig.ret) ||
|
|
// A promise whose payload widens into the inherited one
|
|
// (`Promise<void>` over an untyped async `Promise<any>`).
|
|
(ft.ret.kind === "promise" &&
|
|
overridden.sig.ret.kind === "promise" &&
|
|
lowerer.promisePayloadWidens(ft.ret, overridden.sig.ret)) ||
|
|
// A JavaScript override's inferred `any` result checks
|
|
// into the inherited type (`toString() { return
|
|
// this.message }` over Error's).
|
|
(ft.ret.kind === "dyn" &&
|
|
isJsSourceFile(ts.getSourceFileOfNode(member)) &&
|
|
overridden.sig.ret.kind !== "void" &&
|
|
canDynCheckTo(
|
|
overridden.sig.ret,
|
|
(id) => lowerer.shapes.get(id),
|
|
(id) => lowerer.unions.get(id),
|
|
))))
|
|
) {
|
|
returnAdapters.set(mName, { params: [...shapes], ret: ft.ret, async: ownAsync });
|
|
shapes.splice(0, shapes.length, ...overridden.sig.params);
|
|
ft.ret = overridden.sig.ret;
|
|
if (overridden.sig.async === true) thunkAsyncSlot(overridden, mName);
|
|
} else 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)) &&
|
|
!member.parameters.nodes.some((parameter, index) => {
|
|
if (!parameter.initializer) return false;
|
|
const baseMember = overridden.declarer.decl?.members?.nodes.find(
|
|
(candidate) =>
|
|
ts.isMethodDeclaration(candidate) &&
|
|
classMemberNameOf(lowerer, candidate.name) === mName,
|
|
);
|
|
const inherited =
|
|
baseMember && ts.isMethodDeclaration(baseMember)
|
|
? baseMember.parameters.nodes[index]?.initializer
|
|
: undefined;
|
|
if (!inherited) return false;
|
|
const ownType = lowerer.mapTypeOf(lowerer.typeOf(parameter.initializer));
|
|
const baseType = lowerer.mapTypeOf(lowerer.typeOf(inherited));
|
|
return ownType !== null && baseType !== null && !typeEquals(ownType, baseType);
|
|
});
|
|
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 types must match the base declaration exactly; returns may narrow to a subclass)`,
|
|
),
|
|
);
|
|
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 types must match the base declaration exactly; returns may narrow to a subclass)",
|
|
);
|
|
}
|
|
}
|
|
// A return narrowed to part of an inherited union returns through
|
|
// the union ABI; call sites narrow it back to the declared type.
|
|
if (overridden?.sig.ret.kind === "union" && !typeEquals(ft.ret, overridden.sig.ret)) {
|
|
ft.ret = overridden.sig.ret;
|
|
lowerer.unionReturnOverrides.add(member);
|
|
}
|
|
const asyncMember =
|
|
member.modifiers?.nodes.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.
|
|
// An override chain touching async methods dispatches through
|
|
// plain slot functions that return the body's promise: the async
|
|
// bodies lower as `%C.m$own` behind them (thunkAsyncSlot).
|
|
if (
|
|
overridden &&
|
|
!returnAdapters.has(mName) &&
|
|
(asyncMember || overridden.sig.async === true) &&
|
|
!isJsSourceFile(ts.getSourceFileOfNode(member)) &&
|
|
!functionLocalClass(decl) &&
|
|
member.asteriskToken === undefined &&
|
|
overridden.sig.gen === undefined &&
|
|
!overridden.sig.abstract &&
|
|
ft.ret.kind === "promise" &&
|
|
typeEquals(ft.ret, overridden.sig.ret) &&
|
|
overridden.sig.params.length === shapes.length &&
|
|
overridden.sig.params.every(
|
|
(p, i) => typeEquals(p.type, shapes[i]!.type) && p.mode === shapes[i]!.mode,
|
|
)
|
|
) {
|
|
if (overridden.sig.async === true) thunkAsyncSlot(overridden, mName);
|
|
if (asyncMember)
|
|
returnAdapters.set(mName, { params: [...shapes], ret: ft.ret, async: true });
|
|
}
|
|
if (
|
|
overridden &&
|
|
!returnAdapters.has(mName) &&
|
|
(asyncMember || overridden.sig.async === true)
|
|
) {
|
|
const message = `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 (isJsSourceFile(ts.getSourceFileOfNode(member)))
|
|
methodEntryFences.set(mName, unsupportedDiag("SC1090", locOf(member.name), message));
|
|
else lowerer.unsupported("SC1090", member.name, message);
|
|
}
|
|
if (
|
|
overridden &&
|
|
(member.asteriskToken !== undefined || overridden.sig.gen !== undefined)
|
|
) {
|
|
const message = `overriding the generator method '${mName}' (generator methods dispatch statically — a virtual slot cannot enter their spawn wrapper)`;
|
|
if (isJsSourceFile(ts.getSourceFileOfNode(member)))
|
|
methodEntryFences.set(mName, unsupportedDiag("SC1090", locOf(member.name), message));
|
|
else lowerer.unsupported("SC1090", member.name, message);
|
|
}
|
|
// 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 && !returnAdapters.has(mName)
|
|
? { 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) &&
|
|
isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
|
|
base?.fields.has(prop) &&
|
|
!fieldOrder.some((field) => field.name === prop)
|
|
) {
|
|
// JS may override an inherited data field with a prototype
|
|
// accessor. Retain the base storage prefix for its constructor;
|
|
// property dispatch on this subclass selects the accessor.
|
|
fields.delete(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) {
|
|
let ret = lowerer.declaredReturnType(member, member.name);
|
|
// A getter that never returns a value: typed `never` (it only
|
|
// throws, Next.js's removed `page`/`ua` getters) or, unannotated
|
|
// with no return statement, inferred `void`. A read answers
|
|
// undefined or throws, so the property takes the checked-dynamic
|
|
// slot instead of VOID.
|
|
if (
|
|
ret.kind === "void" &&
|
|
(lowerer.typeOf(member.name).flags & (ts.TypeFlags.Never | ts.TypeFlags.Void)) !== 0
|
|
)
|
|
ret = DYN;
|
|
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.nodes), ret: VOID };
|
|
}
|
|
if (lowerer.checkedVirtualJsAccessors.has(member)) {
|
|
if (isGet) sig.ret = DYN;
|
|
else for (const parameter of sig.params) parameter.type = DYN;
|
|
}
|
|
// 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,
|
|
})),
|
|
);
|
|
}
|
|
|
|
// Fields read before their first assignment. Construction that hands
|
|
// the instance to other code (a method call, an argument, a callback,
|
|
// a base constructor that does either) before assigning a class-typed
|
|
// field lets that code read the field while Node still holds
|
|
// undefined. Such fields take the deferred-init representation, whose
|
|
// slot (a nullable instance pointer) starts empty and whose reads check
|
|
// it. Fields assigned before any exposure keep their plain reads.
|
|
const derived =
|
|
decl.heritageClauses?.nodes.some((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword) ??
|
|
false;
|
|
// Generic instances and mixins share layouts across declarations, so
|
|
// they only report whether construction exposes the instance.
|
|
const escape: { escapes: boolean; assigned: Set<string> | null } =
|
|
familyMode || mixin
|
|
? { escapes: derived, assigned: null }
|
|
: constructionEscape({
|
|
ctor,
|
|
fields: fieldOrder.flatMap((f) =>
|
|
paramProps.some((pp) => pp.name === f.name)
|
|
? []
|
|
: [{ name: f.name, initializer: f.initializer }],
|
|
),
|
|
paramProps: paramProps.map((pp) => pp.name),
|
|
derived,
|
|
baseEscapes:
|
|
base === null ||
|
|
callableBase !== undefined ||
|
|
factoryBaseExpression !== undefined ||
|
|
base.constructionEscapes === true,
|
|
isField: (name) =>
|
|
fields.has(name) &&
|
|
!methods.has(`set:${name}`) &&
|
|
!lowerer.findMethodOn(base, `set:${name}`),
|
|
privateMethodBody: (call) => {
|
|
const callee = call.expression;
|
|
if (!ts.isPropertyAccessExpression(callee)) return null;
|
|
const name = callee.name.text;
|
|
const declaration = decl.members.nodes.find(
|
|
(member): member is ts.MethodDeclaration =>
|
|
ts.isMethodDeclaration(member) &&
|
|
member.body !== undefined &&
|
|
(ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) &&
|
|
member.name.text === name,
|
|
);
|
|
if (!declaration?.body || declaration.asteriskToken) return null;
|
|
const modifiers = ts.getModifiers(declaration) ?? [];
|
|
if (
|
|
modifiers.some(
|
|
(m) =>
|
|
m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.AsyncKeyword,
|
|
)
|
|
)
|
|
return null;
|
|
const isPrivate =
|
|
ts.isPrivateIdentifier(declaration.name) ||
|
|
modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword);
|
|
return isPrivate ? declaration.body : null;
|
|
},
|
|
});
|
|
// 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?.nodes ?? []) {
|
|
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);
|
|
} else if (
|
|
ts.isExpressionStatement(stmt) &&
|
|
ts.isBinaryExpression(stmt.expression) &&
|
|
stmt.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
ts.isElementAccessExpression(stmt.expression.left) &&
|
|
stmt.expression.left.expression.kind === ts.SyntaxKind.ThisKeyword
|
|
) {
|
|
const key = stmt.expression.left.argumentExpression;
|
|
const name = classSymbolKeyOf(lowerer, key)?.fieldName ?? lowerer.foldedStringKeyOf(key);
|
|
if (name !== null) topAssigned.add(name);
|
|
}
|
|
}
|
|
for (const f of unguardedFields) {
|
|
if (topAssigned.has(f.name) || escape.assigned?.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 !== "dyn" &&
|
|
declared.kind !== "map" &&
|
|
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);
|
|
}
|
|
}
|
|
|
|
if (escape.assigned && !inst) {
|
|
let earlyReads: Set<string> | null = null;
|
|
for (const f of fieldOrder) {
|
|
if (f.redeclared || escape.assigned.has(f.name) || deferredInitFields.has(f.name)) continue;
|
|
if (paramProps.some((pp) => pp.name === f.name)) continue;
|
|
const declared = fields.get(f.name);
|
|
// Records, arrays and strings are pointer slots that start empty
|
|
// too, but only take the checked slot when a method the
|
|
// constructor calls actually reads them: their other early
|
|
// exposures keep the plain slot.
|
|
if (
|
|
declared?.kind === "record" ||
|
|
declared?.kind === "array" ||
|
|
declared?.kind === "string"
|
|
) {
|
|
earlyReads ??= constructorMethodReads(decl, base, ctor ?? undefined);
|
|
if (!earlyReads.has(f.name)) continue;
|
|
} else if (
|
|
declared?.kind !== "object" ||
|
|
lowerer.classes.get(declared.className)?.def.runtime
|
|
)
|
|
continue;
|
|
const armed = lowerer.withUndefinedArm(declared);
|
|
if (armed === null || armed.kind !== "union") continue;
|
|
fields.set(f.name, armed);
|
|
f.type = armed;
|
|
deferredInitFields.add(f.name);
|
|
}
|
|
}
|
|
|
|
// 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 reading undefined without a diagnostic.
|
|
if (isJsSourceFile(ts.getSourceFileOfNode(decl))) {
|
|
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 (!ts.symbolName(p).startsWith("__@")) continue;
|
|
const keySym = lateBoundKeySymOf(lowerer, p);
|
|
if (keySym) lateBoundByKey.set(keySym, p);
|
|
}
|
|
// A subclass field created before anything can observe the instance
|
|
// keeps its type: super() cannot run code that reads it earlier.
|
|
const baseConstruction =
|
|
base && !accessorInterceptsBase(accessorNames(methods.keys()), base)
|
|
? construction(base)
|
|
: null;
|
|
const sealedPrefix = baseConstruction
|
|
? constructionPrefix(
|
|
decl,
|
|
baseConstruction,
|
|
(name) => !hasAccessor(name) && !methods.has(name) && !lowerer.findMethodOn(base, name),
|
|
)
|
|
: null;
|
|
if (ctor?.body) {
|
|
for (const stmt of ctor.body.statements.nodes) {
|
|
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)) {
|
|
// An own JS callable shadows its prototype declaration.
|
|
// Keep the method in the prototype and store the assigned
|
|
// value in the instance property bag.
|
|
lowerer.noteMethodAccess(name, assign);
|
|
continue;
|
|
}
|
|
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(ts.getSourceFileOfNode(decl))
|
|
)
|
|
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 does not narrow 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;
|
|
}
|
|
}
|
|
type = inheritedJsFieldType(lowerer, decl, name, type);
|
|
// super() can invoke a subclass setter or method before its
|
|
// constructor assignments run. These slots start undefined.
|
|
if (base && lowerer.dynConvertible(type) && !sealedPrefix?.created.has(name))
|
|
type = DYN;
|
|
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);
|
|
const stringKey = lowerer.foldedStringKeyOf(lhs.argumentExpression);
|
|
const fieldName = key?.fieldName ?? stringKey;
|
|
if (fieldName === null) {
|
|
if (lowerer.mapTypeOf(lowerer.typeOf(lhs.argumentExpression))?.kind === "symbol") {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
lhs,
|
|
"symbol-keyed class fields require a stable module-level key",
|
|
);
|
|
}
|
|
continue;
|
|
}
|
|
if (!key && (fields.has(fieldName) || hasAccessor(fieldName))) continue;
|
|
if (!key && (methods.has(fieldName) || lowerer.findMethodOn(base, fieldName))) {
|
|
lowerer.noteMethodAccess(fieldName, lhs);
|
|
continue;
|
|
}
|
|
// A key already declared (own or inherited) makes later
|
|
// assignments writes, not declarations.
|
|
if (key && symbolFields.has(key.identity)) continue;
|
|
if (fields.has(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(fieldName)}' in one class`,
|
|
);
|
|
}
|
|
const propSym = key
|
|
? lateBoundByKey.get(key.sym)
|
|
: instType
|
|
? lowerer.checker.getPropertyOfType(instType, fieldName)
|
|
: undefined;
|
|
// 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));
|
|
type = inheritedJsFieldType(lowerer, decl, fieldName, type);
|
|
if (base && lowerer.dynConvertible(type)) type = DYN;
|
|
fields.set(fieldName, type);
|
|
if (key) symbolFields.set(key.identity, fieldName);
|
|
fieldOrder.push({ name: 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 && ts.symbolName(p) === "cause") continue;
|
|
if (fields.has(ts.symbolName(p)) || methods.has(ts.symbolName(p))) continue;
|
|
if (hasAccessor(ts.symbolName(p))) continue;
|
|
if (
|
|
base &&
|
|
(base.fields.has(ts.symbolName(p)) || lowerer.findMethodOn(base, ts.symbolName(p)))
|
|
)
|
|
continue;
|
|
const site = p.declarations.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 (ts.symbolName(p).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 compile-time fence too: an annotated
|
|
// program can spell `T | undefined` itself.
|
|
if (isJsSourceFile(ts.getSourceFileOfNode(decl))) {
|
|
const original = undefArmedFieldType(lowerer, p);
|
|
const armed =
|
|
original === null
|
|
? null
|
|
: inheritedJsFieldType(lowerer, decl, ts.symbolName(p), original);
|
|
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(ts.symbolName(p), armed);
|
|
fieldOrder.push({ name: ts.symbolName(p), type: armed, initializer: undefined });
|
|
continue;
|
|
}
|
|
}
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
site,
|
|
`fields assigned outside the constructor's top level ('this.${ts.symbolName(p)}' 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.nodes.some((p) => p.dotDotDotToken)) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
ctor.parameters.nodes.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.nodes)
|
|
: (base?.ctorParams ??
|
|
(callableBase || extendsDate || extendsArray ? [{ type: DYN, mode: "dynRest" }] : []));
|
|
if (
|
|
ctor &&
|
|
!mixinForwarding &&
|
|
isJsSourceFile(ts.getSourceFileOfNode(ctor)) &&
|
|
!ctorParams.some((shape) => shape.mode === "rest" || shape.mode === "dynRest") &&
|
|
bodyReadsArguments(ctor)
|
|
) {
|
|
ctorParams.push({ type: DYN, mode: "arguments" });
|
|
}
|
|
|
|
const info: ClassInfo = {
|
|
...(runtimeStatics.length || checkedStatics ? { 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 } : {}),
|
|
...(decl.members.nodes.some(
|
|
(member) =>
|
|
ts.isPropertyDeclaration(member) &&
|
|
!ts.isPrivateIdentifier(member.name) &&
|
|
member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.StaticKeyword),
|
|
)
|
|
? { ownStaticFields: true as const }
|
|
: {}),
|
|
jsLength:
|
|
ctor?.parameters?.nodes.findIndex(
|
|
(parameter) => parameter.initializer || parameter.dotDotDotToken,
|
|
) === -1
|
|
? ctor.parameters.nodes.length
|
|
: Math.max(
|
|
0,
|
|
ctor?.parameters?.nodes.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: [
|
|
...(extendsDate ? [{ name: DATE_TIME_FIELD, type: DATE_T }] : []),
|
|
...(base?.def.fields ?? []),
|
|
...fieldOrder
|
|
.filter((f) => f.redeclared !== true)
|
|
.map((f) => ({ name: f.name, type: f.type })),
|
|
],
|
|
...(methods.size > 0 ? { methods: [...methods.keys()] } : {}),
|
|
...((): { prototypeNames?: string[] } => {
|
|
const names = classPrototypeNames(decl);
|
|
// A [util.inspect.custom] method, by its reserved slot name: the
|
|
// checked-dynamic inspect walk refuses instances that have one.
|
|
if (methods.has(INSPECT_CUSTOM_MEMBER)) names.push(INSPECT_CUSTOM_MEMBER);
|
|
return names.length > 0 ? { prototypeNames: names } : {};
|
|
})(),
|
|
...(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 } : {}),
|
|
...(extendsArray
|
|
? {
|
|
arrayElem: extendsArray.elem,
|
|
arrayFields: `%${className}.arrayFields`,
|
|
arraySpecies: `%${className}.species`,
|
|
}
|
|
: {}),
|
|
loc: locOf(decl),
|
|
},
|
|
fields,
|
|
fieldOrder,
|
|
methods,
|
|
...(methodEntryFences.size > 0 ? { methodEntryFences } : {}),
|
|
...(returnAdapters.size > 0 ? { returnAdapters } : {}),
|
|
decl,
|
|
...(emitOverride !== undefined ? { emitOverride } : {}),
|
|
ctor,
|
|
ctorParams,
|
|
...(paramProps.length > 0 ? { paramProps } : {}),
|
|
base,
|
|
...(genericBase ? { genericBase } : {}),
|
|
...(callableBase ? { callableBase } : {}),
|
|
...(extendsDate ? { extendsDate: true as const } : {}),
|
|
...(extendsArray ? { extendsArray } : {}),
|
|
...(factoryBaseExpression ? { factoryBaseExpression } : {}),
|
|
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 } : {}),
|
|
...(escape.escapes ? { constructionEscapes: true as const } : {}),
|
|
};
|
|
let root: ClassInfo | null = info;
|
|
while (root?.base) root = root.base;
|
|
if (root !== info && root?.def.name === "%Error") {
|
|
const after = errorNameAfterField(info, errorViewFields(info.def.fields));
|
|
if (after !== undefined) info.def.errorNameAfter = after;
|
|
}
|
|
// 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 = {
|
|
path: inst.path,
|
|
family: inst.family,
|
|
bindings: inst.bindings,
|
|
tsBindings: inst.tsBindings,
|
|
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.nodes) {
|
|
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.subclassLinks++;
|
|
}
|
|
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 (factoryBaseExpression && !functionLocalClass(decl)) {
|
|
info.def.baseValueGlobal = `%g.${className}.computedBase`;
|
|
lowerer.globalsList.push({
|
|
id: info.def.baseValueGlobal,
|
|
name: `${className} base`,
|
|
type: { kind: "classval", className: base!.def.name },
|
|
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 {
|
|
let gci = lowerer.genericClassByDecl.get(decl);
|
|
if (!gci && ts.isClassDeclaration(decl)) {
|
|
// A method signature can reference a generic class declared later in
|
|
// the same module. Resolve its family now rather than freezing the
|
|
// signature at the erased family layout for the rest of the build.
|
|
lowerer.collectClassShape(decl);
|
|
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[] = [];
|
|
const tsArgs: ts.Type[] = [];
|
|
if (args.length === gci.typeParams.length) {
|
|
for (const a of args) {
|
|
tsArgs.push(lowerer.typeParamTsResolver(a) ?? a);
|
|
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 bound = lowerer.typeParamTsBindings?.get(tp);
|
|
if (bound) tsArgs.push(bound);
|
|
}
|
|
}
|
|
// A type parameter that a conditional type in the class depends on
|
|
// (`K extends "required" ? T : T | undefined`) is bound to its checker
|
|
// type for the members, and that type keys the instance: mapType alone
|
|
// widens a literal, so Field<string, "required"> and Field<string,
|
|
// "optional"> would otherwise share one layout. The other parameters get
|
|
// no checker binding, so nothing in the members depends on a literal
|
|
// the key does not carry.
|
|
gci.literalSensitive ??= literalSensitiveTypeParams(lowerer, gci);
|
|
const sensitive = (i: number): boolean =>
|
|
tsArgs.length === gci.typeParams.length && gci.literalSensitive!.has(gci.typeParams[i]!);
|
|
const checkerKey = gci.typeParams
|
|
.map((_, i) => (sensitive(i) ? `#${tsArgs[i]!.id}` : "*"))
|
|
.join(",");
|
|
const key =
|
|
mapped.map(typeKey).join(",") + (gci.literalSensitive.size > 0 ? `@${checkerKey}` : "");
|
|
const literalText = (i: number): string | null => {
|
|
const arg = sensitive(i) ? tsArgs[i] : undefined;
|
|
if (arg?.isStringLiteralType()) return JSON.stringify(arg.value);
|
|
if (arg?.isNumberLiteralType()) return String(arg.value);
|
|
if (arg && arg.flags & ts.TypeFlags.BooleanLiteral) return lowerer.checker.typeToString(arg);
|
|
return null;
|
|
};
|
|
const existing = gci.instances.get(key);
|
|
if (existing) {
|
|
if (existing.info) lowerer.noteGenericClassInstanceDemand(existing.info);
|
|
return existing.poisoned ? null : { kind: "object", className: existing.name };
|
|
}
|
|
// Recursive class fields and queued member bodies share the function
|
|
// demand budget. Independent uses do not limit the family size.
|
|
const path = extendInstantiationPath(lowerer.genericInstantiationPath, decl);
|
|
if (!path) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
decl,
|
|
`unbounded generic class instantiation ('${gci.baseName}' exceeded ${MAX_INSTANTIATION_RECURSION} recursive specializations on one demand path)`,
|
|
);
|
|
}
|
|
const ordinal = gci.instances.size;
|
|
// The name does not take the checker type ids of the key: a literal
|
|
// argument is named by its text, another checker type by its
|
|
// description. Two keys that still describe alike number the later one.
|
|
const nameKey =
|
|
mapped.map(typeKey).join(",") +
|
|
(gci.literalSensitive.size > 0
|
|
? `@${gci.typeParams
|
|
.map((_, i) =>
|
|
sensitive(i)
|
|
? (literalText(i) ?? checkerTypeDescription(tsArgs[i]!, lowerer.typeCtx))
|
|
: "*",
|
|
)
|
|
.join(",")}`
|
|
: "");
|
|
const prefix = `${gci.family.def.name}%`;
|
|
const taken = new Set([...gci.instances.values()].map((instance) => instance.name));
|
|
let name = keyedHelperName(lowerer, prefix, nameKey, "");
|
|
for (let n = 1; taken.has(name); n++)
|
|
name = keyedHelperName(lowerer, prefix, `${nameKey}#${n}`, "");
|
|
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 tsBindings = new Map<ts.Symbol, ts.Type>();
|
|
gci.typeParams.forEach((tp, i) => {
|
|
if (sensitive(i)) tsBindings.set(tp, tsArgs[i]!);
|
|
});
|
|
const rendered = mapped.map((m, i) => literalText(i) ?? lowerer.fmt(m)).join(", ");
|
|
const typeArgsText = `<${rendered.length > 80 ? rendered.slice(0, 77) + "..." : rendered}>`;
|
|
const prevBindings = lowerer.typeParamBindings;
|
|
const prevTsBindings = lowerer.typeParamTsBindings;
|
|
const prevContext = lowerer.instantiationContext;
|
|
const previousPath = lowerer.genericInstantiationPath;
|
|
lowerer.genericInstantiationPath = path;
|
|
lowerer.typeParamBindings = bindings;
|
|
lowerer.typeParamTsBindings = tsBindings;
|
|
lowerer.instantiationContext = `instantiating class '${gci.baseName}' with ${typeArgsText}`;
|
|
try {
|
|
lowerer.collectClassShapeInner(decl, undefined, {
|
|
path,
|
|
family: gci.family,
|
|
name,
|
|
bindings,
|
|
tsBindings,
|
|
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.typeParamTsBindings = prevTsBindings;
|
|
lowerer.instantiationContext = prevContext;
|
|
lowerer.genericInstantiationPath = previousPath;
|
|
}
|
|
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 };
|
|
}
|
|
|
|
/** The class's type parameters that a conditional type inside it depends
|
|
* on: the parameters named within a type node that is a conditional type
|
|
* or reaches one through the type aliases and interfaces it refers to
|
|
* (`get(): Value<T, K>` with `type Value<T, K> = K extends "required" ? T
|
|
* : T | undefined`, also when the alias wraps the conditional in a union
|
|
* or another alias). Their arguments pick the conditional's branch. */
|
|
function literalSensitiveTypeParams(lowerer: Lowerer, gci: GenericClassInfo): Set<ts.Symbol> {
|
|
const params = new Set(gci.typeParams);
|
|
const found = new Set<ts.Symbol>();
|
|
const reaches = new Map<ts.Symbol, boolean>();
|
|
const declarationsReach = (symbol: ts.Symbol): boolean => {
|
|
const target =
|
|
symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
|
|
const known = reaches.get(target);
|
|
if (known !== undefined) return known;
|
|
reaches.set(target, false); // a recursive alias reaches through its other parts
|
|
const result = target.declarations.some((d) =>
|
|
ts.isTypeAliasDeclaration(d)
|
|
? reachesConditional(d.type)
|
|
: ts.isInterfaceDeclaration(d)
|
|
? d.members.nodes.some(reachesConditional)
|
|
: false,
|
|
);
|
|
reaches.set(target, result);
|
|
return result;
|
|
};
|
|
const reachesConditional = (node: ts.Node): boolean => {
|
|
if (ts.isConditionalTypeNode(node)) return true;
|
|
if (ts.isTypeReferenceNode(node)) {
|
|
const name = ts.isIdentifier(node.typeName) ? node.typeName : node.typeName.right;
|
|
const symbol = lowerer.checker.getSymbolAtLocation(name);
|
|
if (symbol && !params.has(symbol) && declarationsReach(symbol)) return true;
|
|
}
|
|
let found = false;
|
|
ts.forEachChild(node, (child) => {
|
|
if (!found && reachesConditional(child)) found = true;
|
|
});
|
|
return found;
|
|
};
|
|
const collect = (node: ts.Node): void => {
|
|
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
|
|
const symbol = lowerer.checker.getSymbolAtLocation(node.typeName);
|
|
if (symbol && params.has(symbol)) found.add(symbol);
|
|
}
|
|
ts.forEachChild(node, collect);
|
|
};
|
|
const visit = (node: ts.Node): void => {
|
|
if (ts.isTypeNode(node) && reachesConditional(node)) {
|
|
collect(node);
|
|
return;
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
for (const member of gci.decl.members.nodes) visit(member);
|
|
return found;
|
|
}
|
|
|
|
/** 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 prevTsBindings = lowerer.typeParamTsBindings;
|
|
const prevContext = lowerer.instantiationContext;
|
|
const prevSuppress = lowerer.suppressStats;
|
|
const previousPath = lowerer.genericInstantiationPath;
|
|
lowerer.genericInstantiationPath = gi.path;
|
|
lowerer.typeParamBindings = gi.bindings;
|
|
lowerer.typeParamTsBindings = gi.tsBindings;
|
|
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.typeParamTsBindings = prevTsBindings;
|
|
lowerer.instantiationContext = prevContext;
|
|
lowerer.suppressStats = prevSuppress;
|
|
lowerer.genericInstantiationPath = previousPath;
|
|
}
|
|
}
|
|
|
|
/** 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).
|
|
const diagsBefore = lowerer.diags.length;
|
|
const sinkMark = lowerer.diagSinkMark();
|
|
try {
|
|
return withInstanceBindings(lowerer, info, () => lowerStaticFieldInitsInner(lowerer, info));
|
|
} catch (error) {
|
|
// Heritage and decorators run before the per-field recovery below.
|
|
// Keep their refusal attached to the class declaration instead of
|
|
// letting a poisoned initializer escape the module lowering pass.
|
|
if (!(error instanceof PoisonError)) throw error;
|
|
if (info.decl && isJsSourceFile(ts.getSourceFileOfNode(info.decl))) {
|
|
const fence =
|
|
lowerer.diagSink === null
|
|
? lowerer.deferToRuntimeFence(diagsBefore, info.decl, { kind: "statement" })
|
|
: lowerer.capturedRuntimeFence(sinkMark, info.decl);
|
|
if (fence) return [fence];
|
|
}
|
|
return [];
|
|
}
|
|
}
|
|
|
|
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 (info.factoryBaseExpression && info.def.baseValueGlobal) {
|
|
const loc = locOf(info.factoryBaseExpression);
|
|
const type: IrType = { kind: "classval", className: info.base!.def.name };
|
|
out.push({
|
|
kind: "assign",
|
|
localId: info.def.baseValueGlobal,
|
|
value: lowerer.lowerExprExpecting(info.factoryBaseExpression, type),
|
|
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, loc },
|
|
DYN,
|
|
),
|
|
],
|
|
type: DYN,
|
|
loc,
|
|
},
|
|
loc,
|
|
});
|
|
}
|
|
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,
|
|
});
|
|
}
|
|
if (info.runtimeStatics && !info.localClass && info.decl) {
|
|
out.push({
|
|
kind: "exprStmt",
|
|
expr: initializeRuntimeStatics(lowerer, info, classValueRef(lowerer, info, info.decl)),
|
|
loc: locOf(info.decl),
|
|
});
|
|
}
|
|
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: ts.tokenStartOfNode(f.initializer),
|
|
kind: "field",
|
|
f,
|
|
})),
|
|
...(info.staticBlocks ?? []).map((b): Item => ({
|
|
pos: ts.tokenStartOfNode(b),
|
|
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.countStatement(f.initializer, locOf(f.initializer).file);
|
|
const diagsBefore = lowerer.diags.length;
|
|
const sinkMark = lowerer.diagSinkMark();
|
|
try {
|
|
const previousThis = lowerer.ctx.thisLocal;
|
|
const receiver =
|
|
f.initializer.kind === ts.SyntaxKind.ThisKeyword || hasStaticThis(f.initializer)
|
|
? lowerer.declareHiddenLocal("%staticThis", {
|
|
kind: "classval",
|
|
className: info.def.name,
|
|
})
|
|
: null;
|
|
if (receiver) {
|
|
lowerer.noteEdge(`%${info.def.name}.constructor`);
|
|
out.push({
|
|
kind: "varDecl",
|
|
localId: receiver.id,
|
|
init: {
|
|
kind: "classRef",
|
|
className: info.def.name,
|
|
type: receiver.type,
|
|
loc: locOf(f.initializer),
|
|
},
|
|
loc: locOf(f.initializer),
|
|
});
|
|
}
|
|
let value: IrExpr;
|
|
try {
|
|
if (receiver) lowerer.ctx.thisLocal = receiver;
|
|
value = lowerer.lowerExprExpecting(f.initializer, f.type);
|
|
} finally {
|
|
lowerer.ctx.thisLocal = previousThis;
|
|
}
|
|
out.push({ kind: "assign", localId: f.globalId, value, loc: locOf(f.initializer) });
|
|
} catch (e) {
|
|
if (!(e instanceof PoisonError)) throw e;
|
|
lowerer.countStatementFailed(f.initializer, locOf(f.initializer).file);
|
|
if (isJsSourceFile(ts.getSourceFileOfNode(f.initializer))) {
|
|
const fence =
|
|
lowerer.diagSink === null
|
|
? lowerer.deferToRuntimeFence(diagsBefore, f.initializer, { kind: "statement" })
|
|
: lowerer.capturedRuntimeFence(sinkMark, f.initializer);
|
|
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) ? sym.declarations[0] : undefined;
|
|
const ambientVar =
|
|
vdecl !== undefined &&
|
|
ts.isVariableDeclaration(vdecl) &&
|
|
vdecl.initializer === undefined &&
|
|
(ts.getCombinedModifierFlags(vdecl) & ts.ModifierFlags.Ambient) !== 0 &&
|
|
!ts.getSourceFileOfNode(vdecl).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(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(s, name),
|
|
);
|
|
}
|
|
|
|
/** The `this` members read by the methods and accessors a constructor
|
|
* calls through `this` (transitively, over the class and its bases),
|
|
* outside nested functions. The constructor's own reads are not included:
|
|
* they run where the constructor's statements put them. */
|
|
function constructorMethodReads(
|
|
decl: ts.ClassLikeDeclaration,
|
|
base: ClassInfo | null,
|
|
ctor: ts.ConstructorDeclaration | undefined,
|
|
): Set<string> {
|
|
const reads = new Set<string>();
|
|
const visited = new Set<string>();
|
|
const bodies = (name: string): ts.Node[] => {
|
|
const out: ts.Node[] = [];
|
|
let owner: ts.ClassLikeDeclaration | undefined = decl;
|
|
for (let next = base; owner; owner = next?.decl ?? undefined, next = next?.base ?? null) {
|
|
for (const member of owner.members.nodes) {
|
|
if (
|
|
(ts.isMethodDeclaration(member) || ts.isAccessor(member)) &&
|
|
member.body &&
|
|
(ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) &&
|
|
member.name.text === name
|
|
)
|
|
out.push(member.body);
|
|
}
|
|
}
|
|
return out;
|
|
};
|
|
const enter = (name: string): void => {
|
|
if (visited.has(name)) return;
|
|
visited.add(name);
|
|
for (const body of bodies(name)) scan(body, true);
|
|
};
|
|
// Nested functions (callbacks handed elsewhere) are not followed: only
|
|
// the straight-line code of the constructor and the methods it calls.
|
|
const scan = (node: ts.Node, inMethod: boolean): void => {
|
|
ts.walkPreorder(node, (n) => {
|
|
if (n !== node && ts.isFunctionLike(n)) return "skip";
|
|
if (
|
|
ts.isPropertyAccessExpression(n) &&
|
|
n.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
|
(ts.isIdentifier(n.name) || ts.isPrivateIdentifier(n.name))
|
|
) {
|
|
if (inMethod) reads.add(n.name.text);
|
|
enter(n.name.text);
|
|
}
|
|
return undefined;
|
|
});
|
|
};
|
|
if (ctor?.body) scan(ctor.body, false);
|
|
return reads;
|
|
}
|
|
|
|
/** 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 reflected = reflectedClassStaticMethodValue(lowerer, declarer, name);
|
|
if (reflected) return reflected;
|
|
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)) {
|
|
let stored = lowerer.resolveLocal(blame);
|
|
// A reference from a function hoisted above the class statement.
|
|
if (!stored) {
|
|
const symbol = lowerer.resolveValueSymbol(blame);
|
|
if (symbol && predeclareForwardClass(lowerer, symbol)) 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) {
|
|
if (info.valueCtorParams === undefined) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
blame,
|
|
`builtin classes as values ('${display}' is runtime-provided — reference program-declared classes instead)`,
|
|
);
|
|
}
|
|
defineClassValueConstruct(lowerer, info);
|
|
return {
|
|
kind: "classRef",
|
|
className: info.def.name,
|
|
type: { kind: "classval", className: info.def.name },
|
|
loc: locOf(blame),
|
|
};
|
|
}
|
|
// Classes extending EventEmitter or a stream class construct through
|
|
// their own (or synthesized) `%C.constructor`, whose super call runs the
|
|
// runtime initializer, so the class object's construct thunk serves them
|
|
// like any program class. EventTarget subclasses keep the refusal.
|
|
if (eventTargetRootOf(info) !== null) {
|
|
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),
|
|
};
|
|
}
|
|
|
|
/** Notes a runtime-provided class whose value is named, widened to, or
|
|
* constructed through (a no-op for program classes, whose constructor is
|
|
* their own). runtimeClassConstructs defines `%C.construct` for the
|
|
* noted classes the finished module still references. */
|
|
export function defineClassValueConstruct(lowerer: Lowerer, info: ClassInfo): void {
|
|
if (!info.def.runtime || info.valueCtorParams === undefined) return;
|
|
lowerer.noteEdge(classValueConstructor(info.def).fn);
|
|
lowerer.runtimeClassValues.set(info.def.name, info);
|
|
}
|
|
|
|
/** The `%C.construct` functions of the noted runtime classes that a
|
|
* classRef, newValue or classval upcast in `functions` names. Lowering can
|
|
* name a class speculatively and discard the expression, so the finished
|
|
* functions decide. */
|
|
export function runtimeClassConstructs(lowerer: Lowerer, functions: IrFunction[]): IrFunction[] {
|
|
if (lowerer.runtimeClassValues.size === 0) return [];
|
|
const used = new Set<string>();
|
|
const note = (type: IrType): void => {
|
|
if (type.kind === "classval" && lowerer.runtimeClassValues.has(type.className))
|
|
used.add(type.className);
|
|
};
|
|
const visitor = {
|
|
expr: (e: IrExpr): boolean => {
|
|
if (e.kind === "classRef") note(e.type);
|
|
else if (e.kind === "newValue") note(e.callee.type);
|
|
else if (e.kind === "upcast") {
|
|
note(e.type);
|
|
note(e.value.type);
|
|
}
|
|
return true;
|
|
},
|
|
stmt: (): boolean => true,
|
|
};
|
|
for (const fn of functions) everyStmtList(fn.body, visitor);
|
|
return [...used]
|
|
.sort()
|
|
.map((name) => runtimeClassConstruct(lowerer.runtimeClassValues.get(name)!));
|
|
}
|
|
|
|
/** `%C.construct` for a runtime-provided class value: the thunk's target,
|
|
* which builds the instance through the runtime constructor the direct
|
|
* `new C(...)` uses (emitter.new, a stream's .newDyn without options,
|
|
* error.newOptions). */
|
|
function runtimeClassConstruct(info: ClassInfo): IrFunction {
|
|
const params = info.valueCtorParams ?? [];
|
|
const name = classValueConstructor(info.def).fn;
|
|
const loc = info.def.loc;
|
|
const type: IrType = { kind: "object", className: info.def.name };
|
|
const ids = params.map((_, i) => `a${i}.0`);
|
|
const args = ids.map((id) => varRef(id, DYN, loc));
|
|
let fn: IrLibFn = "error.newOptions";
|
|
if (info.builtinEmitter) fn = "emitter.new";
|
|
else if (info.builtinStream !== undefined) {
|
|
fn = `${streamCtorShape(info.def.name).fn}.newDyn` as IrLibFn;
|
|
args.push(dynUndefinedExpr(loc));
|
|
}
|
|
return {
|
|
name,
|
|
params: params.map((shape, i) => ({ localId: ids[i]!, name: `a${i}`, type: shape.type })),
|
|
returnType: type,
|
|
locals: params.map((shape, i) => ({
|
|
id: ids[i]!,
|
|
name: `a${i}`,
|
|
type: shape.type,
|
|
mutable: false,
|
|
})),
|
|
body: [{ kind: "return", value: { kind: "libCall", fn, args, type, loc }, loc }],
|
|
loc,
|
|
};
|
|
}
|
|
|
|
/** The constructor parameters construction through a class value completes
|
|
* against: a program class's constructor, or a runtime class's
|
|
* `%C.construct`. */
|
|
function classValueParams(info: ClassInfo): ParamShape[] {
|
|
return info.def.runtime ? (info.valueCtorParams ?? info.ctorParams) : info.ctorParams;
|
|
}
|
|
|
|
/** 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 = classValueParams(sub);
|
|
const b = classValueParams(sup);
|
|
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;
|
|
const info = staticReceiverClass(lowerer, expr.expression);
|
|
if (!info) return null;
|
|
return lowerClassStaticRead(lowerer, info, expr.name.text, expr.expression, expr);
|
|
}
|
|
|
|
/** The class a static member read through `receiver` reads exactly: the
|
|
* class a name binds (directly, through a require binding, or through a
|
|
* const alias), or null. */
|
|
export function staticReceiverClass(lowerer: Lowerer, receiver: ts.Expression): ClassInfo | null {
|
|
if (
|
|
!ts.isIdentifier(receiver) &&
|
|
receiver.kind !== ts.SyntaxKind.ThisKeyword &&
|
|
exactClassOfReceiver(lowerer, receiver) === null
|
|
)
|
|
return null;
|
|
const symbol = ts.isIdentifier(receiver) ? lowerer.resolveValueSymbol(receiver) : 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, receiver) ??
|
|
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;
|
|
return info;
|
|
}
|
|
|
|
/** The static member `name` of `info` read through `receiver` (an exact
|
|
* reference to the class, see staticReceiverClass): `C.name` for the
|
|
* property access `site`, or a destructuring element over `C` whose
|
|
* initializer is `site`. Null when the class has no such member. */
|
|
export function lowerClassStaticRead(
|
|
lowerer: Lowerer,
|
|
info: ClassInfo,
|
|
name: string,
|
|
receiver: ts.Expression,
|
|
site: ts.Expression,
|
|
): IrExpr | null {
|
|
const loc = locOf(site);
|
|
const found = findStaticOn(lowerer, info, name);
|
|
// A Date static read as a value through a class declared `extends Date`:
|
|
// the statics lower only as calls.
|
|
if (
|
|
!found &&
|
|
dateRootOf(info) &&
|
|
["now", "parse", "UTC"].includes(name) &&
|
|
!(ts.isCallExpression(site.parent) && site.parent.expression === site)
|
|
)
|
|
lowerer.noLowering(`Date.${name} read through a class that extends Date`, site);
|
|
if (hasRuntimeStatics(info) && !found?.field)
|
|
return {
|
|
kind: "dynKeyGet",
|
|
value: lowerer.coerceToExpected(classValueRef(lowerer, info, receiver), DYN),
|
|
key: { kind: "strLit", value: name, 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 — reported as SC1090 instead of resolved.
|
|
if (found && name.startsWith("#") && found.declarer !== info) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
site,
|
|
`reading the private static '${name}' 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) {
|
|
const value: IrExpr = {
|
|
kind: "varRef",
|
|
localId: found.field.globalId,
|
|
type: found.field.type,
|
|
loc,
|
|
};
|
|
return found.field.runtimeOptional || !ts.isPropertyAccessExpression(site)
|
|
? value
|
|
: lowerer.maybeNarrow(value, site);
|
|
}
|
|
if (found) {
|
|
return staticMethodValue(lowerer, found.declarer, name, found.method, site, 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, name);
|
|
if (gfound) return lowerer.lowerGenericFnValue(site, 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). A class of an
|
|
// import cycle reads it from its class object instead, whose reference
|
|
// checks the class's initialization (checkCycleBindingAccesses).
|
|
if (name === "name" && info.def.jsName !== undefined) {
|
|
if (lowerer.classInitFlags.get(info.def.name)?.cycle === true)
|
|
return {
|
|
kind: "libCall",
|
|
fn: "class.name",
|
|
args: [classValueRef(lowerer, info, receiver)],
|
|
type: STRING,
|
|
loc,
|
|
};
|
|
return { kind: "strLit", value: info.def.jsName, type: STRING, loc };
|
|
}
|
|
if (info.callableBase)
|
|
return {
|
|
kind: "dynKeyGet",
|
|
value: lowerer.coerceToExpected(classValueRef(lowerer, info, receiver), DYN),
|
|
key: { kind: "strLit", value: name, type: STRING, loc },
|
|
type: DYN,
|
|
loc,
|
|
};
|
|
const data = classStaticDataFor(lowerer, info, name, loc);
|
|
if (data) {
|
|
const value: IrExpr = {
|
|
kind: "dynKeyGet",
|
|
value: data,
|
|
key: { kind: "strLit", value: name, type: STRING, loc },
|
|
type: DYN,
|
|
loc,
|
|
};
|
|
// JavaScript often assigns public statics after the class declaration.
|
|
// Their storage stays mutable and checked, while reads retain the
|
|
// checker's concrete type instead of losing the method receiver.
|
|
const type = isJsSourceFile(ts.getSourceFileOfNode(site))
|
|
? null
|
|
: lowerer.mapTypeOf(lowerer.typeOf(site));
|
|
return type ? lowerer.coerceToExpected(value, type) : value;
|
|
}
|
|
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 "";
|
|
}
|
|
|
|
/** Each evaluation of a function-local class creates fresh private names,
|
|
* while the compiled class shares one layout across evaluations. A
|
|
* JavaScript class's prototype methods are closures per evaluation, so the
|
|
* names agree when every private member is an instance field and every use
|
|
* is on `this` (privateNamesOnlyOnThis): native methods only run on their
|
|
* own evaluation's instances, and the closures, which receive any `this`,
|
|
* check the receiver's evaluation against the class's own binding
|
|
* (private-brands.ts), so the class must be a named declaration (undici's
|
|
* webidl FastIterableIterator). The checker rejects writes to that binding. */
|
|
function localPrivateFieldsAgree(expr: ts.ClassLikeDeclaration): boolean {
|
|
if (!isJsSourceFile(ts.getSourceFileOfNode(expr)) || !ts.isClassDeclaration(expr) || !expr.name)
|
|
return false;
|
|
if (
|
|
expr.members.nodes.some(
|
|
(member) =>
|
|
member.name !== undefined &&
|
|
ts.isPrivateIdentifier(member.name) &&
|
|
!ts.isPropertyDeclaration(member),
|
|
)
|
|
)
|
|
return false;
|
|
return privateNamesOnlyOnThis(expr);
|
|
}
|
|
|
|
/** Every private name is used only on `this` inside the class's own instance
|
|
* members (`this.#x`, `#x in this`), outside nested classes and non-arrow
|
|
* functions, and the class has no static members that could hand instances
|
|
* across evaluations. */
|
|
function privateNamesOnlyOnThis(expr: ts.ClassLikeDeclaration): boolean {
|
|
let ok = true;
|
|
for (const member of expr.members.nodes) {
|
|
if (ts.getModifiers(member)?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword)) return false;
|
|
ts.walkPreorder(member, (node) => {
|
|
if (!ok) return "stop";
|
|
// A nested class or non-arrow function rebinds `this`: any private
|
|
// name inside it disqualifies.
|
|
if (
|
|
node !== member &&
|
|
(ts.isClassDeclaration(node) ||
|
|
ts.isClassExpression(node) ||
|
|
ts.isFunctionExpression(node) ||
|
|
ts.isFunctionDeclaration(node))
|
|
) {
|
|
ts.walkPreorder(node, (inner) => {
|
|
if (ts.isPrivateIdentifier(inner)) ok = false;
|
|
return ok ? undefined : "stop";
|
|
});
|
|
return "skip";
|
|
}
|
|
if (ts.isPrivateIdentifier(node)) {
|
|
const parent = node.parent;
|
|
const onThis =
|
|
parent === member ||
|
|
(ts.isPropertyAccessExpression(parent) &&
|
|
parent.name === node &&
|
|
parent.expression.kind === ts.SyntaxKind.ThisKeyword) ||
|
|
(ts.isBinaryExpression(parent) &&
|
|
parent.left === node &&
|
|
parent.operatorToken.kind === ts.SyntaxKind.InKeyword &&
|
|
parent.right.kind === ts.SyntaxKind.ThisKeyword);
|
|
if (!onThis) ok = false;
|
|
}
|
|
return undefined;
|
|
});
|
|
if (!ok) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/** 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.nodes.some((member) => member.name && ts.isPrivateIdentifier(member.name)) &&
|
|
!localPrivateFieldsAgree(expr)
|
|
) {
|
|
lowerer.unsupported("SC1090", expr, "function-local classes with private brands");
|
|
}
|
|
if (
|
|
local &&
|
|
(expr.typeParameters?.nodes.length ||
|
|
decoratorNodesOf(expr).length ||
|
|
expr.members.nodes.some(
|
|
(member) =>
|
|
decoratorNodesOf(member).length ||
|
|
(!isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
|
|
(ts.isClassStaticBlockDeclaration(member) ||
|
|
(!ts.isMethodDeclaration(member) &&
|
|
ts
|
|
.getModifiers(member)
|
|
?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword)))) ||
|
|
(ts.isMethodDeclaration(member) && member.typeParameters?.nodes.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?.nodes.find(
|
|
(clause) => clause.token === ts.SyntaxKind.ExtendsKeyword,
|
|
)?.types?.nodes[0]?.expression;
|
|
if (
|
|
heritage &&
|
|
((ts.isCallExpression(heritage) && !builtinRequireBaseOf(lowerer, 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?.nodes.find(
|
|
(clause) => clause.token === ts.SyntaxKind.ExtendsKeyword,
|
|
)?.types?.nodes[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 (
|
|
!local &&
|
|
(info.callableBase ||
|
|
info.def.baseValueGlobal ||
|
|
info.runtimeStatics ||
|
|
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.nodes.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)",
|
|
);
|
|
}
|
|
}
|
|
return info;
|
|
}
|
|
|
|
/** True when a named class expression's body refers to the class value by
|
|
* its own name (`class Box { static of() { return new Box(); } }`, a
|
|
* shorthand property `{ Box }` included); type annotations naming it
|
|
* (`again(): Box`) do not count. */
|
|
function classExpressionNamesItself(lowerer: Lowerer, expr: ts.ClassExpression): boolean {
|
|
const name = expr.name;
|
|
if (!name) return false;
|
|
const symbol = lowerer.checker.getSymbolAtLocation(name);
|
|
if (!symbol) return false;
|
|
let found = false;
|
|
for (const member of expr.members.nodes) {
|
|
ts.walkPreorder(member, (n) => {
|
|
if (found || ts.isTypeNode(n)) return "skip";
|
|
if (ts.isIdentifier(n) && n.text === name.text && identifierReadSymbol(lowerer, n) === symbol)
|
|
found = true;
|
|
return undefined;
|
|
});
|
|
if (found) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** `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.
|
|
*
|
|
* `binding` is the local of a function-local class declaration's name,
|
|
* declared without a value; the class value assigns it before the class's
|
|
* static members are defined (see initializeRuntimeStatics). */
|
|
export function lowerClassExpression(
|
|
lowerer: Lowerer,
|
|
expr: ts.ClassLikeDeclaration,
|
|
binding?: IrLocal,
|
|
): IrExpr {
|
|
if (
|
|
decoratorNodesOf(expr).length > 0 ||
|
|
expr.members.nodes.some((m) => decoratorNodesOf(m).length > 0)
|
|
) {
|
|
if (!isJsSourceFile(ts.getSourceFileOfNode(expr)) && 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);
|
|
// A function-local class expression's own name is a binding of its body:
|
|
// declared before the members lower, so their references to the name
|
|
// resolve to it. The class value captures it before it is assigned, as
|
|
// it captures a class declaration's binding.
|
|
if (
|
|
!binding &&
|
|
info.localClass &&
|
|
ts.isClassExpression(expr) &&
|
|
expr.name &&
|
|
classExpressionNamesItself(lowerer, expr)
|
|
) {
|
|
const own = lowerer.declareLocal(
|
|
expr.name,
|
|
expr.name.text,
|
|
{ kind: "classval", className: info.def.name },
|
|
true,
|
|
);
|
|
const value = lowerClassValue(lowerer, expr, info, own);
|
|
return {
|
|
kind: "seqExpr",
|
|
stmts: [{ kind: "varDecl", localId: own.id, init: null, loc: value.loc }],
|
|
result: value,
|
|
type: value.type,
|
|
loc: value.loc,
|
|
};
|
|
}
|
|
return lowerClassValue(lowerer, expr, info, binding);
|
|
}
|
|
|
|
function lowerClassValue(
|
|
lowerer: Lowerer,
|
|
expr: ts.ClassLikeDeclaration,
|
|
info: ClassInfo,
|
|
binding: IrLocal | undefined,
|
|
): IrExpr {
|
|
if (
|
|
!info.localClass &&
|
|
!info.expressionInitializationEmitted &&
|
|
(info.callableBase ||
|
|
info.def.baseValueGlobal ||
|
|
info.runtimeStatics ||
|
|
info.staticFields.length > 0 ||
|
|
(info.staticBlocks?.length ?? 0) > 0)
|
|
) {
|
|
info.expressionInitializationEmitted = true;
|
|
lowerer.pendingClassExprInits.push(...lowerer.lowerStaticFieldInits(info));
|
|
}
|
|
if (info.localClass && !info.localClass.ready) {
|
|
const context = newFnCtx(true, null, null, VOID);
|
|
info.localClass.context = context;
|
|
lowerer.fnStack.push(context);
|
|
const diagsBefore = lowerer.diags.length;
|
|
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;
|
|
// A retry of the enclosing statement reuses these bodies.
|
|
for (const d of lowerer.diags.slice(diagsBefore)) lowerer.loweredOnceDiags.add(d);
|
|
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;
|
|
const built: IrExpr = {
|
|
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,
|
|
};
|
|
return initializeRuntimeStatics(lowerer, info, built, binding);
|
|
}
|
|
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;
|
|
const built: IrExpr = {
|
|
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, built, binding);
|
|
}
|
|
if (!info.localClass) return value;
|
|
const classValue = initializeRuntimeStatics(
|
|
lowerer,
|
|
info,
|
|
value.kind === "classRef" ? { ...value, captures: info.localClass.captureSources } : value,
|
|
binding,
|
|
);
|
|
// The base resolves statically here, but Node still evaluates the
|
|
// heritage expression each time the class definition runs, first.
|
|
const heritage = heritageWithEffects(expr);
|
|
if (heritage === undefined) return classValue;
|
|
return {
|
|
kind: "seqExpr",
|
|
stmts: [lowerer.lowerExprStatement(heritage)],
|
|
result: classValue,
|
|
type: classValue.type,
|
|
loc: classValue.loc,
|
|
};
|
|
}
|
|
|
|
/** The `extends` expression of `decl` when evaluating it can have effects
|
|
* (a call, construction, assignment, update, delete, await or yield
|
|
* outside nested functions). */
|
|
function heritageWithEffects(decl: ts.ClassLikeDeclaration): ts.Expression | undefined {
|
|
const heritage = decl.heritageClauses?.nodes.find(
|
|
(clause) => clause.token === ts.SyntaxKind.ExtendsKeyword,
|
|
)?.types?.nodes[0]?.expression;
|
|
if (heritage === undefined) return undefined;
|
|
const effects = (node: ts.Node): boolean => {
|
|
if (ts.isFunctionLike(node) || ts.isClassDeclaration(node) || ts.isClassExpression(node))
|
|
return false;
|
|
if (
|
|
ts.isCallExpression(node) ||
|
|
ts.isNewExpression(node) ||
|
|
ts.isTaggedTemplateExpression(node) ||
|
|
ts.isDeleteExpression(node) ||
|
|
ts.isAwaitExpression(node) ||
|
|
ts.isYieldExpression(node) ||
|
|
(ts.isPrefixUnaryExpression(node) &&
|
|
(node.operator === ts.SyntaxKind.PlusPlusToken ||
|
|
node.operator === ts.SyntaxKind.MinusMinusToken)) ||
|
|
ts.isPostfixUnaryExpression(node) ||
|
|
(ts.isBinaryExpression(node) &&
|
|
node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
|
node.operatorToken.kind <= ts.SyntaxKind.LastAssignment)
|
|
)
|
|
return true;
|
|
return ts.forEachChild(node, (child) => effects(child) || undefined) ?? false;
|
|
};
|
|
return effects(heritage) ? heritage : undefined;
|
|
}
|
|
|
|
/** 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 {
|
|
if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) {
|
|
const member = nsMemberIdentOf(lowerer, expr);
|
|
if (member) return exactClassOfReceiver(lowerer, member);
|
|
}
|
|
// 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 = symbol.valueDeclaration;
|
|
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 = resolved.declarations;
|
|
// 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 = ts.getSourceFileOfNode(assign);
|
|
// `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 = ts.getTextOfNode(lhs).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.nodes) {
|
|
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);
|
|
}
|
|
// Namespace exports are immutable bindings to the same declaration as
|
|
// named imports. Resolve them before asking for a constructor-value ABI:
|
|
// a generic family has statics but no single constructor signature.
|
|
if (ts.isPropertyAccessExpression(access.expression) && !access.expression.questionDotToken) {
|
|
const member = nsMemberIdentOf(lowerer, access.expression);
|
|
const symbol = member ? lowerer.resolveValueSymbol(member) : null;
|
|
const info = symbol ? lowerer.classBySymbol.get(symbol) : undefined;
|
|
if (info && info.classDecorators?.valueGlobalId === undefined) {
|
|
if (symbol) fenceEarlyNsMemberRef(lowerer, access.expression, symbol);
|
|
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(ts.getSourceFileOfNode(access), lowerer.program)
|
|
) {
|
|
let receiverType = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
|
if (receiverType?.kind !== "classval" && isJsSourceFile(ts.getSourceFileOfNode(call))) {
|
|
const receiver = tryLowerExpression(lowerer, access.expression);
|
|
if (receiver?.type.kind === "classval") receiverType = receiver.type;
|
|
}
|
|
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(ts.getSourceFileOfNode(access));
|
|
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);
|
|
const runtimeStatic = directRuntimeStaticCall(lowerer, call, access, info, throughValue);
|
|
if (runtimeStatic) return runtimeStatic;
|
|
// Date's statics through a class declared `extends Date`
|
|
// (`Moment.UTC(...)`), which inherits them.
|
|
if (dateRootOf(info) && !findStaticOn(lowerer, info, access.name.text)) {
|
|
const date = lowerDateStaticCall(lowerer, call, access.name.text);
|
|
if (date) return date;
|
|
}
|
|
if (
|
|
(hasRuntimeStatics(info) && !findStaticOn(lowerer, info, access.name.text)?.field) ||
|
|
(info.callableBase &&
|
|
!findStaticOn(lowerer, info, access.name.text) &&
|
|
!findGenericStaticOn(lowerer, info, access.name.text))
|
|
)
|
|
return {
|
|
kind: "dynInvoke",
|
|
recv: lowerer.lowerExprExpecting(access.expression, DYN),
|
|
method: access.name.text,
|
|
calleeName: ts.getTextOfNode(access),
|
|
args: call.arguments.nodes.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 classConstructorCall(lowerer, call, access, info);
|
|
if (throughValue && staticShadowBelow(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.nodes, instance.params, loc, call);
|
|
return { kind: "call", callee: instance.name, args, type: instance.returnType, loc };
|
|
}
|
|
if (throughValue && staticShadowBelow(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.nodes, 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.nodes.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,
|
|
receiver: lowerer.lowerExprExpecting(access.expression, DYN),
|
|
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.nodes, 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.nodes, found.method.params, loc, call);
|
|
return { kind: "call", callee: fnName, args, type: found.method.ret, loc };
|
|
}
|
|
|
|
/** `C.call(thisArg, ...)` / `C.apply(thisArg, args)` on a program class that
|
|
* declares no such static: Function.prototype.call/apply invoke the class
|
|
* constructor without `new`, which throws Node's TypeError "Class
|
|
* constructor C cannot be invoked without 'new'" after the arguments are
|
|
* evaluated (and, for apply, after the argument list is read). The boxed
|
|
* class value's call path throws exactly that. Neither expando statics nor
|
|
* writes through untyped values can give a class an own `call` or `apply`.
|
|
* A receiver that may hold a subclass at run time (a class value, `this` in
|
|
* a static method) needs every subclass to lack such a static too. The
|
|
* receiver must be one the caller does not evaluate again (a name, `this`,
|
|
* or a namespace member). Null otherwise. */
|
|
function classConstructorCall(
|
|
lowerer: Lowerer,
|
|
call: ts.CallExpression,
|
|
access: ts.PropertyAccessExpression,
|
|
info: ClassInfo,
|
|
): IrExpr | null {
|
|
const name = access.name.text;
|
|
const receiver = access.expression;
|
|
if (
|
|
(name !== "call" && name !== "apply") ||
|
|
!info.decl ||
|
|
info.def.runtime ||
|
|
info.builtinError ||
|
|
info.builtinEmitter ||
|
|
info.builtinStream ||
|
|
info.builtinEventTarget ||
|
|
info.runtimeStatics?.length ||
|
|
declaresStaticAbove(info, name) ||
|
|
(exactClassOfReceiver(lowerer, receiver) !== info &&
|
|
info.subclasses.some((subclass) => declaresStaticBelow(subclass, name))) ||
|
|
call.arguments.nodes.some(ts.isSpreadElement) ||
|
|
!(
|
|
ts.isIdentifier(receiver) ||
|
|
receiver.kind === ts.SyntaxKind.ThisKeyword ||
|
|
(ts.isPropertyAccessExpression(receiver) && nsMemberIdentOf(lowerer, receiver) !== null)
|
|
)
|
|
)
|
|
return null;
|
|
return {
|
|
kind: "dynInvoke",
|
|
recv: lowerer.lowerExprExpecting(receiver, DYN),
|
|
method: name,
|
|
calleeName: ts.getTextOfNode(access),
|
|
args: call.arguments.nodes.map((argument) => lowerer.lowerExprExpecting(argument, DYN)),
|
|
type: DYN,
|
|
loc: locOf(call),
|
|
};
|
|
}
|
|
|
|
/** Whether `info`'s declaration has a static member that may be named `name`
|
|
* (a method, field or accessor of that name, or a computed name), or `info`
|
|
* has no declaration to tell. */
|
|
function declaresStatic(info: ClassInfo, name: string): boolean {
|
|
if (!info.decl) return true;
|
|
return info.decl.members.nodes.some(
|
|
(member) =>
|
|
member.name !== undefined &&
|
|
ts.canHaveModifiers(member) &&
|
|
ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) ===
|
|
true &&
|
|
(ts.isComputedPropertyName(member.name) ||
|
|
((ts.isIdentifier(member.name) || ts.isStringLiteral(member.name)) &&
|
|
member.name.text === name)),
|
|
);
|
|
}
|
|
|
|
/** declaresStatic over `info` and its bases, which it inherits statics from. */
|
|
function declaresStaticAbove(info: ClassInfo, name: string): boolean {
|
|
for (let c: ClassInfo | null = info; c; c = c.base) if (declaresStatic(c, name)) return true;
|
|
return false;
|
|
}
|
|
|
|
/** declaresStatic over `info` and its subclasses, or runtime statics, which a
|
|
* function-local JavaScript class defines at run time. */
|
|
function declaresStaticBelow(info: ClassInfo, name: string): boolean {
|
|
return (
|
|
declaresStatic(info, name) ||
|
|
!!info.runtimeStatics?.length ||
|
|
info.subclasses.some((subclass) => declaresStaticBelow(subclass, name))
|
|
);
|
|
}
|
|
|
|
/** `C.m(...)` where `m` is a static method that a function-local
|
|
* JavaScript class defines at runtime (a factory's `class extends base`),
|
|
* inherited by `C` through its statically known base chain. When the body
|
|
* captures nothing from the factory's scope, every evaluation of the class
|
|
* defines the same method, so the call lowers directly with `this` bound
|
|
* to `C`, as a free function given `C` would. A receiver-dependent result
|
|
* (a class built from `this`) then keeps a static base. Null when another
|
|
* declaration of `m` answers first, the program assigns that member, or
|
|
* the method form has no direct lowering. */
|
|
function directRuntimeStaticCall(
|
|
lowerer: Lowerer,
|
|
call: ts.CallExpression,
|
|
access: ts.PropertyAccessExpression,
|
|
info: ClassInfo,
|
|
throughValue: boolean,
|
|
): IrExpr | null {
|
|
const name = access.name.text;
|
|
if (throughValue || name.startsWith("#")) return null;
|
|
let declarer: ClassInfo | null = null;
|
|
let member: ts.MethodDeclaration | null = null;
|
|
for (let c: ClassInfo | null = info; c; c = c.base) {
|
|
if (
|
|
c.staticFields.some((field) => field.name === name) ||
|
|
c.staticMethods?.has(name) === true ||
|
|
c.genericStatics?.has(name) === true
|
|
)
|
|
return null;
|
|
const runtime = c.runtimeStatics?.find(
|
|
(element) =>
|
|
(element as { name?: ts.PropertyName }).name !== undefined &&
|
|
ts.isIdentifier((element as { name: ts.PropertyName }).name) &&
|
|
(element as { name: ts.Identifier }).name.text === name,
|
|
);
|
|
if (runtime) {
|
|
if (!ts.isMethodDeclaration(runtime) || !runtime.body) return null;
|
|
declarer = c;
|
|
member = runtime;
|
|
break;
|
|
}
|
|
}
|
|
if (!declarer || !member || !declarer.decl) return null;
|
|
if (member.asteriskToken || member.typeParameters?.nodes) return null;
|
|
if (member.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword))
|
|
return null;
|
|
if (hasLexicalSuper(member) || capturesEnclosingScope(lowerer, member, declarer.decl))
|
|
return null;
|
|
if (memberAssigned(lowerer, member, name) || bodyReadsArguments(member)) return null;
|
|
const implicit =
|
|
member.parameters.nodes.length === 0 ? null : implicitAnyParamSymbolsOf(lowerer, member, true);
|
|
if (member.parameters.nodes.length > 0 && !implicit) return null;
|
|
let calls = declarer.runtimeStaticCalls;
|
|
if (!calls) {
|
|
calls = new Map();
|
|
declarer.runtimeStaticCalls = calls;
|
|
}
|
|
const method: GenericFnInfo = calls.get(name) ?? {
|
|
decl: member,
|
|
baseName: name,
|
|
qualifiedName: `%${declarer.def.name}.runtimeStatic:${name}`,
|
|
typeParams: [],
|
|
instances: new Map(),
|
|
member: { cls: declarer, kind: "static" },
|
|
...(implicit ? { implicitParams: implicit } : {}),
|
|
};
|
|
calls.set(name, method);
|
|
// A direct lowering the method form does not support keeps the dynamic
|
|
// call: its refusal is not this program's error.
|
|
const sink: ScrDiagnostic[] = [];
|
|
const previousSink = lowerer.diagSink;
|
|
lowerer.diagSink = sink;
|
|
let lowered: IrExpr;
|
|
try {
|
|
const receiver = staticMethodForReceiver(lowerer, access, method, info, false);
|
|
const instance = implicit
|
|
? implicitCallInstance(lowerer, call, receiver)
|
|
: genericCallInstance(lowerer, call, receiver);
|
|
lowered = {
|
|
kind: "call",
|
|
callee: instance.name,
|
|
args: lowerer.completeArgs(call.arguments.nodes, instance.params, locOf(call), call),
|
|
type: instance.returnType,
|
|
loc: locOf(call),
|
|
};
|
|
} catch (error) {
|
|
if (error instanceof PoisonError) return null;
|
|
throw error;
|
|
} finally {
|
|
lowerer.diagSink = previousSink;
|
|
}
|
|
return sink.length === 0 ? lowered : null;
|
|
}
|
|
|
|
/** `super` inside the method (arrows included) names the defining
|
|
* class's base, which a receiver specialization does not track. */
|
|
function hasLexicalSuper(root: ts.Node): boolean {
|
|
let found = false;
|
|
const visit = (node: ts.Node): void => {
|
|
if (node.kind === ts.SyntaxKind.SuperKeyword) found = true;
|
|
if (
|
|
(ts.isFunctionLike(node) && !ts.isArrowFunction(node)) ||
|
|
ts.isClassDeclaration(node) ||
|
|
ts.isClassExpression(node)
|
|
)
|
|
return;
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
ts.forEachChild(root, visit);
|
|
return found;
|
|
}
|
|
|
|
/** The method reads a binding declared in a function that encloses its
|
|
* class: each evaluation of the class can see a different value. */
|
|
function capturesEnclosingScope(
|
|
lowerer: Lowerer,
|
|
member: ts.MethodDeclaration,
|
|
classDecl: ts.ClassLikeDeclaration,
|
|
): boolean {
|
|
let scope: ts.Node | undefined = classDecl.parent;
|
|
while (scope && !ts.isFunctionLike(scope) && !ts.isSourceFile(scope)) scope = scope.parent;
|
|
if (!scope || ts.isSourceFile(scope)) return false;
|
|
const inside = (node: ts.Node, container: ts.Node): boolean => {
|
|
for (let n: ts.Node | undefined = node; n; n = n.parent) if (n === container) return true;
|
|
return false;
|
|
};
|
|
let captured = false;
|
|
const visit = (node: ts.Node): void => {
|
|
if (captured) return;
|
|
if (ts.isIdentifier(node)) {
|
|
const symbol = lowerer.checker.getSymbolAtLocation(node);
|
|
const decl = symbol ? symbol.valueDeclaration : undefined;
|
|
// The enclosing function's own name is the same binding for every
|
|
// evaluation.
|
|
if (decl && decl !== scope && inside(decl, scope) && !inside(decl, member)) captured = true;
|
|
return;
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
ts.forEachChild(member, visit);
|
|
return captured;
|
|
}
|
|
|
|
/** Some code assigns or deletes a member of this name, or defines it by
|
|
* reflection: the member a runtime lookup finds can then differ. */
|
|
function memberAssigned(lowerer: Lowerer, member: ts.MethodDeclaration, name: string): boolean {
|
|
let found = false;
|
|
for (const sf of lowerer.program.getImplementationSourceFiles()) {
|
|
if (found) continue;
|
|
if (!sf.text.includes(name)) continue;
|
|
ts.walkPreorder(sf, (node) => {
|
|
if (found) return "skip";
|
|
if (ts.isPropertyAccessExpression(node) && node.name.text === name) {
|
|
const parent = node.parent;
|
|
if (
|
|
parent &&
|
|
((ts.isBinaryExpression(parent) &&
|
|
parent.left === node &&
|
|
parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
|
parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment) ||
|
|
ts.isDeleteExpression(parent))
|
|
)
|
|
found = true;
|
|
}
|
|
if (ts.isStringLiteral(node) && node.text === name && node !== member.name) {
|
|
const parent = node.parent;
|
|
if (parent && (ts.isCallExpression(parent) || ts.isElementAccessExpression(parent)))
|
|
found = true;
|
|
}
|
|
return undefined;
|
|
});
|
|
}
|
|
return found;
|
|
}
|
|
|
|
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);
|
|
// A runtime-provided class value answers `.name` from its class object;
|
|
// its other statics (`Error.stackTraceLimit`) keep their own lowerings.
|
|
if (info?.def.runtime && member !== "name") return null;
|
|
if (
|
|
info &&
|
|
((hasRuntimeStatics(info) && !findStaticOn(lowerer, info, member)?.field) ||
|
|
(info.callableBase && !findStaticOn(lowerer, info, member)))
|
|
)
|
|
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(ts.getSourceFileOfNode(expr), 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(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) {
|
|
const value: IrExpr = {
|
|
kind: "varRef",
|
|
localId: found.field.globalId,
|
|
type: found.field.type,
|
|
loc,
|
|
};
|
|
return found.field.runtimeOptional ? value : lowerer.maybeNarrow(value, 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.nodes) {
|
|
if (!ts.isIdentifier(param.name)) lowerer.unsupported("SC1031", param);
|
|
}
|
|
return {
|
|
shapes: lowerer.paramShapes(member.parameters.nodes),
|
|
ret: lowerer.declaredReturnType(member, member.name),
|
|
};
|
|
}
|
|
|
|
/** A nominal return subtype uses the same owned pointer as the inherited
|
|
* return slot. The current class is still being collected, so consult its
|
|
* already collected base chain before the complete class graph is available. */
|
|
/** An inherited ASYNC method that an override now dispatches against: its
|
|
* slot becomes a plain function returning the body's promise, and the
|
|
* async body lowers behind it as `%C.m$own` (the adapter path with an
|
|
* identity conversion), so a virtual call never enters an async body
|
|
* without its spawn wrapper. */
|
|
function thunkAsyncSlot(
|
|
overridden: { declarer: ClassInfo; sig: ClassMethodSignature },
|
|
mName: string,
|
|
): void {
|
|
if (overridden.sig.async !== true) return;
|
|
const declarer = overridden.declarer;
|
|
// The slot answers the body's promise: an untyped async method's `dyn`
|
|
// result is the promise's payload, so the slot's ABI is that promise. An
|
|
// async generator method's slot keeps its generator object.
|
|
const ret: IrType =
|
|
overridden.sig.gen || overridden.sig.ret.kind === "promise"
|
|
? overridden.sig.ret
|
|
: { kind: "promise", inner: overridden.sig.ret };
|
|
declarer.returnAdapters ??= new Map();
|
|
if (!declarer.returnAdapters.has(mName)) {
|
|
declarer.returnAdapters.set(mName, {
|
|
params: [...overridden.sig.params],
|
|
ret,
|
|
async: true,
|
|
});
|
|
}
|
|
overridden.sig.ret = ret;
|
|
delete overridden.sig.async;
|
|
}
|
|
|
|
function overrideReturnAbi(
|
|
lowerer: Lowerer,
|
|
declared: IrType,
|
|
inherited: IrType,
|
|
className: string,
|
|
base: ClassInfo | null,
|
|
): IrType {
|
|
// A return narrowed to part of an inherited union (`Meta` for
|
|
// `Meta | undefined`) keeps the union ABI; returns convert into it.
|
|
if (inherited.kind === "union" && !typeEquals(declared, inherited)) {
|
|
const arms = lowerer.unions.get(inherited.unionId)?.arms ?? [];
|
|
const fits = (type: IrType): boolean =>
|
|
arms.some(
|
|
(arm) =>
|
|
typeEquals(arm, type) ||
|
|
(arm.kind === "object" &&
|
|
type.kind === "object" &&
|
|
lowerer.isSubclassOf(type.className, arm.className)),
|
|
);
|
|
const parts =
|
|
declared.kind === "union" ? (lowerer.unions.get(declared.unionId)?.arms ?? []) : [declared];
|
|
return parts.length > 0 && parts.every(fits) ? inherited : declared;
|
|
}
|
|
if (declared.kind !== "object" || inherited.kind !== "object") return declared;
|
|
if (
|
|
lowerer.isSubclassOf(declared.className, inherited.className) ||
|
|
(declared.className === className &&
|
|
base !== null &&
|
|
(base.def.name === inherited.className ||
|
|
lowerer.isSubclassOf(base.def.name, inherited.className)))
|
|
)
|
|
return inherited;
|
|
return declared;
|
|
}
|
|
|
|
/** 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 {
|
|
if (sub === sup) return false;
|
|
for (let c = lowerer.classes.get(sub); c; c = c.base ?? undefined) {
|
|
if (c.def.name === sup && c.def.name !== sub) return true;
|
|
for (let family = c.genericInstance?.family; family; family = family.base ?? undefined) {
|
|
if (family.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 {
|
|
if (info.decl !== null && localOverrideBelow(lowerer, info.decl, name)) return true;
|
|
return info.subclasses.some((s) => {
|
|
const m = s.methods.get(name);
|
|
return (m !== undefined && m.abstract !== true) || lowerer.overrideBelow(s, name);
|
|
});
|
|
}
|
|
|
|
/** The class node an `extends` clause names: a class declaration, or the
|
|
* class expression a variable is initialized with. */
|
|
function extendedClassNode(lowerer: Lowerer, decl: ts.ClassLikeDeclaration): ts.Node | null {
|
|
const heritage = decl.heritageClauses?.nodes.find(
|
|
(clause) => clause.token === ts.SyntaxKind.ExtendsKeyword,
|
|
)?.types?.nodes[0]?.expression;
|
|
if (!heritage || !ts.isIdentifier(heritage)) return null;
|
|
let symbol = lowerer.checker.getSymbolAtLocation(heritage);
|
|
if (symbol && symbol.flags & ts.SymbolFlags.Alias)
|
|
symbol = lowerer.checker.getAliasedSymbol(symbol);
|
|
const target = symbol?.valueDeclaration;
|
|
if (!target) return null;
|
|
if (ts.isClassDeclaration(target) || ts.isClassExpression(target)) return target;
|
|
if (ts.isVariableDeclaration(target) && target.initializer) {
|
|
let init: ts.Expression = target.initializer;
|
|
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
|
if (ts.isClassExpression(init)) return init;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** The function-local classes that extend the class node `base`. Function
|
|
* bodies lower on demand, after code that may already call methods of a
|
|
* class their local classes extend, so such a subclass is not yet in its
|
|
* base's `subclasses` when those calls lower. */
|
|
function localSubclassesOf(lowerer: Lowerer, base: ts.Node): readonly ts.ClassLikeDeclaration[] {
|
|
let index = lowerer.localSubclassIndex;
|
|
if (index === null) {
|
|
index = new Map();
|
|
const files = lowerer.moduleOrder.length > 0 ? lowerer.moduleOrder : [lowerer.entry];
|
|
for (const sf of files) {
|
|
if (!sf.text.includes("extends")) continue;
|
|
ts.walkPreorder(sf, (node) => {
|
|
if (
|
|
(ts.isClassDeclaration(node) || ts.isClassExpression(node)) &&
|
|
node.heritageClauses !== undefined &&
|
|
functionLocalClass(node)
|
|
) {
|
|
const extended = extendedClassNode(lowerer, node);
|
|
if (extended) {
|
|
const list = index!.get(extended);
|
|
if (list) list.push(node);
|
|
else index!.set(extended, [node]);
|
|
}
|
|
}
|
|
return undefined;
|
|
});
|
|
}
|
|
lowerer.localSubclassIndex = index;
|
|
}
|
|
return index.get(base) ?? [];
|
|
}
|
|
|
|
/** True when a function-local class below the class node `base` declares
|
|
* a concrete instance method or accessor `name` (accessors are keyed
|
|
* `get:x` and `set:x`). A call on `base`'s class then dispatches on the
|
|
* receiver; devirtualization turns it back into a direct call when no such
|
|
* override is reachable. */
|
|
function localOverrideBelow(
|
|
lowerer: Lowerer,
|
|
base: ts.Node,
|
|
name: string,
|
|
visited?: Set<ts.Node>,
|
|
): boolean {
|
|
const subclasses = localSubclassesOf(lowerer, base);
|
|
if (subclasses.length === 0) return false;
|
|
visited ??= new Set();
|
|
if (visited.has(base)) return false;
|
|
visited.add(base);
|
|
const accessor = name.startsWith("get:") ? "get" : name.startsWith("set:") ? "set" : null;
|
|
const member = accessor ? name.slice(4) : name;
|
|
return subclasses.some(
|
|
(sub) =>
|
|
sub.members.nodes.some(
|
|
(m) =>
|
|
(accessor === null
|
|
? ts.isMethodDeclaration(m)
|
|
: accessor === "get"
|
|
? ts.isGetAccessorDeclaration(m)
|
|
: ts.isSetAccessorDeclaration(m)) &&
|
|
m.body !== undefined &&
|
|
m.name !== undefined &&
|
|
(ts.isIdentifier(m.name) || ts.isStringLiteral(m.name)) &&
|
|
m.name.text === member &&
|
|
!ts.getModifiers(m)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword),
|
|
) || localOverrideBelow(lowerer, sub, name, visited),
|
|
);
|
|
}
|
|
|
|
/** 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(info: ClassInfo, name: string): boolean {
|
|
return info.subclasses.some(
|
|
(s) => s.genericMethods?.has(name) === true || genericOverrideBelow(s, name),
|
|
);
|
|
}
|
|
|
|
/** Discover inherited data writes before any base storage ABI is chosen.
|
|
* The checker does not widen a base field for assignments in JS subclasses. */
|
|
export function collectInheritedJsFieldWrites(
|
|
lowerer: Lowerer,
|
|
files: readonly ts.SourceFile[],
|
|
): void {
|
|
for (const file of files) {
|
|
if (!isJsSourceFile(file)) continue;
|
|
ts.walkPreorder(file, (node) => {
|
|
if (
|
|
(!ts.isClassDeclaration(node) && !ts.isClassExpression(node)) ||
|
|
!node.heritageClauses?.nodes.length
|
|
)
|
|
return;
|
|
const writes = new Map<string, ts.Expression[]>();
|
|
const addWrite = (name: string, expression: ts.Expression): void => {
|
|
const existing = writes.get(name);
|
|
if (existing) existing.push(expression);
|
|
else writes.set(name, [expression]);
|
|
};
|
|
const scan = (part: ts.Node): void => {
|
|
if (
|
|
ts.isClassDeclaration(part) ||
|
|
ts.isClassExpression(part) ||
|
|
(ts.isFunctionLike(part) && !ts.isArrowFunction(part))
|
|
)
|
|
return;
|
|
if (ts.isBinaryExpression(part) && part.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
const left = part.left;
|
|
if (
|
|
ts.isPropertyAccessExpression(left) &&
|
|
left.expression.kind === ts.SyntaxKind.ThisKeyword
|
|
)
|
|
addWrite(left.name.text, part.right);
|
|
else if (
|
|
ts.isElementAccessExpression(left) &&
|
|
left.expression.kind === ts.SyntaxKind.ThisKeyword
|
|
) {
|
|
const name = lowerer.foldedStringKeyOf(left.argumentExpression);
|
|
if (name !== null) addWrite(name, part.right);
|
|
}
|
|
}
|
|
ts.forEachChild(part, scan);
|
|
};
|
|
for (const member of node.members.nodes) {
|
|
if (
|
|
member.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword)
|
|
)
|
|
continue;
|
|
if (ts.isPropertyDeclaration(member) && member.initializer) {
|
|
const name = ts.isPrivateIdentifier(member.name)
|
|
? null
|
|
: classMemberNameOf(lowerer, member.name);
|
|
if (name !== null) addWrite(name, member.initializer);
|
|
scan(member.initializer);
|
|
}
|
|
if (
|
|
ts.isConstructorDeclaration(member) ||
|
|
ts.isMethodDeclaration(member) ||
|
|
ts.isAccessor(member)
|
|
) {
|
|
if (member.body) scan(member.body);
|
|
}
|
|
}
|
|
if (writes.size === 0) return;
|
|
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?.isClassOrInterface() || seen.has(target)) return;
|
|
seen.add(target);
|
|
for (const base of lowerer.checker.getBaseTypes(target)) {
|
|
for (const [name, expressions] of writes) {
|
|
const property = lowerer.checker.getPropertyOfType(base, name);
|
|
for (const declaration of property ? property.declarations : []) {
|
|
if (ts.isMethodDeclaration(declaration) || ts.isAccessor(declaration)) continue;
|
|
let owner: ts.Node | undefined = declaration.parent;
|
|
while (owner && !ts.isClassDeclaration(owner) && !ts.isClassExpression(owner))
|
|
owner = owner.parent;
|
|
if (
|
|
!owner ||
|
|
(!ts.isClassDeclaration(owner) && !ts.isClassExpression(owner)) ||
|
|
!isJsSourceFile(ts.getSourceFileOfNode(owner))
|
|
)
|
|
continue;
|
|
let fields = lowerer.inheritedJsFieldWrites.get(owner);
|
|
if (!fields) lowerer.inheritedJsFieldWrites.set(owner, (fields = new Map()));
|
|
const existing = fields.get(name);
|
|
if (existing) existing.push(...expressions);
|
|
else fields.set(name, [...expressions]);
|
|
}
|
|
}
|
|
markBases(base);
|
|
}
|
|
};
|
|
if (instance) markBases(instance);
|
|
});
|
|
}
|
|
}
|
|
|
|
/** 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 {
|
|
const families: [ts.MethodDeclaration, ts.MethodDeclaration][] = [];
|
|
lowerer.speculativeTypeQueries(() => collectClassMethodMutations(lowerer, files));
|
|
const visit = (node: ts.Node): void => {
|
|
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
|
|
const accessors = node.members.nodes.filter(
|
|
(member): member is ts.GetAccessorDeclaration | ts.SetAccessorDeclaration =>
|
|
(ts.isGetAccessorDeclaration(member) || ts.isSetAccessorDeclaration(member)) &&
|
|
ts.isIdentifier(member.name) &&
|
|
!member.modifiers?.nodes.some(
|
|
(modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword,
|
|
),
|
|
);
|
|
if (
|
|
node.heritageClauses?.nodes.length &&
|
|
accessors.length &&
|
|
isJsSourceFile(ts.getSourceFileOfNode(node))
|
|
) {
|
|
const symbol = lowerer.typeOf(node).getSymbol();
|
|
const instance = symbol ? lowerer.checker.getDeclaredTypeOfSymbol(symbol) : null;
|
|
const seen = new Set<ts.Type>();
|
|
const markAccessors = (type: ts.Type): void => {
|
|
const target = type.isTypeReference() ? type.getTarget() : type;
|
|
if (!target?.isClassOrInterface() || seen.has(target)) return;
|
|
seen.add(target);
|
|
for (const base of lowerer.checker.getBaseTypes(target)) {
|
|
for (const member of accessors) {
|
|
if (!ts.isIdentifier(member.name)) continue;
|
|
const property = lowerer.checker.getPropertyOfType(base, member.name.text);
|
|
for (const declaration of property ? property.declarations : []) {
|
|
if (
|
|
(ts.isGetAccessorDeclaration(declaration) ||
|
|
ts.isSetAccessorDeclaration(declaration)) &&
|
|
isJsSourceFile(ts.getSourceFileOfNode(declaration))
|
|
) {
|
|
lowerer.checkedVirtualJsAccessors.add(declaration);
|
|
lowerer.checkedVirtualJsAccessors.add(member);
|
|
for (const twin of node.members.nodes)
|
|
if (
|
|
(ts.isGetAccessorDeclaration(twin) || ts.isSetAccessorDeclaration(twin)) &&
|
|
ts.isIdentifier(twin.name) &&
|
|
twin.name.text === member.name.text
|
|
)
|
|
lowerer.checkedVirtualJsAccessors.add(twin);
|
|
}
|
|
}
|
|
}
|
|
markAccessors(base);
|
|
}
|
|
};
|
|
if (instance) markAccessors(instance);
|
|
}
|
|
const members = node.members.nodes.filter(
|
|
(member): member is ts.MethodDeclaration =>
|
|
ts.isMethodDeclaration(member) &&
|
|
ts.isIdentifier(member.name) &&
|
|
!member.modifiers?.nodes.some(
|
|
(modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword,
|
|
),
|
|
);
|
|
if (node.heritageClauses?.nodes.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 ? property.declarations : []) {
|
|
if (
|
|
ts.isMethodDeclaration(declaration) &&
|
|
isJsSourceFile(ts.getSourceFileOfNode(declaration)) &&
|
|
checkedJsOverride(lowerer, declaration, member)
|
|
) {
|
|
// A checked override pair dispatches through the vtable,
|
|
// so neither side may become an implicit generic.
|
|
lowerer.checkedJsOverrideMethods.add(declaration);
|
|
lowerer.checkedJsOverrideMethods.add(member);
|
|
}
|
|
if (
|
|
ts.isMethodDeclaration(declaration) &&
|
|
isJsSourceFile(ts.getSourceFileOfNode(declaration)) &&
|
|
!checkedJsOverride(lowerer, declaration, member)
|
|
) {
|
|
lowerer.virtualJsMethods.add(declaration);
|
|
if (
|
|
isJsSourceFile(ts.getSourceFileOfNode(member)) &&
|
|
!member.typeParameters?.nodes &&
|
|
!member.asteriskToken &&
|
|
!member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) &&
|
|
!bodyReadsArguments(member) &&
|
|
member.parameters.nodes.every((p) => !p.dotDotDotToken)
|
|
) {
|
|
lowerer.virtualJsMethodArity.set(
|
|
declaration,
|
|
Math.max(
|
|
lowerer.virtualJsMethodArity.get(declaration) ?? 0,
|
|
member.parameters.nodes.length,
|
|
),
|
|
);
|
|
const checked =
|
|
!declaration.typeParameters?.nodes &&
|
|
!declaration.asteriskToken &&
|
|
!declaration.modifiers?.nodes.some(
|
|
(m) => m.kind === ts.SyntaxKind.AsyncKeyword,
|
|
) &&
|
|
!bodyReadsArguments(declaration) &&
|
|
[...member.parameters.nodes, ...declaration.parameters.nodes].every(
|
|
(p) => !p.dotDotDotToken,
|
|
);
|
|
if (checked) {
|
|
lowerer.checkedVirtualJsMethods.add(declaration);
|
|
lowerer.checkedVirtualJsMethods.add(member);
|
|
families.push([declaration, member]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
markBases(base);
|
|
}
|
|
};
|
|
if (instance) markBases(instance);
|
|
}
|
|
}
|
|
};
|
|
for (const file of files) ts.walkPreorder(file, visit);
|
|
for (const member of lowerer.checkedVirtualJsMethods) {
|
|
const scan = (node: ts.Node): void => {
|
|
if (node !== member && ts.isFunctionLike(node)) return;
|
|
if (ts.isReturnStatement(node) && node.expression)
|
|
lowerer.checkedVirtualJsValueReturns.add(member);
|
|
ts.forEachChild(node, scan);
|
|
};
|
|
scan(member);
|
|
}
|
|
let changed = true;
|
|
while (changed) {
|
|
changed = false;
|
|
for (const [base, derived] of families) {
|
|
if (
|
|
!lowerer.checkedVirtualJsValueReturns.has(base) &&
|
|
!lowerer.checkedVirtualJsValueReturns.has(derived)
|
|
)
|
|
continue;
|
|
for (const member of [base, derived])
|
|
if (!lowerer.checkedVirtualJsValueReturns.has(member)) {
|
|
lowerer.checkedVirtualJsValueReturns.add(member);
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** An override whose base and overriding methods are in files TypeScript
|
|
* checks and document every parameter: TypeScript checks the override
|
|
* against its base, so both keep their declared signatures, as in
|
|
* TypeScript classes. */
|
|
function checkedJsOverride(
|
|
lowerer: Lowerer,
|
|
base: ts.MethodDeclaration,
|
|
member: ts.MethodDeclaration,
|
|
): boolean {
|
|
return [base, member].every(
|
|
(method) =>
|
|
(lowerer.checkedJsFile(ts.getSourceFileOfNode(method)) || shapeOverlayAnnotated(method)) &&
|
|
method.parameters.nodes.every(documentedParameter),
|
|
);
|
|
}
|
|
|
|
/** 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 ? symbol.valueDeclaration : 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" && declaredType.kind !== "dyn") 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 annT = lowerer.typeOf(decl.name);
|
|
const props = lowerer.checker.getPropertiesOfType(annT);
|
|
if (props.length === 0) return false;
|
|
if (declaredType.kind === "dyn")
|
|
return (
|
|
lowerer.classImplementedProtocol(annT) &&
|
|
props.some((property) =>
|
|
isGenericCallableMemberType(lowerer.checker.getTypeOfSymbol(property), lowerer.checker),
|
|
)
|
|
);
|
|
const shape = lowerer.shapes.get(declaredType.shapeId);
|
|
if (!shape || shape.fields.length > 0 || shape.indexValue !== undefined || shape.tuple === true)
|
|
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(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.nodes, instance.params, loc, call);
|
|
const result: IrExpr = {
|
|
kind: "call",
|
|
callee: instance.name,
|
|
args: [thisArg, ...args],
|
|
type: instance.returnType,
|
|
loc,
|
|
};
|
|
// A fluent JavaScript method (every return ends in `this`, jose's claim
|
|
// setters) answers the receiver: the instance returns the declarer's
|
|
// class, a used result's checker type is the receiver's subclass.
|
|
if (
|
|
result.type.kind === "object" &&
|
|
!ts.isExpressionStatement(call.parent) &&
|
|
ts.isMethodDeclaration(info.decl) &&
|
|
isJsSourceFile(ts.getSourceFileOfNode(info.decl)) &&
|
|
returnsOnlyThisValue(info.decl)
|
|
) {
|
|
return lowerer.maybeNarrow(result, call);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/** 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) {
|
|
// A value lowered at an ANCESTOR of the class the site needs (a
|
|
// fluent `this` result whose lowering kept the method's declaring
|
|
// class, the receiver of the next call in a chain): the checker
|
|
// vouches for the subclass, so the bridge is the verified downcast,
|
|
// never an upcast the validator rejects (an internal error before).
|
|
if (
|
|
!lowerer.isSubclassOf(expr.type.className, className) &&
|
|
lowerer.isSubclassOf(className, expr.type.className)
|
|
) {
|
|
return { kind: "downcast", value: expr, type: { kind: "object", className }, loc: expr.loc };
|
|
}
|
|
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.nodes.length !== 1 || !ctor.body) return false;
|
|
const p = ctor.parameters.nodes[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.nodes[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.nodes.length !== 1) return false;
|
|
const a = call.arguments.nodes[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`))) {
|
|
const diagsBefore = lowerer.diags.length;
|
|
try {
|
|
out.push(lowerer.lowerClassCtor(info));
|
|
if (info.extendsArray) {
|
|
const fields = arrayClassFieldsFn(lowerer, info);
|
|
if (fields) out.push(fields);
|
|
const species = arrayClassSpeciesFn(lowerer, info);
|
|
if (species) out.push(species);
|
|
}
|
|
} catch (e) {
|
|
if (!(e instanceof PoisonError)) throw e;
|
|
if (info.decl && isJsSourceFile(ts.getSourceFileOfNode(info.decl))) {
|
|
const types: IrType[] = [
|
|
{ kind: "object", className },
|
|
...info.ctorParams.map((param) => param.type),
|
|
];
|
|
const fallback = lowerer.deferToRuntimeFence(diagsBefore, info.ctor ?? info.decl, {
|
|
kind: "function",
|
|
name: `%${className}.constructor`,
|
|
returnType: VOID,
|
|
params: types.map((type, index) => ({
|
|
localId: `p.${index}`,
|
|
name: index === 0 ? "this" : `p${index}`,
|
|
type,
|
|
})),
|
|
});
|
|
if (fallback) out.push(fallback);
|
|
}
|
|
}
|
|
}
|
|
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 {
|
|
if (info.extendsArray) {
|
|
lowerer.noteEdge(`%${info.def.name}.arrayFields`);
|
|
lowerer.noteEdge(`%${info.def.name}.species`);
|
|
}
|
|
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.nodes, 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 && !info.extendsDate && !info.extendsArray) {
|
|
// 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.nodes));
|
|
} else if (info.ctor.body) {
|
|
body.push(...lowerer.lowerDerivedCtorBody(info, thisLocal));
|
|
}
|
|
} else {
|
|
if (ctorBase || info.callableBase || info.extendsDate || info.extendsArray) {
|
|
// 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 string keys a class declaration puts on its prototype: concrete
|
|
* instance methods and accessors with literal names. Symbol-keyed,
|
|
* private, static and abstract members are not prototype string keys. */
|
|
function classPrototypeNames(decl: ts.ClassLikeDeclaration): string[] {
|
|
const names: string[] = [];
|
|
for (const member of decl.members.nodes) {
|
|
if (
|
|
!ts.isMethodDeclaration(member) &&
|
|
!ts.isGetAccessorDeclaration(member) &&
|
|
!ts.isSetAccessorDeclaration(member)
|
|
)
|
|
continue;
|
|
if (
|
|
ts
|
|
.getModifiers(member)
|
|
?.some(
|
|
(m) => m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.AbstractKeyword,
|
|
)
|
|
)
|
|
continue;
|
|
const name = member.name;
|
|
let text: string | null = null;
|
|
if (ts.isIdentifier(name) || ts.isStringLiteralLike(name)) text = name.text;
|
|
else if (ts.isNumericLiteral(name)) text = String(Number(name.text));
|
|
else if (ts.isComputedPropertyName(name) && ts.isStringLiteralLike(name.expression))
|
|
text = name.expression.text;
|
|
if (text !== null && !names.includes(text)) names.push(text);
|
|
}
|
|
return names;
|
|
}
|
|
|
|
/** 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;
|
|
if (ts.isStringLiteralLike(name)) return name.text;
|
|
if (ts.isNumericLiteral(name)) return String(Number(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" ||
|
|
member === "toPrimitive" ||
|
|
// A toStringTag field keeps its symbol field name; its accessor and
|
|
// method forms take the method slot name.
|
|
(member === "toStringTag" && !ts.isPropertyDeclaration(name.parent))
|
|
) {
|
|
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 skipping a declared return() without a
|
|
* diagnostic 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's type (the method's return): a class instance, or a
|
|
* record (an object literal) whose `next` field is a function. */
|
|
iterT: IrType & ({ kind: "object" } | { kind: "record" });
|
|
/** next()'s result record type. */
|
|
resultT: IrType & { kind: "record" };
|
|
valueT: IrType;
|
|
/** False: no done field — the protocol never terminates. */
|
|
hasDone: boolean;
|
|
/** The iterator declares a zero-parameter return(), which a consumer
|
|
* that stops before the end calls (IteratorClose). Only consumers that
|
|
* ask for `close` see such iterators. */
|
|
hasReturn: boolean;
|
|
}
|
|
/** A return() result type that is always an object: IteratorClose after a
|
|
* break throws a TypeError for any other result, which these desugars do
|
|
* not model. */
|
|
function objectResult(t: IrType): boolean {
|
|
return t.kind === "record" || t.kind === "object";
|
|
}
|
|
|
|
/** What a class iterable's consumer handles: `records` accepts an
|
|
* object-literal iterator; `close` accepts an iterator with return(),
|
|
* which the consumer calls when it stops early (or never stops early). */
|
|
export interface ClassIteratorOptions {
|
|
records?: boolean;
|
|
close?: boolean;
|
|
}
|
|
export function classIteratorOf(
|
|
lowerer: Lowerer,
|
|
t: IrType,
|
|
options: ClassIteratorOptions = {},
|
|
): 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;
|
|
let resultT: IrType;
|
|
let hasReturn: boolean;
|
|
if (iterT.kind === "object") {
|
|
const itInfo = lowerer.classes.get(iterT.className);
|
|
if (!itInfo) return null;
|
|
// IteratorClose honesty: a declared return() would be called by JS on
|
|
// abrupt completion, so only a consumer that closes takes it; for-of
|
|
// never calls throw(), the other consumers refuse it.
|
|
const close = findMethodOn(lowerer, itInfo, "return");
|
|
if (
|
|
close &&
|
|
(!options.close ||
|
|
close.sig.abstract === true ||
|
|
close.sig.params.length !== 0 ||
|
|
!objectResult(close.sig.ret))
|
|
)
|
|
return null;
|
|
if (findMethodOn(lowerer, itInfo, "throw") && !options.close) return null;
|
|
const next = findMethodOn(lowerer, itInfo, "next");
|
|
if (!next || next.sig.params.length !== 0 || next.sig.abstract === true) return null;
|
|
resultT = next.sig.ret;
|
|
hasReturn = !!close;
|
|
} else if (iterT.kind === "record" && options.records) {
|
|
const fields = lowerer.shapes.get(iterT.shapeId)?.fields;
|
|
const next = fields?.find((f) => f.name === "next")?.type;
|
|
if (!next || next.kind !== "func" || next.params.length !== 0 || next.rest) return null;
|
|
const close = fields?.find((f) => f.name === "return")?.type;
|
|
if (
|
|
close &&
|
|
(!options.close ||
|
|
close.kind !== "func" ||
|
|
close.params.length !== 0 ||
|
|
close.rest ||
|
|
!objectResult(close.ret))
|
|
)
|
|
return null;
|
|
resultT = next.ret;
|
|
hasReturn = close !== undefined;
|
|
} else return null;
|
|
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,
|
|
hasReturn,
|
|
};
|
|
}
|
|
|
|
/** The `next` an object-literal iterator's protocol calls, read once when
|
|
* the iterator opens: JS keeps the `next` it reads then in the iterator
|
|
* record, so a `next` the iterator assigns while it runs is not called.
|
|
* Null for an iterator class, whose `next` is a method. */
|
|
export function classIteratorNextRead(
|
|
lowerer: Lowerer,
|
|
cit: ClassIteratorInfo,
|
|
itRef: IrExpr,
|
|
loc: SrcLoc,
|
|
): (IrExpr & { type: IrType & { kind: "func" } }) | null {
|
|
const iterT = cit.iterT;
|
|
if (iterT.kind === "object") return null;
|
|
const field = lowerer.shapes.get(iterT.shapeId)!.fields.find((f) => f.name === "next")!;
|
|
return {
|
|
kind: "recordGet",
|
|
obj: itRef,
|
|
shapeId: iterT.shapeId,
|
|
field: "next",
|
|
type: field.type as IrType & { kind: "func" },
|
|
loc,
|
|
};
|
|
}
|
|
|
|
/** The `it.next()` step of a class iterator: an ordinary (possibly
|
|
* virtual) method call, or the call of `next`, the object-literal
|
|
* iterator's `next` that classIteratorNextRead read when it opened. */
|
|
export function classIteratorNextCall(
|
|
lowerer: Lowerer,
|
|
cit: ClassIteratorInfo,
|
|
itRef: IrExpr,
|
|
next: IrExpr | null,
|
|
loc: SrcLoc,
|
|
): IrExpr {
|
|
if (next === null || next.type.kind !== "func")
|
|
return classIteratorMethodCall(lowerer, cit, "next", itRef, loc);
|
|
return { kind: "callValue", callee: next, args: [], type: next.type.ret, loc };
|
|
}
|
|
|
|
/** `it.next()` or `it.return()` of a class iterator: a (possibly virtual)
|
|
* method call of an iterator class, or the call of an object-literal
|
|
* iterator's function field. */
|
|
export function classIteratorMethodCall(
|
|
lowerer: Lowerer,
|
|
cit: ClassIteratorInfo,
|
|
method: "next" | "return",
|
|
itRef: IrExpr,
|
|
loc: SrcLoc,
|
|
): IrExpr {
|
|
const iterT = cit.iterT;
|
|
if (iterT.kind === "object") {
|
|
const ret =
|
|
method === "next"
|
|
? cit.resultT
|
|
: findMethodOn(lowerer, lowerer.classes.get(iterT.className)!, "return")!.sig.ret;
|
|
return accessorCall(lowerer, iterT.className, method, itRef, [], ret, loc);
|
|
}
|
|
const field = lowerer.shapes.get(iterT.shapeId)!.fields.find((f) => f.name === method)!;
|
|
const fnT = field.type as IrType & { kind: "func" };
|
|
return {
|
|
kind: "callValue",
|
|
callee: {
|
|
kind: "recordGet",
|
|
obj: itRef,
|
|
shapeId: iterT.shapeId,
|
|
field: method,
|
|
type: fnT,
|
|
loc,
|
|
},
|
|
args: [],
|
|
type: fnT.ret,
|
|
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 {
|
|
// A spread takes every value, so it never closes the iterator.
|
|
const cit = classIteratorOf(lowerer, src.type, { records: true, close: true });
|
|
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).
|
|
// A checked-dynamic value (`IteratorResult<T>`'s value is `T | any`)
|
|
// converts to the destination element with a check.
|
|
if (!typeEquals(outElem, cit.valueT) && cit.valueT.kind !== "dyn") {
|
|
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 = keyedHelperName(lowerer, "%iter.drain", key);
|
|
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 nextRead = classIteratorNextRead(lowerer, cit, itRef, loc);
|
|
const nextRef: IrExpr | null = nextRead ? varRef("next.0", nextRead.type, loc) : null;
|
|
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, nextRef, 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 },
|
|
...(nextRead ? [{ id: "next.0", name: "next", type: nextRead.type, 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,
|
|
},
|
|
...(nextRead
|
|
? [{ kind: "varDecl", localId: "next.0", init: nextRead, loc } satisfies IrStmt]
|
|
: []),
|
|
{
|
|
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.kind === "object" ? cit.iterT.className : typeKey(cit.iterT)}`;
|
|
let name = lowerer.iterDrainHelpers.get(key);
|
|
if (!name) {
|
|
name = keyedHelperName(lowerer, "%iter.drain", key);
|
|
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, null, 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,
|
|
});
|
|
}
|
|
// An override converting its declared result into the inherited ABI:
|
|
// the body lowers as `%C.m$own` under its declared result, and the slot
|
|
// function forwards to it and converts what it returns.
|
|
const adapter = info.returnAdapters?.get(mName);
|
|
if (adapter) {
|
|
const ownSig: ClassMethodSignature = { ...sig, params: adapter.params, ret: adapter.ret };
|
|
if (adapter.async) ownSig.async = true;
|
|
const own = lowerClassMethodBody(lowerer, info, fnLike, `%${className}.${mName}$own`, ownSig);
|
|
if (!own) return null;
|
|
lowerer.liftedFns.push(own);
|
|
const loc = locOf(fnLike);
|
|
const params: IrParam[] = [
|
|
{ localId: "this.0", name: "this", type: thisType },
|
|
...sig.params.map((p, i) => ({ localId: `p.${i}`, name: `p${i}`, type: p.type })),
|
|
];
|
|
const fnCtx = newFnCtx(false, null, null, sig.ret);
|
|
lowerer.fnStack.push(fnCtx);
|
|
try {
|
|
const call: IrExpr = {
|
|
kind: "call",
|
|
callee: own.name,
|
|
args: params.map((p, i): IrExpr => {
|
|
const ref: IrExpr = { kind: "varRef", localId: p.localId, type: p.type, loc };
|
|
// `this` passes through; each inherited argument converts into
|
|
// the override's declared parameter (checked where it narrows).
|
|
return i === 0 ? ref : lowerer.coerceInto(fnLike.name, ref, adapter.params[i - 1]!.type);
|
|
}),
|
|
type: adapter.ret,
|
|
loc,
|
|
};
|
|
// A void slot discards the override's result after running it. A
|
|
// toString slot hands on what every ToString consumer of the result
|
|
// makes of it (`String(err)`, templates): undefined reads
|
|
// "undefined", as in Node. Other slots check the result into the
|
|
// inherited type.
|
|
const result = (): IrExpr =>
|
|
mName === "toString" && sig.ret.kind === "string"
|
|
? call.type.kind === "dyn"
|
|
? { kind: "libCall", fn: "dyn.stringConstructor", args: [call], type: STRING, loc }
|
|
: lowerer.ensureString(call, fnLike.name)
|
|
: lowerer.coerceInto(fnLike.name, call, sig.ret);
|
|
const body: IrStmt[] =
|
|
sig.ret.kind === "void"
|
|
? [
|
|
{ kind: "exprStmt", expr: call, loc },
|
|
{ kind: "return", value: null, loc },
|
|
]
|
|
: [{ kind: "return", value: result(), loc }];
|
|
return {
|
|
name: `%${className}.${mName}`,
|
|
params,
|
|
returnType: sig.ret,
|
|
locals: [
|
|
...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
|
|
...lowerer.ctx.locals,
|
|
],
|
|
body,
|
|
loc,
|
|
};
|
|
} finally {
|
|
lowerer.fnStack.pop();
|
|
}
|
|
}
|
|
return lowerClassMethodBody(lowerer, info, fnLike, `%${className}.${mName}`, sig);
|
|
}
|
|
|
|
function lowerClassMethodBody(
|
|
lowerer: Lowerer,
|
|
info: ClassInfo,
|
|
fnLike: ts.MethodDeclaration | ts.AccessorDeclaration,
|
|
fnName: string,
|
|
sig: ClassMethodSignature,
|
|
): IrFunction | null {
|
|
const className = info.def.name;
|
|
const thisType: IrType = { kind: "object", className };
|
|
if (!fnLike.body) return null;
|
|
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.nodes, 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.nodes)];
|
|
appendImplicitUndefinedReturn(lowerer, body, bodyReturn, locOf(fnLike));
|
|
const fn: IrFunction = {
|
|
name: fnName,
|
|
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?.nodes.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.nodes, 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.nodes)];
|
|
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 declared type a refused checked-dynamic class field names in its
|
|
* diagnostic (`'any'`, `'WeakMap<object, string>'`): `unknown` and
|
|
* `object` fields store checked-dynamic values, so the refusal is about
|
|
* whatever else the declaration spells. */
|
|
function dynFieldTypeSpelling(lowerer: Lowerer, node: ts.Node): string {
|
|
return `'${lowerer.checker.typeToString(lowerer.typeOf(node))}'`;
|
|
}
|
|
|
|
/** 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.errorCause) {
|
|
const loc = f.initializer ? locOf(f.initializer) : f.undefinedInitializer!;
|
|
const value = f.initializer
|
|
? lowerer.lowerExprExpecting(f.initializer, DYN)
|
|
: dynUndefinedExpr(loc);
|
|
out.push({
|
|
kind: "exprStmt",
|
|
expr: {
|
|
kind: "libCall",
|
|
fn: "error.defineCause",
|
|
args: [{ kind: "varRef", localId: thisLocal.id, type: thisType, loc }, value],
|
|
type: VOID,
|
|
loc,
|
|
},
|
|
loc,
|
|
});
|
|
continue;
|
|
}
|
|
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),
|
|
define: true,
|
|
loc,
|
|
});
|
|
}
|
|
if (!f.initializer) continue;
|
|
lowerer.countStatement(f.initializer, locOf(f.initializer).file);
|
|
const diagsBefore = lowerer.diags.length;
|
|
const sinkMark = lowerer.diagSinkMark();
|
|
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,
|
|
define: true,
|
|
loc: locOf(f.initializer),
|
|
});
|
|
} catch (e) {
|
|
if (!(e instanceof PoisonError)) throw e;
|
|
lowerer.countStatementFailed(f.initializer, locOf(f.initializer).file);
|
|
if (isJsSourceFile(ts.getSourceFileOfNode(f.initializer))) {
|
|
const fence =
|
|
lowerer.diagSink === null
|
|
? lowerer.deferToRuntimeFence(diagsBefore, f.initializer, { kind: "statement" })
|
|
: lowerer.capturedRuntimeFence(sinkMark, f.initializer);
|
|
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;
|
|
}
|
|
|
|
/** Whether a super() call's value is discarded by its statement: the call
|
|
* is the expression statement itself, or an operand of a comma chain that
|
|
* is (`super(m, o), this.code = c;`, as minifiers join statements). The
|
|
* statement lowering runs each comma operand as its own statement in
|
|
* source order, so the chain lowers exactly like `super(m, o); this.code =
|
|
* c;`. Parentheses are transparent to both. */
|
|
function superCallInStatementPosition(call: ts.CallExpression): boolean {
|
|
let node: ts.Node = call;
|
|
for (;;) {
|
|
const parent = node.parent;
|
|
if (ts.isExpressionStatement(parent)) return true;
|
|
if (ts.isIfStatement(parent)) {
|
|
const lead = superConditionLead(parent.expression);
|
|
return lead !== null && call.pos >= lead.left.pos && call.end <= lead.left.end;
|
|
}
|
|
const comma =
|
|
ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.CommaToken;
|
|
if (!comma && !ts.isParenthesizedExpression(parent)) return false;
|
|
node = parent;
|
|
}
|
|
}
|
|
|
|
/** The comma chain of an `if` condition whose leading operands hold a
|
|
* super() call (`if (super(), r === 0) throw ...`, as minifiers join a
|
|
* super statement with the next test), or null. The if statement runs the
|
|
* chain's left operand as a statement first and tests its right operand,
|
|
* which is what the comma evaluates. */
|
|
export function superConditionLead(cond: ts.Expression): ts.BinaryExpression | null {
|
|
let expr = cond;
|
|
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
|
|
if (!ts.isBinaryExpression(expr) || expr.operatorToken.kind !== ts.SyntaxKind.CommaToken)
|
|
return null;
|
|
const holdsSuperCall = (operand: ts.Expression): boolean => {
|
|
let e = operand;
|
|
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
|
if (ts.isCallExpression(e)) return e.expression.kind === ts.SyntaxKind.SuperKeyword;
|
|
return (
|
|
ts.isBinaryExpression(e) &&
|
|
e.operatorToken.kind === ts.SyntaxKind.CommaToken &&
|
|
(holdsSuperCall(e.left) || holdsSuperCall(e.right))
|
|
);
|
|
};
|
|
return holdsSuperCall(expr.left) ? expr : null;
|
|
}
|
|
|
|
/** Super statements initialize the base and then the derived fields on
|
|
* whichever control-flow path executes. Repeated calls are explicitly
|
|
* refused until native layouts can allocate a second receiver. */
|
|
export function lowerDerivedCtorBody(
|
|
lowerer: Lowerer,
|
|
info: ClassInfo,
|
|
thisLocal: IrLocal,
|
|
forward?: IrExpr[],
|
|
): IrStmt[] {
|
|
let superSeen = false;
|
|
const initialized = lowerer.declareHiddenLocal("%superInitialized", BOOL);
|
|
initialized.mutable = true;
|
|
const context = lowerer.ctx;
|
|
const previous = context.superCall;
|
|
const previousInitialized = context.superInitialized;
|
|
context.superInitialized = initialized;
|
|
context.superCall = (superCall) => {
|
|
if (!superCallInStatementPosition(superCall))
|
|
lowerer.unsupported("SC1090", superCall, "super() in an expression position");
|
|
superSeen = true;
|
|
const loc = locOf(superCall);
|
|
const statements: IrStmt[] = [
|
|
{
|
|
kind: "if",
|
|
cond: { kind: "varRef", localId: initialized.id, type: BOOL, loc },
|
|
then: [
|
|
{
|
|
kind: "exprStmt",
|
|
expr: nodeThrowExpr(
|
|
0,
|
|
"SC2020",
|
|
"repeated super() calls require a fresh receiver and are not supported yet",
|
|
DYN,
|
|
loc,
|
|
),
|
|
loc,
|
|
},
|
|
],
|
|
else_: null,
|
|
loc,
|
|
},
|
|
];
|
|
if (info.extendsArray) {
|
|
statements.push(...lowerArraySuperCall(lowerer, info, thisLocal, superCall));
|
|
} else if (info.callableBase || info.extendsDate) {
|
|
if (superCall.arguments.nodes.some(ts.isSpreadElement))
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
superCall,
|
|
info.extendsDate
|
|
? "spreading super arguments into the Date constructor"
|
|
: "spreading explicit super arguments into an ordinary constructor",
|
|
);
|
|
const pack: IrExpr = {
|
|
kind: "dynArrLit",
|
|
elems: superCall.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
|
|
type: DYN,
|
|
loc,
|
|
};
|
|
statements.push(lowerer.superCallStmt(info, thisLocal, [pack], loc));
|
|
} else {
|
|
const base = superBaseOf(info)!;
|
|
if (base.builtinEmitter && superCall.arguments.nodes.length > 0)
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
superCall,
|
|
"EventEmitter constructor options ('captureRejections')",
|
|
);
|
|
if (base.builtinStream) {
|
|
statements.push(
|
|
...lowerStreamSuperCall(
|
|
lowerer,
|
|
info,
|
|
base,
|
|
superCall.arguments.nodes,
|
|
thisLocal,
|
|
loc,
|
|
superCall,
|
|
),
|
|
);
|
|
} else {
|
|
const surplusSpread =
|
|
forward === undefined &&
|
|
!base.builtinError &&
|
|
!base.builtinEmitter &&
|
|
base.ctorParams.length === 0 &&
|
|
superCall.arguments.nodes.some(ts.isSpreadElement);
|
|
if (surplusSpread) {
|
|
const evaluated = lowerer.completeArgs(
|
|
superCall.arguments.nodes,
|
|
[{ mode: "dynRest", type: DYN }],
|
|
loc,
|
|
superCall,
|
|
);
|
|
for (const value of evaluated) statements.push({ kind: "exprStmt", expr: value, loc });
|
|
}
|
|
const surplus =
|
|
forward === undefined &&
|
|
!base.builtinError &&
|
|
!base.builtinEmitter &&
|
|
!base.ctorParams.some((param) => param.mode === "rest" || param.mode === "dynRest") &&
|
|
!superCall.arguments.nodes.some(ts.isSpreadElement) &&
|
|
superCall.arguments.nodes.length > base.ctorParams.length;
|
|
let args = surplusSpread
|
|
? []
|
|
: forward !== undefined
|
|
? forward
|
|
: base.builtinError
|
|
? lowerer.errorConstructorArgs(superCall.arguments.nodes, loc, superCall)
|
|
: base.builtinEmitter
|
|
? []
|
|
: lowerer.completeArgs(
|
|
surplus
|
|
? superCall.arguments.nodes.slice(0, base.ctorParams.length)
|
|
: superCall.arguments.nodes,
|
|
base.ctorParams,
|
|
loc,
|
|
superCall,
|
|
);
|
|
if (surplus) {
|
|
args = args.map((arg) => {
|
|
const local = lowerer.declareHiddenLocal("%superArgument", arg.type);
|
|
statements.push({ kind: "varDecl", localId: local.id, init: arg, loc });
|
|
return { kind: "varRef", localId: local.id, type: arg.type, loc };
|
|
});
|
|
for (const argument of superCall.arguments.nodes.slice(base.ctorParams.length)) {
|
|
statements.push({
|
|
kind: "exprStmt",
|
|
expr: lowerer.lowerExpr(argument),
|
|
loc: locOf(argument),
|
|
});
|
|
}
|
|
}
|
|
statements.push(lowerer.superCallStmt(info, thisLocal, args, loc));
|
|
}
|
|
}
|
|
statements.push({
|
|
kind: "assign",
|
|
localId: initialized.id,
|
|
value: { kind: "boolLit", value: true, type: BOOL, loc },
|
|
loc,
|
|
});
|
|
delete context.superInitialized;
|
|
try {
|
|
statements.push(
|
|
...lowerer.fieldInitStmts(info, thisLocal),
|
|
...paramPropInitStmts(lowerer, info, thisLocal),
|
|
);
|
|
} finally {
|
|
context.superInitialized = initialized;
|
|
}
|
|
const result: IrExpr = { kind: "varRef", localId: thisLocal.id, type: thisLocal.type, loc };
|
|
return {
|
|
kind: "seqExpr",
|
|
stmts: statements.map((statement) =>
|
|
statement.kind === "runtimeFence"
|
|
? {
|
|
kind: "exprStmt",
|
|
expr: nodeThrowExpr(0, statement.code, statement.message, DYN, statement.loc),
|
|
loc: statement.loc,
|
|
}
|
|
: statement,
|
|
),
|
|
result,
|
|
type: result.type,
|
|
loc,
|
|
};
|
|
};
|
|
try {
|
|
const result = lowerer.lowerStmts(info.ctor!.body!.statements.nodes);
|
|
result.push({
|
|
kind: "if",
|
|
cond: {
|
|
kind: "unary",
|
|
op: "!",
|
|
operand: { kind: "varRef", localId: initialized.id, type: BOOL, loc: locOf(info.ctor!) },
|
|
type: BOOL,
|
|
loc: locOf(info.ctor!),
|
|
},
|
|
then: [
|
|
{
|
|
kind: "exprStmt",
|
|
expr: nodeThrowExpr(
|
|
5,
|
|
"",
|
|
"Must call super constructor in derived class before accessing 'this' or returning from derived constructor",
|
|
DYN,
|
|
locOf(info.ctor!),
|
|
),
|
|
loc: locOf(info.ctor!),
|
|
},
|
|
],
|
|
else_: null,
|
|
loc: locOf(info.ctor!),
|
|
});
|
|
if (!superSeen)
|
|
lowerer.pushDiag(
|
|
unsupportedDiag(
|
|
"SC1090",
|
|
locOf(info.ctor!),
|
|
"derived constructors without a lowered super() call",
|
|
),
|
|
);
|
|
return [
|
|
{
|
|
kind: "varDecl",
|
|
localId: initialized.id,
|
|
init: { kind: "boolLit", value: false, type: BOOL, loc: locOf(info.ctor!) },
|
|
loc: locOf(info.ctor!),
|
|
},
|
|
...result,
|
|
];
|
|
} finally {
|
|
if (previous) context.superCall = previous;
|
|
else delete context.superCall;
|
|
if (previousInitialized) context.superInitialized = previousInitialized;
|
|
else delete context.superInitialized;
|
|
}
|
|
}
|
|
|
|
/** `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 thisRef: IrExpr = {
|
|
kind: "varRef",
|
|
localId: thisLocal.id,
|
|
type: { kind: "object", className: info.def.name },
|
|
loc,
|
|
};
|
|
if (info.extendsDate) return dateSuperCall(thisRef, info, args[0]!, loc);
|
|
if (info.extendsArray) return arraySuperCall(info, thisRef, args[0]!, 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,
|
|
};
|
|
// A callable base has no class info, so the base is read after that case.
|
|
const base = superBaseOf(info)!;
|
|
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.builtinEventTarget) return eventTargetSuperCall(lowerer, base, thisRef, args, 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 ⇒ the init stmt, then the
|
|
// class's _writev/_construct follow-ups.
|
|
const stmts = lowerStreamSuperCall(
|
|
lowerer,
|
|
info,
|
|
base,
|
|
[],
|
|
thisLocal,
|
|
loc,
|
|
info.decl ?? info.ctor!,
|
|
);
|
|
return stmts.length === 1 ? stmts[0]! : { kind: "block", body: stmts, loc };
|
|
}
|
|
lowerer.noteSuperConstructorEdge(`%${base.def.name}.constructor`);
|
|
const call: IrStmt = {
|
|
kind: "exprStmt",
|
|
expr: {
|
|
kind: "call",
|
|
callee: `%${base.def.name}.constructor`,
|
|
args: [lowerer.upcastTo(thisRef, base.def.name), ...args],
|
|
type: VOID,
|
|
loc,
|
|
},
|
|
loc,
|
|
};
|
|
return superIntoResultSlot(
|
|
lowerer,
|
|
base,
|
|
info,
|
|
call,
|
|
lowerer.upcastTo({ ...thisRef }, base.def.name),
|
|
info.ctor ?? info.decl!,
|
|
);
|
|
}
|
|
|
|
/** `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;
|
|
// `super.toISOString()` in a class declared `extends Date`.
|
|
if (cls?.extendsDate && !cls.base) {
|
|
const thisL = lowerer.resolveThis();
|
|
if (!thisL) lowerer.unsupported("SC1080", access);
|
|
const thisRef: IrExpr = {
|
|
kind: "varRef",
|
|
localId: thisL.id,
|
|
type: thisL.type,
|
|
loc: locOf(call),
|
|
};
|
|
const date = lowerDateSuperMethodCall(lowerer, call, access.name.text, cls, thisRef);
|
|
if (date) return date;
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
access,
|
|
`'super.${access.name.text}' (Date.prototype does not declare it)`,
|
|
);
|
|
}
|
|
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.nodes, 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;
|
|
}
|
|
// `super.dispatchEvent(e)`, `super.preventDefault()`: the inherited
|
|
// runtime members over this method's own `this` (subclasses cannot
|
|
// override them, so the static answer is the runtime's).
|
|
if (dateRootOf(cls.base) && isDatePrototypeMethod(access.name.text)) {
|
|
const thisL = lowerer.resolveThis();
|
|
if (!thisL) lowerer.unsupported("SC1080", access);
|
|
const thisRef: IrExpr = {
|
|
kind: "varRef",
|
|
localId: thisL.id,
|
|
type: thisL.type,
|
|
loc: locOf(call),
|
|
};
|
|
return lowerDateSuperMethodCall(lowerer, call, access.name.text, cls.base, thisRef)!;
|
|
}
|
|
if (eventTargetApiMethod(cls.base, access.name.text)) {
|
|
const thisL = lowerer.resolveThis();
|
|
if (!thisL) lowerer.unsupported("SC1080", access);
|
|
const thisRef: IrExpr = {
|
|
kind: "varRef",
|
|
localId: thisL.id,
|
|
type: thisL.type,
|
|
loc: locOf(call),
|
|
};
|
|
return lowerEventTargetMethodCall(lowerer, call, access, cls.base, thisRef);
|
|
}
|
|
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 };
|
|
// A method whose `this` is the call-site receiver (a JavaScript class's
|
|
// receiver-independent method value, reflectedClassMethodValue) looks the
|
|
// method up on the base class's complete prototype, as `super` reads
|
|
// [[HomeObject]].[[Prototype]], and calls it with that receiver.
|
|
const dynamicThis = thisRef.type.kind === "dyn";
|
|
// While no code reads a prototype method by name (which could replace it),
|
|
// that method is the declaring class's own: its receiver-independent value
|
|
// is called directly. Reflecting the base's whole prototype instead would
|
|
// put its members on the prototype chain of every subclass instance's
|
|
// property bag, ahead of the subclasses' own compiled overrides.
|
|
const declared =
|
|
dynamicThis && !lowerer.prototypeMethodAccesses.has(access.name.text)
|
|
? reflectedClassMethodValue(lowerer, found.declarer, access.name.text)
|
|
: null;
|
|
if (dynamicThis || lowerer.prototypeMethodAccesses.has(access.name.text)) {
|
|
const prototype = declared
|
|
? null
|
|
: dynamicThis && cls.base.decl
|
|
? reflectClassPrototype(lowerer, cls.base, loc, classValueRef(lowerer, cls.base, access))
|
|
: classPrototypeData(lowerer, cls.base, loc);
|
|
if (declared || prototype) {
|
|
const callee: IrExpr = declared
|
|
? lowerer.coerceToExpected(declared, DYN)
|
|
: {
|
|
kind: "dynKeyGet",
|
|
value: prototype!,
|
|
key: { kind: "strLit", value: access.name.text, type: STRING, loc },
|
|
type: DYN,
|
|
loc,
|
|
};
|
|
// Spread arguments are spread into the dynamic call, as in a dynamic method call.
|
|
const spreads: { arg: number; what: string }[] = [];
|
|
const args = call.arguments.nodes.map((arg, index) => {
|
|
if (!ts.isSpreadElement(arg)) return lowerer.lowerExprExpecting(arg, DYN);
|
|
const spelling = ts.getTextOfNode(arg.expression);
|
|
spreads.push({ arg: index, what: spelling });
|
|
return checkedIterableSpread(lowerer, lowerer.lowerExpr(arg.expression), spelling, loc);
|
|
});
|
|
const value: IrExpr = {
|
|
kind: "dynCall",
|
|
callee,
|
|
receiver: lowerer.coerceToExpected(thisRef, DYN),
|
|
calleeName: ts.getTextOfNode(access),
|
|
args,
|
|
...(spreads.length > 0 ? { spreads } : {}),
|
|
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.nodes, 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;
|
|
}
|
|
|
|
/** The run-time form of refuseInheritedBuiltinCtorArgs, for construction
|
|
* from checked-dynamic code (`new F(options)` with F an untyped value
|
|
* holding C), whose argument count is known only to the caller: a
|
|
* runtimeFence located at C, or null when C's constructor takes its
|
|
* arguments. */
|
|
export function inheritedBuiltinCtorFence(
|
|
lowerer: Lowerer,
|
|
info: ClassInfo,
|
|
loc: SrcLoc,
|
|
): IrStmt | null {
|
|
let feature: string | null = null;
|
|
if (inheritsBuiltinStreamCtor(lowerer, info))
|
|
feature = `new ${info.def.name.replace(/^%/, "")} with arguments through an inherited stream constructor (declare a constructor that passes an inline options object to super(...))`;
|
|
else if (lowerer.inheritsBuiltinEmitterCtor(info))
|
|
feature = "EventEmitter constructor options ('captureRejections')";
|
|
if (feature === null) return null;
|
|
// Located at the class, whose constructor takes no arguments.
|
|
const at = info.decl !== null ? locOf(info.decl) : loc;
|
|
const diag = unsupportedDiag("SC1090", at, feature);
|
|
lowerer.runtimeFences.push(diag);
|
|
const line =
|
|
info.decl !== null
|
|
? ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(info.decl), at.start).line + 1
|
|
: null;
|
|
const where = line !== null ? ` [${diag.code} at ${at.file}:${line}]` : "";
|
|
return { kind: "runtimeFence", code: diag.code, message: `${diag.message}${where}`, loc };
|
|
}
|
|
|
|
/** `new C(args)` where C's constructor is the inherited runtime stream or
|
|
* EventEmitter constructor (a ctor-less chain into the base): that
|
|
* constructor takes zero arguments here (the options bag fences, like the
|
|
* super() form), so arguments report at the first one. Applies to direct
|
|
* construction and to construction through a class value of C. Stream
|
|
* subclasses come first: their chain roots at the emitter too, but the
|
|
* message should name the stream story. */
|
|
function refuseInheritedBuiltinCtorArgs(
|
|
lowerer: Lowerer,
|
|
info: ClassInfo,
|
|
expr: ts.NewExpression,
|
|
): void {
|
|
const first = expr.arguments?.nodes[0];
|
|
if (first === undefined) return;
|
|
if (inheritsBuiltinStreamCtor(lowerer, info)) {
|
|
lowerer.noLowering(
|
|
`new ${info.def.name.replace(/^%/, "")} with arguments through an inherited stream constructor`,
|
|
first,
|
|
"declare a constructor that passes an inline options object to super(...)",
|
|
);
|
|
}
|
|
if (lowerer.inheritsBuiltinEmitterCtor(info)) {
|
|
lowerer.unsupported("SC1090", first, "EventEmitter constructor options ('captureRejections')");
|
|
}
|
|
}
|
|
|
|
/** 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. */
|
|
/** `new AggregateError(errors, message, options)` in the spec's order:
|
|
* the message and cause install first, then `errors` iterates (a
|
|
* non-iterable throws out of the constructor). */
|
|
function aggregateErrorConstructor(lowerer: Lowerer, loc: SrcLoc): string {
|
|
const name = "%aggregate.new";
|
|
if (lowerer.liftedFns.some((fn) => fn.name === name)) return name;
|
|
const type: IrType = { kind: "object", className: "%AggregateError" };
|
|
const context = newFnCtx(false, null, null, type);
|
|
lowerer.fnStack.push(context);
|
|
try {
|
|
const params = ["errors", "message", "options"].map((id) => ({
|
|
localId: id,
|
|
name: id,
|
|
type: DYN,
|
|
}));
|
|
const error = varRef("error", type, loc);
|
|
const list = checkedIterableSpread(lowerer, varRef("errors", DYN, loc), "", loc);
|
|
lowerer.liftedFns.push({
|
|
name,
|
|
params,
|
|
returnType: type,
|
|
locals: [
|
|
...params.map((p) => ({ id: p.localId, name: p.name, type: DYN, mutable: false })),
|
|
{ id: "error", name: "error", type, mutable: false },
|
|
...context.locals,
|
|
],
|
|
body: [
|
|
{
|
|
kind: "varDecl",
|
|
localId: "error",
|
|
init: {
|
|
kind: "libCall",
|
|
fn: "error.newAggregate",
|
|
args: [varRef("message", DYN, loc), varRef("options", DYN, loc)],
|
|
type,
|
|
loc,
|
|
},
|
|
loc,
|
|
},
|
|
{
|
|
kind: "exprStmt",
|
|
expr: {
|
|
kind: "libCall",
|
|
fn: "error.aggregateSetErrors",
|
|
args: [error, list],
|
|
type: VOID,
|
|
loc,
|
|
},
|
|
loc,
|
|
},
|
|
{ kind: "return", value: error, loc },
|
|
],
|
|
loc,
|
|
});
|
|
} finally {
|
|
lowerer.fnStack.pop();
|
|
}
|
|
return name;
|
|
}
|
|
|
|
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`);
|
|
refuseInheritedBuiltinCtorArgs(lowerer, info, expr);
|
|
// 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?.nodes ?? [], loc, expr)
|
|
: lowerer.completeArgs(expr.arguments?.nodes ?? [], info.ctorParams, loc, expr);
|
|
for (let current: ClassInfo | null = info; current; current = current.base) {
|
|
if (current.def.baseValueGlobal)
|
|
return constructedValue(lowerer, info, {
|
|
kind: "newValue",
|
|
callee: classValueRef(lowerer, info, expr),
|
|
args,
|
|
type: { kind: "object", className: info.def.name },
|
|
loc,
|
|
});
|
|
}
|
|
return constructedValue(lowerer, info, {
|
|
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;
|
|
}
|
|
|
|
/** An array element read that construction narrows to a class value with a
|
|
* check (`%new.callee`, which throws "is not a constructor"). */
|
|
interface NarrowedConstructorRead {
|
|
read: IrExpr;
|
|
check: string;
|
|
unionT: IrType & { kind: "union" };
|
|
calleeT: IrType & { kind: "classval" };
|
|
message: IrExpr;
|
|
}
|
|
|
|
/** `new arr[i](args)` through a narrowed element read: an interned lifted
|
|
* function takes the read and the completed arguments, which evaluate in
|
|
* that order as call arguments, then runs the check and constructs. JS
|
|
* evaluates the arguments before it checks that the callee constructs, and
|
|
* the arguments stay in the one call, where a spread's argument pack lives
|
|
* until every argument has read it. */
|
|
function checkedElementConstruction(
|
|
lowerer: Lowerer,
|
|
info: ClassInfo,
|
|
narrowed: NarrowedConstructorRead,
|
|
args: IrExpr[],
|
|
loc: SrcLoc,
|
|
): IrExpr {
|
|
const type: IrType = { kind: "object", className: info.def.name };
|
|
const key = `newChecked:${narrowed.check}:${info.def.name}:${args.map((a) => typeKey(a.type)).join(",")}`;
|
|
let name = lowerer.valueHelpers.get(key);
|
|
if (!name) {
|
|
name = keyedHelperName(lowerer, "%new.checked", key);
|
|
lowerer.valueHelpers.set(key, name);
|
|
const params: IrParam[] = [
|
|
{ localId: "value", name: "value", type: narrowed.unionT },
|
|
{ localId: "message", name: "message", type: STRING },
|
|
...args.map((arg, i) => ({ localId: `arg.${i}`, name: `arg${i}`, type: arg.type })),
|
|
];
|
|
lowerer.liftedFns.push({
|
|
name,
|
|
params,
|
|
returnType: type,
|
|
locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
|
|
body: [
|
|
{
|
|
kind: "return",
|
|
value: {
|
|
kind: "newValue",
|
|
callee: {
|
|
kind: "call",
|
|
callee: narrowed.check,
|
|
args: [varRef("value", narrowed.unionT, loc), varRef("message", STRING, loc)],
|
|
type: narrowed.calleeT,
|
|
loc,
|
|
},
|
|
args: params.slice(2).map((p) => varRef(p.localId, p.type, loc)),
|
|
type,
|
|
loc,
|
|
},
|
|
loc,
|
|
},
|
|
],
|
|
loc,
|
|
});
|
|
}
|
|
return {
|
|
kind: "call",
|
|
callee: name,
|
|
args: [narrowed.read, narrowed.message, ...args],
|
|
type,
|
|
loc,
|
|
};
|
|
}
|
|
/** Map, Set and the error classes construct in lowerNew's identifier
|
|
* branch, which a module member alias of one (`new mod.M()`) also takes. */
|
|
function isCollectionOrErrorClass(ref: BuiltinClassRef | null): boolean {
|
|
return ref?.kind === "collection" || ref?.kind === "error";
|
|
}
|
|
|
|
export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
|
const loc = locOf(expr);
|
|
// The deprecated `new Buffer(...)` (containers/bytes.ts).
|
|
{
|
|
const buffer = lowerBufferConstruct(lowerer, expr);
|
|
if (buffer) return buffer;
|
|
}
|
|
// new Function(...): Node's EvalError under
|
|
// --disallow-code-generation-from-strings (code-generation.ts).
|
|
if (codeGenerationCalleeOf(lowerer, expr.expression) === "Function")
|
|
return lowerCodeGenerationCall(lowerer, expr, "Function");
|
|
// `new <temporal>.X(...)`: Node 24 has no such global, so the callee's
|
|
// root read throws the ReferenceError before any argument evaluates.
|
|
{
|
|
const root = ambientUndefVarRootOf(lowerer, expr.expression);
|
|
if (root !== null && root.text === TEMPORAL_GLOBAL) {
|
|
const t =
|
|
ambientUndefReadType(lowerer, expr) ?? contextualUndefReadType(lowerer, expr) ?? F64;
|
|
return nsUndefRead(lowerer, root.text, expr, t);
|
|
}
|
|
}
|
|
// `new (require("./x").C)()`: the module's %init at the require's
|
|
// position, then construction through the member as for a binding.
|
|
if (ts.isPropertyAccessExpression(expr.expression) && !expr.expression.questionDotToken) {
|
|
const req = inlineNamespaceRequireOf(lowerer, expr.expression.expression);
|
|
if (req !== null && !lowerer.inlineRequireInitDone.has(req.call)) {
|
|
return lowerWithInlineRequireInit(lowerer, req, () => lowerNew(lowerer, expr));
|
|
}
|
|
}
|
|
const worker = lowerWorkerNew(lowerer, expr);
|
|
if (worker) return worker;
|
|
const readlineUi = lowerReadlineUiNew(lowerer, expr) ?? lowerReadlineInterfaceNew(lowerer, expr);
|
|
if (readlineUi) return readlineUi;
|
|
const channel = lowerMessageChannelNew(lowerer, expr) ?? lowerBroadcastChannelNew(lowerer, expr);
|
|
if (channel) return channel;
|
|
let intlConstructor = expr.expression;
|
|
if (ts.isIdentifier(intlConstructor)) {
|
|
const symbol = lowerer.resolveValueSymbol(intlConstructor);
|
|
const declaration = symbol ? symbol.valueDeclaration : undefined;
|
|
if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer)
|
|
intlConstructor = declaration.initializer;
|
|
}
|
|
const intl = ts.isPropertyAccessExpression(intlConstructor)
|
|
? lowerer.stdlibGlobalMember(intlConstructor, "Intl")
|
|
: null;
|
|
if (intl && intl !== "Segmenter")
|
|
lowerer.noLowering(
|
|
`new Intl.${intl}`,
|
|
expr,
|
|
undefined,
|
|
lowerer.checker.getSymbolAtLocation(expr.expression),
|
|
);
|
|
const valueReturning = lowerValueReturningConstructorNew(lowerer, expr);
|
|
if (valueReturning) return valueReturning;
|
|
const instanceConstructor = lowerInstanceConstructorNew(lowerer, expr);
|
|
if (instanceConstructor) return instanceConstructor;
|
|
const objectFactory = lowerObjectFactoryNew(lowerer, expr);
|
|
if (objectFactory) return objectFactory;
|
|
if (ts.isIdentifier(expr.expression)) {
|
|
const symbol = lowerer.resolveValueSymbol(expr.expression);
|
|
const declaration =
|
|
symbol &&
|
|
symbol.declarations.find((node) => ts.isFunctionDeclaration(node) && node.body !== undefined);
|
|
if (declaration && ts.isFunctionDeclaration(declaration) && declaration.body) {
|
|
const last = declaration.body.statements.nodes.at(-1);
|
|
if (
|
|
last &&
|
|
ts.isReturnStatement(last) &&
|
|
last.expression &&
|
|
ts.isObjectLiteralExpression(last.expression)
|
|
) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
expr,
|
|
"object-returning constructors that observe this or return mixed primitive/object values",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
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?.nodes ?? [];
|
|
const undefinedArg = (arg: ts.Expression): boolean =>
|
|
(lowerer.typeOf(arg).flags & ts.TypeFlags.Undefined) !== 0;
|
|
// A literal language tag: Unicode grapheme clusters have no locale
|
|
// tailoring. Only tags of the form language[-script][-region] are
|
|
// accepted; each is well-formed, so Node constructs the segmenter
|
|
// without the RangeError a malformed tag throws.
|
|
const literalLocale = (arg: ts.Expression): boolean =>
|
|
ts.isStringLiteralLike(arg) &&
|
|
/^[A-Za-z]{2,3}(-[A-Za-z]{4})?(-([A-Za-z]{2}|[0-9]{3}))?$/.test(arg.text);
|
|
if (args.length > 2 || (args[0] && !undefinedArg(args[0]) && !literalLocale(args[0]))) {
|
|
lowerer.noLowering(
|
|
"Intl.Segmenter locale negotiation",
|
|
expr,
|
|
"omit the locale, pass undefined, or pass one language tag as a string literal",
|
|
);
|
|
}
|
|
// JavaScript dependencies may construct word and sentence segmenters
|
|
// that they use only on some paths (pi-tui's shared word segmenter):
|
|
// the construction compiles and their segment() throws SC2020.
|
|
// TypeScript sources keep the build-time refusal.
|
|
const granularities = isJsSourceFile(ts.getSourceFileOfNode(expr))
|
|
? ["grapheme", "word", "sentence"]
|
|
: ["grapheme"];
|
|
const options = args[1];
|
|
let granularity = "grapheme";
|
|
if (options && !undefinedArg(options)) {
|
|
// An object literal of `granularity` entries (none: the default; a
|
|
// repeated key: the last one, as in JavaScript).
|
|
if (!ts.isObjectLiteralExpression(options))
|
|
lowerer.noLowering(
|
|
"Intl.Segmenter options",
|
|
options,
|
|
'the native segmenter supports default options and { granularity: "grapheme" }; word and sentence segmentation remain unsupported',
|
|
);
|
|
for (const property of options.properties.nodes) {
|
|
if (
|
|
!ts.isPropertyAssignment(property) ||
|
|
!(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ||
|
|
property.name.text !== "granularity" ||
|
|
!ts.isStringLiteral(property.initializer) ||
|
|
!granularities.includes(property.initializer.text)
|
|
) {
|
|
lowerer.noLowering(
|
|
"Intl.Segmenter options",
|
|
options,
|
|
'the native segmenter supports default options and { granularity: "grapheme" }; word and sentence segmentation remain unsupported',
|
|
);
|
|
}
|
|
granularity = property.initializer.text;
|
|
}
|
|
}
|
|
// 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),
|
|
};
|
|
});
|
|
let result: IrExpr = { kind: "libCall", fn: "intl.segmenterNew", args: [], type: DYN, loc };
|
|
// A word or sentence segmenter carries its segment() refusal, which
|
|
// coverage lists as deferred to runtime.
|
|
if (granularity !== "grapheme") {
|
|
const surface = `Intl.Segmenter segmentation with granularity '${granularity}'`;
|
|
lowerer.runtimeFences.push(
|
|
noLoweringDiag(
|
|
surface,
|
|
loc,
|
|
"only grapheme segmenters segment; word and sentence segmenters construct",
|
|
),
|
|
);
|
|
const line = ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(expr), loc.start).line;
|
|
const refusal = `${surface} is not supported yet [SC2020 at ${loc.file}:${line + 1}]`;
|
|
result = {
|
|
kind: "libCall",
|
|
fn: "intl.segmenterRefusing",
|
|
args: [strLit(refusal, loc)],
|
|
type: DYN,
|
|
loc,
|
|
};
|
|
}
|
|
return stmts.length ? { kind: "seqExpr", stmts, result, type: DYN, loc } : result;
|
|
}
|
|
// A package declaration without a compiled constructor cannot provide
|
|
// a runtime value. Report the package at the construction site.
|
|
{
|
|
const pkg = ts.isIdentifier(expr.expression)
|
|
? lowerer.npmPackageOfSymbol(lowerer.resolveValueSymbol(expr.expression) ?? undefined)
|
|
: null;
|
|
if (pkg) {
|
|
lowerer.pushDiag(unsupportedPackageValueDiag(pkg, loc));
|
|
throw new PoisonError();
|
|
}
|
|
}
|
|
|
|
// `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?.nodes ?? []).length > 0) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
expr.arguments!.nodes[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 fsStreamNs = fsStreamClassOf(lowerer, resolved);
|
|
if (fsStreamNs) return lowerFsStreamNew(lowerer, expr, fsStreamNs);
|
|
const streamInfoNs = builtinStreamInfoOf(lowerer, resolved);
|
|
if (streamInfoNs) return lowerStreamNew(lowerer, expr, streamInfoNs);
|
|
// `new zlib.Gzip(options?)` — the zlib stream classes.
|
|
const zlibModeNs = zlibStreamModeOf(lowerer, resolved);
|
|
if (zlibModeNs !== null) {
|
|
const what = `new zlib.${zlibClassName(zlibModeNs)}`;
|
|
return lowerZlibStreamNew(lowerer, expr.arguments?.nodes ?? [], zlibModeNs, what, expr, loc);
|
|
}
|
|
// `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(ts.getSourceFileOfNode(expr), 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(ts.getSourceFileOfNode(expr), 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?.nodes ?? [],
|
|
info.ctorParams,
|
|
loc,
|
|
expr,
|
|
);
|
|
return constructedFromValue(
|
|
lowerer,
|
|
info,
|
|
{
|
|
kind: "newValue",
|
|
callee,
|
|
args,
|
|
type: { kind: "object", className: info.def.name },
|
|
loc,
|
|
},
|
|
expr,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// `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 tls.TLSSocket(socket, options)` — the server-mode wrap of an open
|
|
// socket (lower-server).
|
|
{
|
|
const tlsSocket = lowerTlsSocketNew(lowerer, expr);
|
|
if (tlsSocket) return tlsSocket;
|
|
}
|
|
// `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;
|
|
}
|
|
// `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.
|
|
// It runs before the standard-library constructor fence below, which
|
|
// also sees `const X = require("crypto").X509Certificate` as the
|
|
// builtin class.
|
|
{
|
|
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?.nodes ?? [];
|
|
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));
|
|
return x509RecordCall(lowerer, data, t, loc);
|
|
}
|
|
}
|
|
if (
|
|
ts.isIdentifier(expr.expression) ||
|
|
(ts.isPropertyAccessExpression(expr.expression) &&
|
|
ts.isIdentifier(expr.expression.name) &&
|
|
(lowerer.isStdlibGlobal(expr.expression.expression, "globalThis") ||
|
|
// `new mod.M()` over an exported `const M = Map` or `const E =
|
|
// TypeError`
|
|
isCollectionOrErrorClass(builtinClassRefOf(lowerer, expr.expression))))
|
|
) {
|
|
// `import C = N.C; new C()` — the alias's own source-order guards
|
|
// (a no-op for every non-import= binding).
|
|
if (ts.isIdentifier(expr.expression)) fenceEarlyAliasUse(lowerer, expr.expression, expr);
|
|
const identifier = ts.isIdentifier(expr.expression) ? expr.expression : expr.expression.name;
|
|
// `const SD = require("string_decoder").StringDecoder; new SD()`
|
|
// constructs the member, as a named import of it does.
|
|
const symbol = builtinMemberBindingTarget(
|
|
lowerer,
|
|
ts.isIdentifier(identifier) ? lowerer.resolveValueSymbol(identifier) : null,
|
|
);
|
|
if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Array")) {
|
|
return lowerArrayConstructor(lowerer, expr, expr.arguments?.nodes ?? []);
|
|
}
|
|
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?.nodes ?? [];
|
|
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" || 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,
|
|
};
|
|
}
|
|
// `new Assert(...)` that is not a static instance (assert-instances.ts):
|
|
// the instance would be a run-time value.
|
|
if (isAssertClassNew(lowerer, expr)) {
|
|
lowerer.noLowering(
|
|
"assert.Assert instances used as values",
|
|
expr,
|
|
"bind `new Assert(...)` with literal options to a const and only call methods on it (`const a = new Assert({ diff: 'full' }); a.strictEqual(x, y)`)",
|
|
);
|
|
}
|
|
if (errInfo && errInfo.def.name === "%AssertionError") {
|
|
// new AssertionError(options): Node validates the options object and
|
|
// generates the message from operator, actual and expected.
|
|
const args = expr.arguments?.nodes ?? [];
|
|
if (args.length > 1 || args.some(ts.isSpreadElement)) {
|
|
lowerer.noLowering(`new AssertionError with this argument list`, expr);
|
|
}
|
|
return {
|
|
kind: "libCall",
|
|
fn: "assert.errorNew",
|
|
args: [args[0] ? lowerer.lowerExprExpecting(args[0], DYN) : dynUndefinedExpr(loc)],
|
|
type: { kind: "object", className: "%AssertionError" },
|
|
loc,
|
|
};
|
|
}
|
|
if (errInfo && errInfo.def.name === "%SuppressedError") {
|
|
const args = expr.arguments?.nodes ?? [];
|
|
if (args.length > 3 || args.some(ts.isSpreadElement)) {
|
|
lowerer.noLowering(`new SuppressedError with this argument list`, expr);
|
|
}
|
|
return {
|
|
kind: "libCall",
|
|
fn: "error.newSuppressed",
|
|
args: [0, 1, 2].map((index) =>
|
|
args[index] ? lowerer.lowerExprExpecting(args[index], DYN) : dynUndefinedExpr(loc),
|
|
),
|
|
type: { kind: "object", className: "%SuppressedError" },
|
|
loc,
|
|
};
|
|
}
|
|
if (errInfo && errInfo.def.name === "%AggregateError") {
|
|
const args = expr.arguments?.nodes ?? [];
|
|
if (args.length > 3 || args.some(ts.isSpreadElement)) {
|
|
lowerer.noLowering(`new AggregateError with this argument list`, expr);
|
|
}
|
|
const errors = args[0] ? lowerer.lowerExprExpecting(args[0], DYN) : dynUndefinedExpr(loc);
|
|
return {
|
|
kind: "call",
|
|
callee: aggregateErrorConstructor(lowerer, loc),
|
|
args: [errors, ...lowerer.errorConstructorArgs(args.slice(1), loc, expr)],
|
|
type: { kind: "object", className: "%AggregateError" },
|
|
loc,
|
|
};
|
|
}
|
|
if (errInfo) {
|
|
const args = lowerer.errorConstructorArgs(expr.arguments?.nodes ?? [], 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?.nodes ?? []).length > 0) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
expr.arguments!.nodes[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 fsStream = fsStreamClassOf(lowerer, symbol);
|
|
if (fsStream) return lowerFsStreamNew(lowerer, expr, fsStream);
|
|
const streamInfo = builtinStreamInfoOf(lowerer, symbol);
|
|
if (streamInfo) return lowerStreamNew(lowerer, expr, streamInfo);
|
|
const zlibMode = zlibStreamModeOf(lowerer, symbol);
|
|
if (zlibMode !== null) {
|
|
const what = `new zlib.${zlibClassName(zlibMode)}`;
|
|
return lowerZlibStreamNew(lowerer, expr.arguments?.nodes ?? [], zlibMode, what, expr, loc);
|
|
}
|
|
// `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 && ts.symbolName(symbol) === "RegExp" && lowerer.isStdlibSymbol(symbol)) {
|
|
const args = expr.arguments?.nodes ?? [];
|
|
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)
|
|
: dynUndefinedExpr(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");
|
|
// Node accepts the 'd' flag the engine does not have; a literal
|
|
// flags string refuses here like the literal regex form.
|
|
if (flags.kind === "strLit" && args[1] !== undefined && flags.value.includes("d")) {
|
|
lowerer.unsupported("SC1120", args[1], "the regex 'd' flag (match indices)");
|
|
}
|
|
return {
|
|
kind: "libCall",
|
|
fn: "regex.new",
|
|
args: [pattern, flags],
|
|
type: { kind: "regex" },
|
|
loc,
|
|
};
|
|
}
|
|
if (symbol && ts.symbolName(symbol) === "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 && ts.symbolName(symbol) === "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 && ts.symbolName(symbol) === "Date" && lowerer.isStdlibSymbol(symbol)) {
|
|
const args = expr.arguments?.nodes ?? [];
|
|
if (args.some(ts.isSpreadElement)) {
|
|
lowerer.noLowering(
|
|
`new Date with spread arguments`,
|
|
expr,
|
|
"new Date(), new Date(milliseconds), new Date(dateString), and new Date(year, monthIndex, ...) with listed arguments are supported",
|
|
symbol,
|
|
);
|
|
}
|
|
// new Date(year, monthIndex[, date[, hours[, minutes[, seconds[, ms]]]]]):
|
|
// local-time fields, converted to UTC in the host zone at run time.
|
|
// JavaScript callers pass their arguments to the runtime's ToNumber
|
|
// (in order, exactly the constructor's coercions); typed callers
|
|
// pass numbers, completed with the spec's defaults (date 1, time
|
|
// parts 0) for the seven-f64 ABI.
|
|
if (args.length > 1) {
|
|
if (isJsSourceFile(ts.getSourceFileOfNode(expr))) {
|
|
return {
|
|
kind: "libCall",
|
|
fn: "date.nativeNewFields",
|
|
args: [
|
|
{
|
|
kind: "dynArrLit",
|
|
elems: args.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
|
|
type: DYN,
|
|
loc,
|
|
},
|
|
],
|
|
type: DYN,
|
|
loc,
|
|
};
|
|
}
|
|
if (args.length > 7) {
|
|
lowerer.noLowering(`new Date with ${args.length} arguments`, expr, undefined, symbol);
|
|
}
|
|
const defaults = [0, 0, 1, 0, 0, 0, 0];
|
|
const fields: IrExpr[] = [];
|
|
for (let i = 0; i < 7; i++) {
|
|
const arg = args[i];
|
|
fields.push(
|
|
arg !== undefined
|
|
? lowerer.lowerExprExpecting(arg, F64)
|
|
: { kind: "numLit", value: defaults[i]!, type: F64, loc },
|
|
);
|
|
}
|
|
return { kind: "libCall", fn: "date.newLocal", args: fields, type: DATE_T, loc };
|
|
}
|
|
if (isJsSourceFile(ts.getSourceFileOfNode(expr))) {
|
|
return {
|
|
kind: "libCall",
|
|
// One argument: a string parses with the full date parser.
|
|
fn: args.length === 1 ? "date.nativeNewValue" : "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 };
|
|
}
|
|
// new Date(date): a new Date object with the same time value.
|
|
if (arg.type.kind === "date") {
|
|
const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [arg], type: F64, loc };
|
|
return { kind: "libCall", fn: "date.newMs", args: [time], type: DATE_T, loc };
|
|
}
|
|
// A union (`Date | string | number`) or checked-dynamic value: the
|
|
// runtime's constructor reads a Date's time value, parses a string
|
|
// and converts anything else to a primitive and a number.
|
|
if (
|
|
(arg.type.kind === "union" || arg.type.kind === "dyn") &&
|
|
lowerer.dynConvertible(arg.type)
|
|
) {
|
|
const value: IrExpr = {
|
|
kind: "libCall",
|
|
fn: "date.nativeNewValue",
|
|
args: [
|
|
{
|
|
kind: "dynArrLit",
|
|
elems: [lowerer.coerceToExpected(arg, DYN)],
|
|
type: DYN,
|
|
loc,
|
|
},
|
|
],
|
|
type: DYN,
|
|
loc,
|
|
};
|
|
return { kind: "libCall", fn: "date.checkedValue", args: [value], type: DATE_T, loc };
|
|
}
|
|
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 (exactly what
|
|
// `.encoding` answers in Node) and the packed-f64 pending state
|
|
// starting at 0 (nothing buffered). A literal Buffer encoding folds
|
|
// its alias at compile time; any other encoding argument is
|
|
// normalized at run time (Node's normalizeEncoding, which throws
|
|
// ERR_UNKNOWN_ENCODING for an unknown name). Omitted means utf8,
|
|
// Node's default.
|
|
if (symbol && ts.symbolName(symbol) === "StringDecoder" && lowerer.isStdlibSymbol(symbol)) {
|
|
const args = expr.arguments?.nodes ?? [];
|
|
if (args.length > 1) {
|
|
lowerer.noLowering("new StringDecoder with 2 arguments", expr, undefined, symbol);
|
|
}
|
|
const encNode = args[0];
|
|
const encT = encNode ? lowerer.typeOf(encNode) : undefined;
|
|
const encName = !encNode
|
|
? "utf8"
|
|
: encT!.isStringLiteralType()
|
|
? knownBufEncoding(encT!.value)
|
|
: undefined;
|
|
const encoding: IrExpr =
|
|
encName !== undefined
|
|
? { kind: "strLit", value: encName, type: STRING, loc }
|
|
: {
|
|
kind: "libCall",
|
|
fn: "strdec.encoding",
|
|
args: [lowerer.lowerExprExpecting(encNode!, DYN)],
|
|
type: STRING,
|
|
loc,
|
|
};
|
|
const decT = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
|
if (decT?.kind !== "record") lowerer.badType(expr, lowerer.typeOf(expr));
|
|
return {
|
|
kind: "recordLit",
|
|
fields: [
|
|
{ name: "%enc", value: encoding },
|
|
{ name: "%pending", value: { kind: "numLit", value: 0, type: F64, loc } },
|
|
],
|
|
type: decT,
|
|
loc,
|
|
};
|
|
}
|
|
const codec = symbol ? ts.symbolName(symbol) : undefined;
|
|
if (
|
|
symbol &&
|
|
(codec === "TextDecoder" || codec === "TextEncoder") &&
|
|
lowerer.isStdlibSymbol(symbol)
|
|
) {
|
|
return lowerTextCodecNew(lowerer, expr, codec);
|
|
}
|
|
// `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.
|
|
if (symbol !== null && ts.symbolName(symbol) === "Object" && lowerer.isStdlibSymbol(symbol)) {
|
|
const object = lowerObjectConstructor(lowerer, expr.arguments?.nodes ?? [], loc);
|
|
if (object) return object;
|
|
}
|
|
if (symbol !== null && ts.symbolName(symbol) === "Array" && lowerer.isStdlibSymbol(symbol)) {
|
|
return lowerArrayConstructor(lowerer, expr, expr.arguments?.nodes ?? []);
|
|
}
|
|
// A const alias (`const M = Map`) constructs the same Map.
|
|
if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Map")) {
|
|
const seedArg = (expr.arguments?.nodes.length ?? 0) === 1 ? expr.arguments!.nodes[0]! : null;
|
|
const isPairLit = (el: ts.Expression): el is ts.ArrayLiteralExpression =>
|
|
ts.isArrayLiteralExpression(el) &&
|
|
el.elements.nodes.length === 2 &&
|
|
!el.elements.nodes.some(ts.isSpreadElement);
|
|
const entriesLit =
|
|
seedArg && ts.isArrayLiteralExpression(seedArg) && seedArg.elements.nodes.every(isPairLit)
|
|
? seedArg.elements.nodes.filter(isPairLit)
|
|
: null;
|
|
let tsType = lowerer.typeOf(expr);
|
|
let mapped = lowerer.mapTypeOf(tsType);
|
|
if (mapped === null && isJsSourceFile(ts.getSourceFileOfNode(expr)))
|
|
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?.nodes.length ?? 0) === 0 &&
|
|
(entriesLit !== null || (expr.arguments?.nodes.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.
|
|
const inferred = initializedCollectionType(lowerer, expr);
|
|
if (inferred) {
|
|
tsType = inferred;
|
|
mapped = lowerer.mapTypeOf(inferred);
|
|
} else 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?.nodes.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 collection or collection iterator, 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.nodes[0]!, mapped.key),
|
|
value:
|
|
mapped.value.kind === "dyn"
|
|
? lowerer.lowerCollectionKey(pair.elements.nodes[1]!, mapped.value)
|
|
: lowerer.lowerExprExpecting(pair.elements.nodes[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(ts.getSourceFileOfNode(expr)) &&
|
|
(expr.arguments?.nodes.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, booleans, bigints, identity references, or supported unions)`,
|
|
);
|
|
}
|
|
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, functions, ` +
|
|
`typed arrays, arrays, promises, Dates, Maps, Sets, or supported unions and checked values)`,
|
|
);
|
|
}
|
|
lowerer.badType(expr, tsType);
|
|
}
|
|
// Weak containers share checked native key identities and lifetime hooks.
|
|
if (
|
|
((symbol !== null && ts.symbolName(symbol) === "WeakMap") ||
|
|
(symbol !== null && ts.symbolName(symbol) === "WeakSet")) &&
|
|
lowerer.isStdlibSymbol(symbol)
|
|
) {
|
|
const args = expr.arguments?.nodes ?? [];
|
|
if (args.length > 1 || args.some(ts.isSpreadElement)) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
expr,
|
|
`${ts.symbolName(symbol)} construction with surplus or spread arguments`,
|
|
);
|
|
}
|
|
|
|
return {
|
|
kind: "libCall",
|
|
fn: ts.symbolName(symbol) === "WeakMap" ? "weakMap.new" : "weakSet.new",
|
|
args: [args[0] ? lowerer.lowerExprExpecting(args[0], DYN) : dynUndefinedExpr(loc)],
|
|
type: DYN,
|
|
loc,
|
|
};
|
|
}
|
|
// A FinalizationRegistry construction: register and unregister keep
|
|
// their bookkeeping, and the cleanup callback never runs, which
|
|
// ECMAScript allows (scr_finalization_registry_new).
|
|
if (
|
|
symbol !== null &&
|
|
ts.symbolName(symbol) === "FinalizationRegistry" &&
|
|
lowerer.isStdlibSymbol(symbol)
|
|
) {
|
|
const args = expr.arguments?.nodes ?? [];
|
|
if (args.some(ts.isSpreadElement)) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
expr,
|
|
"FinalizationRegistry construction with spread arguments",
|
|
);
|
|
}
|
|
const cleanup = args[0] ? lowerer.lowerExprExpecting(args[0], DYN) : dynUndefinedExpr(loc);
|
|
const surplus = args.slice(1).map((arg) => lowerer.lowerExpr(arg));
|
|
const created: IrExpr = {
|
|
kind: "libCall",
|
|
fn: "finalizationRegistry.new",
|
|
args: [cleanup],
|
|
type: DYN,
|
|
loc,
|
|
};
|
|
if (surplus.length === 0) return created;
|
|
// Surplus arguments evaluate after the cleanup argument and are ignored.
|
|
const local = lowerer.declareHiddenLocal("%finalizationCleanup", DYN);
|
|
return {
|
|
kind: "seqExpr",
|
|
stmts: [
|
|
{ kind: "varDecl", localId: local.id, init: cleanup, loc },
|
|
...surplus.map((value): IrStmt => ({ kind: "exprStmt", expr: value, loc })),
|
|
],
|
|
result: { ...created, args: [varRef(local.id, DYN, loc)] },
|
|
type: DYN,
|
|
loc,
|
|
};
|
|
}
|
|
// `new Set<T>()`: construct + bulk add from supported values,
|
|
// including live collection cursors. Duplicates collapse in insertion order.
|
|
if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Set")) {
|
|
const inferred = initializedCollectionType(lowerer, expr);
|
|
const tsType = inferred ?? 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?.nodes.length ?? 0) === 1) {
|
|
const seeded = lowerSetSeedNew(lowerer, expr.arguments!.nodes[0]!, mapped);
|
|
if (seeded) return seeded;
|
|
}
|
|
if (
|
|
!mapped &&
|
|
isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
|
|
(expr.arguments?.nodes.length ?? 0) <= 1 &&
|
|
!expr.arguments?.nodes.some(ts.isSpreadElement)
|
|
) {
|
|
return {
|
|
kind: "libCall",
|
|
fn: "dyn.nativeSetNew",
|
|
args: [
|
|
expr.arguments?.nodes[0]
|
|
? lowerer.lowerExprExpecting(expr.arguments.nodes[0], DYN)
|
|
: dynUndefinedExpr(loc),
|
|
],
|
|
type: DYN,
|
|
loc,
|
|
};
|
|
}
|
|
// 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(ts.getSourceFileOfNode(expr)) &&
|
|
(expr.arguments?.nodes.length ?? 0) === 1 &&
|
|
ts.isArrayLiteralExpression(expr.arguments!.nodes[0]!) &&
|
|
!(expr.arguments!.nodes[0] as ts.ArrayLiteralExpression).elements.nodes.some(
|
|
ts.isSpreadElement,
|
|
)
|
|
) {
|
|
const lit = expr.arguments!.nodes[0] as ts.ArrayLiteralExpression;
|
|
const elems = lit.elements.nodes.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?.nodes.length ?? 0) > 0) {
|
|
lowerer.noLowering(
|
|
"new Set(values)",
|
|
expr,
|
|
"construct the Set empty and add() each value — only nullish values or a matching Set, collection iterator, string, array, or fixed tuple of " +
|
|
"supported key values 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, booleans, bigints, identity references, or supported unions)`,
|
|
);
|
|
}
|
|
lowerer.badType(expr, tsType);
|
|
}
|
|
// `new AsyncLocalStorage()` (node:async_hooks): a fresh store id —
|
|
// an f64 handle (type-mapper.ts), the Channel story. The options form
|
|
// ({ defaultValue, name }) passes the options object as a dyn value.
|
|
if (
|
|
symbol !== null &&
|
|
ts.symbolName(symbol) === "AsyncLocalStorage" &&
|
|
lowerer.isStdlibSymbol(symbol)
|
|
) {
|
|
const options = expr.arguments?.nodes[0];
|
|
if (options === undefined)
|
|
return { kind: "libCall", fn: "als.new", args: [], type: F64, loc };
|
|
return {
|
|
kind: "libCall",
|
|
fn: "als.newOptions",
|
|
args: [lowerAsyncLocalStorageOptions(lowerer, options)],
|
|
type: F64,
|
|
loc,
|
|
};
|
|
}
|
|
// `new Promise<T>((resolve) => ...)`: the ambient Promise constructor.
|
|
if (symbol !== null && ts.symbolName(symbol) === "Promise" && lowerer.isStdlibSymbol(symbol)) {
|
|
const type = lowerer.irTypeOf(expr);
|
|
if (type.kind !== "promise") lowerer.badType(expr, lowerer.typeOf(expr));
|
|
const args = expr.arguments?.nodes ?? [];
|
|
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 ? sym.declarations : [];
|
|
if (
|
|
decls.length > 0 &&
|
|
decls.every((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d)))
|
|
) {
|
|
// 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.
|
|
let executor = lowerer.lowerExpr(args[0]!);
|
|
if (executor.type.kind !== "func") lowerer.badType(args[0]!, lowerer.typeOf(args[0]!));
|
|
executor = adaptPromiseExecutor(lowerer, executor, type.inner, loc) ?? executor;
|
|
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)) {
|
|
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",
|
|
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[
|
|
ts.isIdentifier(expr.expression) ? expr.expression.text : expr.expression.name.text
|
|
],
|
|
symbol,
|
|
);
|
|
}
|
|
}
|
|
// `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);
|
|
}
|
|
// 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, unless the flush
|
|
// registers it with dynamic slots) and poison this construction
|
|
// site, never an ICE.
|
|
if (!info) info = lowerer.flushDeferredClass(calleeT.className);
|
|
if (!info) {
|
|
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>')",
|
|
);
|
|
}
|
|
let callee = lowerer.lowerExpr(expr.expression);
|
|
if (callee.type.kind === "dyn") return lowerCheckedConstructor(lowerer, expr, callee);
|
|
/** The union-typed element read a narrowing check guards, and the
|
|
* check (`%new.callee`) that yields the class value or throws. */
|
|
let narrowedCallee: NarrowedConstructorRead | null = null;
|
|
// An array element read (`new T[0]()` over `[Dog, Animal]`, whose
|
|
// checker type is the common base) keeps the undefined an index past
|
|
// the end reads: the class value is narrowed out with a check, which
|
|
// throws a TypeError for undefined.
|
|
if (callee.type.kind === "union") {
|
|
const arms = lowerer.unions.get(callee.type.unionId)?.arms ?? [];
|
|
const classArms = arms.filter((arm) => arm.kind === "classval");
|
|
if (
|
|
classArms.length === 1 &&
|
|
classArms[0]!.kind === "classval" &&
|
|
classArms[0]!.className === calleeT.className &&
|
|
arms.every((arm) => arm.kind === "classval" || arm.kind === "undefinedT")
|
|
) {
|
|
const unionT = callee.type;
|
|
const helper = lowerer.narrowedArmHelper(unionT.unionId, calleeT, callee.loc);
|
|
if (helper) {
|
|
const loc = callee.loc;
|
|
const name = `%new.callee:${unionT.unionId}`;
|
|
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
|
|
const undefinedTag = lowerer.armTag(unionT.unionId, UNDEFINED_T);
|
|
const value: IrExpr = { kind: "varRef", localId: "value", type: unionT, loc };
|
|
lowerer.liftedFns.push({
|
|
name,
|
|
params: [
|
|
{ localId: "value", name: "value", type: unionT },
|
|
{ localId: "message", name: "message", type: STRING },
|
|
],
|
|
locals: [
|
|
{ id: "value", name: "value", type: unionT, mutable: false },
|
|
{ id: "message", name: "message", type: STRING, mutable: false },
|
|
],
|
|
returnType: calleeT,
|
|
body: [
|
|
...(undefinedTag >= 0
|
|
? [
|
|
{
|
|
kind: "if",
|
|
cond: {
|
|
kind: "unionIsTag",
|
|
unionId: unionT.unionId,
|
|
tag: undefinedTag,
|
|
negated: false,
|
|
value,
|
|
type: BOOL,
|
|
loc,
|
|
},
|
|
then: [
|
|
{
|
|
kind: "throw",
|
|
value: {
|
|
kind: "libCall",
|
|
fn: "error.new",
|
|
args: [{ kind: "varRef", localId: "message", type: STRING, loc }],
|
|
type: { kind: "object", className: "%TypeError" },
|
|
loc,
|
|
},
|
|
loc,
|
|
},
|
|
],
|
|
else_: null,
|
|
loc,
|
|
} satisfies IrStmt,
|
|
]
|
|
: []),
|
|
{
|
|
kind: "return",
|
|
value: lowerer.checkedNarrowCall(helper, value, calleeT, loc),
|
|
loc,
|
|
},
|
|
],
|
|
loc,
|
|
});
|
|
}
|
|
// JavaScript evaluates the arguments before it checks that the
|
|
// callee constructs, so the check runs at the construction
|
|
// (checkedElementConstruction).
|
|
narrowedCallee = {
|
|
read: callee,
|
|
check: name,
|
|
unionT,
|
|
calleeT,
|
|
message: {
|
|
kind: "strLit",
|
|
value: `${expr.expression.getText()} is not a constructor`,
|
|
type: STRING,
|
|
loc,
|
|
},
|
|
};
|
|
callee = {
|
|
kind: "call",
|
|
callee: name,
|
|
args: [callee, narrowedCallee.message],
|
|
type: calleeT,
|
|
loc,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
if (callee.type.kind !== "classval")
|
|
lowerer.badType(expr.expression, lowerer.typeOf(expr.expression));
|
|
lowerer.noteEdge(classValueConstructor(info.def).fn);
|
|
// 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(classValueConstructor(s.def).fn);
|
|
below(s);
|
|
}
|
|
};
|
|
below(info);
|
|
// EventEmitter and stream class values construct without options
|
|
// only: a slot of either may hold any of those classes, and the
|
|
// direct `new Readable({...})` lowering reads its options at compile
|
|
// time (EventEmitter's options have no lowering at all).
|
|
if (
|
|
(info.builtinStream !== undefined || info.builtinEmitter) &&
|
|
(expr.arguments?.nodes.length ?? 0) > 0
|
|
) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
expr,
|
|
`EventEmitter and stream class values constructed with options (construct with 'new ${info.def.name.slice(1)}({ ... })' directly)`,
|
|
);
|
|
}
|
|
// A program class whose constructor is the inherited runtime one
|
|
// takes no arguments through its class value either.
|
|
refuseInheritedBuiltinCtorArgs(lowerer, info, expr);
|
|
defineClassValueConstruct(lowerer, info);
|
|
const args = lowerer.completeArgs(
|
|
expr.arguments?.nodes ?? [],
|
|
classValueParams(info),
|
|
loc,
|
|
expr,
|
|
);
|
|
const built = constructedFromValue(
|
|
lowerer,
|
|
info,
|
|
narrowedCallee !== null
|
|
? checkedElementConstruction(lowerer, info, narrowedCallee, args, loc)
|
|
: {
|
|
kind: "newValue",
|
|
callee,
|
|
args,
|
|
type: { kind: "object", className: info.def.name },
|
|
loc,
|
|
},
|
|
expr,
|
|
);
|
|
// A runtime class slot widened from a union (`[Readable, Writable]`'s
|
|
// elements hold EventEmitter values): the checker types the result as
|
|
// the union of the arms' instances, which the checked class view
|
|
// answers from the instance's class.
|
|
if (info.def.runtime) {
|
|
const resultT = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
|
if (resultT?.kind === "union") {
|
|
const asserted = checkedClassUnionAssertion(lowerer, built, resultT, loc);
|
|
if (asserted) return asserted;
|
|
}
|
|
}
|
|
return built;
|
|
}
|
|
if (calleeT?.kind === "union") {
|
|
const def = lowerer.unions.get(calleeT.unionId);
|
|
if (def?.arms.some((a) => a.kind === "classval")) {
|
|
if (
|
|
def.arms.every(
|
|
(arm) =>
|
|
arm.kind === "classval" ||
|
|
(isJsSourceFile(ts.getSourceFileOfNode(expr)) && isUnitType(arm)),
|
|
) &&
|
|
lowerer.dynConvertible(calleeT)
|
|
) {
|
|
const callee = lowerer.lowerExprExpecting(expr.expression, DYN);
|
|
return lowerCheckedConstructor(lowerer, expr, callee);
|
|
}
|
|
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, ts.symbolName(sym)) : 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 &&
|
|
(callee.type.kind === "dyn" ||
|
|
((callee.type.kind === "func" || callee.type.kind === "classval") &&
|
|
lowerer.dynConvertible(callee.type)))
|
|
) {
|
|
let boxed = lowerer.coerceToExpected(callee, DYN);
|
|
// A plain function's box carries its name: the construction's
|
|
// prototype records it as `constructor`, which util.inspect prints
|
|
// (`F { a: 2 }`).
|
|
if (boxed.kind === "dynFrom" && boxed.fnName === undefined && callee.type.kind === "func") {
|
|
const name = jsFuncNameOf(expr.expression, lowerer);
|
|
if (name !== null) boxed = { ...boxed, fnName: name };
|
|
}
|
|
return lowerCheckedConstructor(lowerer, expr, boxed);
|
|
}
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
expr,
|
|
"constructing values other than classes declared in the program",
|
|
);
|
|
}
|
|
|
|
function lowerCheckedConstructor(lowerer: Lowerer, expr: ts.NewExpression, callee: IrExpr): IrExpr {
|
|
const loc = locOf(expr);
|
|
const args = expr.arguments?.nodes ?? [];
|
|
if (args.some(ts.isSpreadElement)) {
|
|
const packed = lowerer.completeArgs(args, [{ type: DYN, mode: "dynRest" }], loc, expr)[0]!;
|
|
return checkedClassConstructionPacked(lowerer, callee, packed, loc);
|
|
}
|
|
return checkedClassConstruction(
|
|
lowerer,
|
|
callee,
|
|
args.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
|
|
loc,
|
|
);
|
|
}
|
|
|
|
/** 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;
|
|
}
|
|
|
|
/** The lifted `new crypto.X509Certificate(d)` constructor: raw decodes the
|
|
* input once (Node's PEM error throws here), and every other field reads
|
|
* raw. subjectAltName, infoAccess and signatureAlgorithm hold undefined
|
|
* where the runtime answers "". */
|
|
/** The X509Certificate record `t` built from `data`, a certificate Buffer
|
|
* (PEM or DER) or PEM string, through the interned constructor helper.
|
|
* `der` marks a Buffer known to hold a certificate's DER (a TLS socket's),
|
|
* which is read without looking for PEM armor inside it. */
|
|
export function x509RecordCall(
|
|
lowerer: Lowerer,
|
|
data: IrExpr,
|
|
t: IrType & { kind: "record" },
|
|
loc: SrcLoc,
|
|
der = false,
|
|
): IrExpr {
|
|
const isBytes = data.type.kind === "bytes" && data.type.elem === "u8";
|
|
const key = `x509.record:${der ? "der" : isBytes ? "bytes" : "str"}`;
|
|
let helper = lowerer.valueHelpers.get(key);
|
|
if (!helper) {
|
|
helper = keyedHelperName(lowerer, "%x509.record", key);
|
|
lowerer.valueHelpers.set(key, helper);
|
|
lowerer.liftedFns.push(x509RecordHelper(lowerer, helper, data.type, t, isBytes, der, loc));
|
|
}
|
|
return { kind: "call", callee: helper, args: [data], type: t, loc };
|
|
}
|
|
|
|
function x509RecordHelper(
|
|
lowerer: Lowerer,
|
|
name: string,
|
|
dataT: IrType,
|
|
t: IrType & { kind: "record" },
|
|
isBytes: boolean,
|
|
der: boolean,
|
|
loc: SrcLoc,
|
|
): IrFunction {
|
|
const shape = lowerer.shapes.get(t.shapeId);
|
|
if (!shape) throw new InternalCompilerError("lowerer bug: X509Certificate shape unregistered");
|
|
const raw: IrExpr = { kind: "varRef", localId: "raw.0", type: BYTES_U8, loc };
|
|
const call = (fn: IrLibFn, type: IrType): IrExpr => ({
|
|
kind: "libCall",
|
|
fn,
|
|
args: [raw],
|
|
type,
|
|
loc,
|
|
});
|
|
const locals: IrLocal[] = [
|
|
{ id: "d.0", name: "d", type: dataT, mutable: false },
|
|
{ id: "raw.0", name: "raw", type: BYTES_U8, mutable: false },
|
|
];
|
|
const decls: IrStmt[] = [
|
|
{
|
|
kind: "varDecl",
|
|
localId: "raw.0",
|
|
init: {
|
|
kind: "libCall",
|
|
fn: der ? "crypto.x509RawDer" : isBytes ? "crypto.x509Raw" : "crypto.x509RawStr",
|
|
args: [{ kind: "varRef", localId: "d.0", type: dataT, loc }],
|
|
type: BYTES_U8,
|
|
loc,
|
|
},
|
|
loc,
|
|
},
|
|
];
|
|
const validTime = (fn: IrLibFn): IrExpr => ({
|
|
kind: "libCall",
|
|
fn: "date.newString",
|
|
args: [call(fn, STRING)],
|
|
type: DATE_T,
|
|
loc,
|
|
});
|
|
const values: Record<string, (type: IrType) => IrExpr> = {
|
|
ca: () => call("crypto.x509Ca", BOOL),
|
|
fingerprint: () => call("crypto.x509Fingerprint", STRING),
|
|
fingerprint256: () => call("crypto.x509Fingerprint256", STRING),
|
|
fingerprint512: () => call("crypto.x509Fingerprint512", STRING),
|
|
subject: () => call("crypto.x509Subject", STRING),
|
|
subjectAltName: (type) => optionalText("san", "crypto.x509SubjectAltName", type),
|
|
infoAccess: (type) => optionalText("access", "crypto.x509InfoAccess", type),
|
|
issuer: () => call("crypto.x509Issuer", STRING),
|
|
raw: () => raw,
|
|
serialNumber: () => call("crypto.x509SerialNumber", STRING),
|
|
signatureAlgorithm: (type) => optionalText("sigalg", "crypto.x509SignatureAlgorithm", type),
|
|
signatureAlgorithmOid: () => call("crypto.x509SignatureAlgorithmOid", STRING),
|
|
validFrom: () => call("crypto.x509ValidFrom", STRING),
|
|
validFromDate: () => validTime("crypto.x509ValidFrom"),
|
|
validTo: () => call("crypto.x509ValidTo", STRING),
|
|
validToDate: () => validTime("crypto.x509ValidTo"),
|
|
[X509_PUBLIC_KEY_FIELD]: () => ({
|
|
kind: "dynFrom",
|
|
value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
|
|
type: DYN,
|
|
loc,
|
|
}),
|
|
// A copy: writes into `raw` must not reach the methods.
|
|
[X509_DER_FIELD]: () => call("crypto.x509RawDer", BYTES_U8),
|
|
};
|
|
/** A string | undefined field: the runtime's text, or undefined for "". */
|
|
function optionalText(local: string, fn: IrLibFn, type: IrType): IrExpr {
|
|
if (type.kind !== "union")
|
|
throw new InternalCompilerError("lowerer bug: X509Certificate optional field type");
|
|
const def = lowerer.unions.get(type.unionId);
|
|
const stringTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1;
|
|
const undefinedTag = def ? def.arms.findIndex((a) => a.kind === "undefinedT") : -1;
|
|
if (stringTag < 0 || undefinedTag < 0)
|
|
throw new InternalCompilerError("lowerer bug: X509Certificate optional field arms");
|
|
const id = `${local}.0`;
|
|
locals.push({ id, name: local, type: STRING, mutable: false });
|
|
decls.push({ kind: "varDecl", localId: id, init: call(fn, STRING), loc });
|
|
const text: IrExpr = { kind: "varRef", localId: id, type: STRING, loc };
|
|
return {
|
|
kind: "ternary",
|
|
cond: {
|
|
kind: "strEq",
|
|
negated: false,
|
|
left: text,
|
|
right: { kind: "strLit", value: "", type: STRING, loc },
|
|
type: BOOL,
|
|
loc,
|
|
},
|
|
then: {
|
|
kind: "unionWrap",
|
|
unionId: type.unionId,
|
|
tag: undefinedTag,
|
|
value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
|
|
type,
|
|
loc,
|
|
},
|
|
else_: { kind: "unionWrap", unionId: type.unionId, tag: stringTag, value: text, type, loc },
|
|
type,
|
|
loc,
|
|
};
|
|
}
|
|
const fields = shape.fields.map((f) => {
|
|
const value = values[f.name];
|
|
if (!value) throw new InternalCompilerError(`lowerer bug: X509Certificate field ${f.name}`);
|
|
return { name: f.name, value: value(f.type) };
|
|
});
|
|
return {
|
|
name,
|
|
params: [{ localId: "d.0", name: "d", type: dataT }],
|
|
returnType: t,
|
|
locals,
|
|
body: [...decls, { kind: "return", value: { kind: "recordLit", fields, type: t, loc }, loc }],
|
|
loc,
|
|
};
|
|
}
|
|
|
|
/** Whether a JavaScript class's own binding is the target of
|
|
* `Object.defineProperty`/`defineProperties` or `Reflect.deleteProperty` in
|
|
* its file (`Object.defineProperty(Definition, "name", { value })`,
|
|
* `Reflect.deleteProperty(Response, "getResponseState")`): its constructor
|
|
* then needs a live own-property table. */
|
|
function definedThroughReflection(lowerer: Lowerer, decl: ts.ClassLikeDeclaration): boolean {
|
|
const name = decl.name;
|
|
const sf = ts.getSourceFileOfNode(decl);
|
|
if (
|
|
name === undefined ||
|
|
(!sf.text.includes("definePropert") && !sf.text.includes("deleteProperty"))
|
|
)
|
|
return false;
|
|
const symbol = lowerer.checker.getSymbolAtLocation(name);
|
|
if (symbol === undefined) return false;
|
|
let found = false;
|
|
ts.walkPreorder(sf, (node) => {
|
|
if (found) return "stop";
|
|
if (
|
|
ts.isCallExpression(node) &&
|
|
ts.isPropertyAccessExpression(node.expression) &&
|
|
(((node.expression.name.text === "defineProperty" ||
|
|
node.expression.name.text === "defineProperties") &&
|
|
lowerer.stdlibGlobalMember(node.expression, "Object") !== null) ||
|
|
(node.expression.name.text === "deleteProperty" &&
|
|
lowerer.stdlibGlobalMember(node.expression, "Reflect") !== null))
|
|
) {
|
|
const target = node.arguments.nodes[0];
|
|
if (
|
|
target !== undefined &&
|
|
ts.isIdentifier(target) &&
|
|
target.text === name.text &&
|
|
lowerer.checker.getSymbolAtLocation(target) === symbol
|
|
)
|
|
found = true;
|
|
}
|
|
return undefined;
|
|
});
|
|
return found;
|
|
}
|
|
|
|
/** The failure group of a class the program declares but did not collect
|
|
* (its own declaration was rejected), or null for any other symbol. */
|
|
function rejectedProgramClassGroup(lowerer: Lowerer, symbol: ts.Symbol | null): string | null {
|
|
if (symbol === null) return null;
|
|
const recorded = lowerer.rejectedGroups.get(symbol);
|
|
if (recorded !== undefined) return recorded;
|
|
const decl = symbol.declarations.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d));
|
|
if (decl === undefined || ts.getSourceFileOfNode(decl).isDeclarationFile) return null;
|
|
return declarationGroupKey(symbol);
|
|
}
|