mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-12 02:13:04 +08:00
TypeScript 7.1 parses the elements of an interface's `extends` and a class's `implements` as TypeReference nodes, whose names are entity names, where 7.0 gives ExpressionWithTypeArguments nodes. heritage-elements.ts recognizes both forms, and the readers that took an element's expression read either: - contract-types.ts finds `implements ProgramLifetime` and `implements Acyclic` written as type references; - narrowing-stability.ts takes a qualified name as it takes a property access; - generic-functions.ts reads an element's name in either form, and keeps counting a name in a heritage clause as a value reference.
3274 lines
138 KiB
TypeScript
3274 lines
138 KiB
TypeScript
import { shapeOverlayAnnotated } from "../shape-inference/index.js";
|
|
import { documentedParameter } from "./call-signatures.js";
|
|
import { staticNameBindings } from "./static-function-names.js";
|
|
import { positionTag } from "./structural-positions.js";
|
|
import { keyedHelperName } from "./stable-names.js";
|
|
import * as ts from "../ts7/adapter.js";
|
|
import { heritageElementTarget, isHeritageTypeReference } from "../heritage-elements.js";
|
|
import { bodyReadsArguments } from "../arguments-usage.js";
|
|
import type { Lowerer } from "./lowerer.js";
|
|
import {
|
|
BOOL,
|
|
DYN,
|
|
type IrExpr,
|
|
type IrFunction,
|
|
type IrParam,
|
|
type IrStmt,
|
|
type IrType,
|
|
type SrcLoc,
|
|
STRING,
|
|
VOID,
|
|
isUnitType,
|
|
typeEquals,
|
|
} from "../../ir/ir.js";
|
|
import { helperLoc, nodeThrowExpr } from "../../ir/build.js";
|
|
import { isJsSourceFile, locOf } from "../program.js";
|
|
import { typeKey } from "../type-mapper.js";
|
|
import { functionCanReturnUndefined } from "../function-completion.js";
|
|
import { PoisonError, dynFallbackType, neverTaintedJsType, newFnCtx } from "./lowerer.js";
|
|
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
|
import { findGenericMethodOn, storedClassValueType, type ClassInfo } from "./lower-classes.js";
|
|
import { nsPathPrefix } from "./lower-namespaces.js";
|
|
import { declSymbolOf } from "./lower-modules.js";
|
|
import { npmStaticPackageOfPath } from "../npm-static.js";
|
|
import { rejectStaticThis } from "./static-this.js";
|
|
import { isVarDeclared, jsBindingHasOpenWrites } from "./lower-stmts.js";
|
|
import {
|
|
type ParamShape,
|
|
paramShapes,
|
|
generatorMeta,
|
|
funcTypeFromParamShapes,
|
|
} from "./call-signatures.js";
|
|
import {
|
|
fenceGenericSignatureResult,
|
|
blockBodyOf,
|
|
resolveInferredReturn,
|
|
appendImplicitUndefinedReturn,
|
|
hasExplicitJsDocReturn,
|
|
promiseCarriesDyn,
|
|
} from "./function-returns.js";
|
|
import { bindingNeverReassigned, stripValueWrappers } from "./binding-analysis.js";
|
|
import {
|
|
extendInstantiationPath,
|
|
MAX_INSTANTIATION_RECURSION,
|
|
type InstantiationPath,
|
|
} from "./instantiation-path.js";
|
|
|
|
/** A generic function-like declaration, collected instead of an FnSig —
|
|
* top-level generic function declarations, class GENERIC METHODS (own type
|
|
* parameters, instance and static), and object-literal generic methods.
|
|
* The body is NOT lowered at collection: each call site's checker-resolved
|
|
* signature (type arguments substituted) becomes an instantiation key, and
|
|
* the body is lowered once per distinct key (monomorphization). */
|
|
export interface GenericFnInfo {
|
|
decl: ts.FunctionDeclaration | ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction;
|
|
/** Unqualified source name, for diagnostics. */
|
|
baseName: string;
|
|
/** Program-wide qualified name; instance `n` is named `<qualified>%<n>`
|
|
* ('%' cannot appear in a TS identifier, so instance names can never
|
|
* collide with user functions). */
|
|
qualifiedName: string;
|
|
/** Declaration-order type parameter symbols. */
|
|
typeParams: ts.Symbol[];
|
|
/** Lazily computed (keyofConstrainedTypeParams): the type parameters
|
|
* declared `K extends keyof …`. Their bound LITERAL keys are semantic —
|
|
* the body's `o[k]` reads the named field — so instances key on the
|
|
* literal (no cross-literal sharing) and keep the checker types
|
|
* (tsBindings) the body resolves through. */
|
|
keyofTps?: Set<ts.Symbol>;
|
|
/** Instantiation key (comma-joined typeKeys of the mapped param types +
|
|
* `=>` + return typeKey) → instance. Key identity IS signature identity:
|
|
* two call sites whose inferred types map to the same IR types share one
|
|
* native function. */
|
|
instances: Map<string, GenericInstance>;
|
|
/** CLASS-member generic methods: the declaring ClassInfo and flavor.
|
|
* Instance methods take `this` (object:<declarer>) as param 0 and lower
|
|
* under the declarer's instantiation bindings (generic-class receivers)
|
|
* MERGED with the method instantiation's own; statics lower as plain
|
|
* module functions with an exact receiver or the this/super fence. Absent for
|
|
* top-level functions and object-literal methods. */
|
|
member?: { cls: ClassInfo; kind: "method" | "static" };
|
|
/** Direct static calls specialize lexical this for the proven receiver. */
|
|
staticReceiver?: ClassInfo;
|
|
receiverSpecializations?: Map<string, GenericFnInfo>;
|
|
/** Object-literal generic methods (`{ m<T>(x: T) {...} }` and generic
|
|
* arrow/function-expression properties): lowered as plain module
|
|
* functions — `this` inside is fenced (rejectThisInObjectMethod) and the
|
|
* defining literal must sit at module scope (no enclosing frame to
|
|
* capture). */
|
|
objectLiteral?: true;
|
|
/** A binding declared inside a function: each evaluation of the
|
|
* declaration makes a new function object in JavaScript, while the
|
|
* compiled instances are shared module functions, so only calls lower. */
|
|
functionScoped?: true;
|
|
/** The declaration of a generic arrow/function-expression binding. */
|
|
bindingDecl?: ts.VariableDeclaration;
|
|
/** A module-scope binding that code can use before its declaration
|
|
* runs: the BOOL global its declaration sets, and whether it is a
|
|
* `let`/`const` (a temporal dead zone) or a `var` (undefined). */
|
|
presence?: { flagId: string; lexical: boolean };
|
|
/** genericValueForwarder's lifted functions, by instance name. */
|
|
valueForwarders?: Map<string, string>;
|
|
/** runsAfterDeclaration's answers for the function declarations whose
|
|
* calls it placed. */
|
|
ordering?: Map<ts.Node, boolean>;
|
|
/** IMPLICIT-ANY monomorphization (npm-static JS): parallel to
|
|
* decl.parameters — the param's own symbol when the slot is a BINDABLE
|
|
* implicit-any parameter (untyped, identifier-named, never written in
|
|
* the body), null for typed or unbindable slots. Present ⇔ this info
|
|
* monomorphizes over its implicit-any params instead of declared type
|
|
* parameters (typeParams stays empty): each call site's WIDENED argument
|
|
* checker types key an instantiation, exactly the generic machinery —
|
|
* the untyped params ARE the type parameters (see implicitCallInstance). */
|
|
implicitParams?: (ts.Symbol | null)[];
|
|
/** Implicit-any monomorphization of destructured object parameters
|
|
* (implicitPatternsOf): parallel to decl.parameters, the destructured
|
|
* names where the slot binds the call site's object type. Computed on
|
|
* first use. */
|
|
implicitPatterns?: (ImplicitPattern | null)[];
|
|
/** Parameter index → symbol for the function parameters whose `.name`
|
|
* the body reads (static-function-names.ts); computed on first use. */
|
|
nameReadParams?: Map<number, ts.Symbol>;
|
|
}
|
|
|
|
export interface GenericInstance {
|
|
path: InstantiationPath;
|
|
name: string;
|
|
/** 0 for the first instance of a base function — the only one whose
|
|
* statements count toward coverage stats (re-instantiations re-visit the
|
|
* same source lines). */
|
|
ordinal: number;
|
|
params: ParamShape[];
|
|
returnType: IrType;
|
|
/** Type-parameter symbol → concrete IR type, consulted by mapType (via
|
|
* typeParamResolver) while the instance body lowers. */
|
|
bindings: Map<ts.Symbol, IrType>;
|
|
/** Call-keyed instances: type-parameter symbol → the bound CHECKER type
|
|
* (pre-widening), consulted while the body lowers where the IrType
|
|
* binding has already lost what the body needs — `T[K]` and `o[k]` reads
|
|
* whose K is bound to one literal key (typeParamTsBindings). */
|
|
tsBindings?: Map<ts.Symbol, ts.Type>;
|
|
/** Rendered type arguments ("<number, string>") for diagnostics. */
|
|
typeArgsText: string;
|
|
/** A factory returning one of its local classes through a declared
|
|
* constructor type: that type (by id) and the class value it maps to
|
|
* while the body lowers (Lowerer.instanceResultClass). */
|
|
resultClass?: { declared: number; value: IrType };
|
|
/** Implicit instances only: param symbol → the call site's (widened)
|
|
* checker type, consulted by the Lowerer's typeOf while this instance's
|
|
* body lowers (the implicit twin of `bindings`). */
|
|
implicitArgTypes?: Map<ts.Symbol, ts.Type>;
|
|
/** Implicit instances only: eager-lowering lifecycle. "lowering" while
|
|
* the body builds (a re-demand is same-key recursion: the caller uses
|
|
* the PINNED fallback returnType and returnPinned locks it); "done" once
|
|
* returnType holds the inferred (or pinned) truth. */
|
|
implicitState?: "lowering" | "done" | "failed";
|
|
/** Same-key recursion observed the fallback return type mid-lowering, so
|
|
* the ABI is locked to it — the return post-pass coerces every return
|
|
* value to the pinned type instead of adopting the inferred one. */
|
|
returnPinned?: boolean;
|
|
/** Implicit instances only: the declared return did not map (the
|
|
* any-params poisoned it) — the body lowers in return-INFERENCE mode
|
|
* (returnType holds the DYN recursion pin until the post-pass settles). */
|
|
implicitInferReturn?: true;
|
|
/** An erased instance (every type parameter bound to `unknown`). */
|
|
erased?: true;
|
|
/** Name-keyed instances: function parameter → its call site's static
|
|
* name (null when only known at run time), folded at `.name` reads. */
|
|
staticFnNames?: Map<ts.Symbol, string | null>;
|
|
}
|
|
|
|
/** Defaults bind during inference; collection only resolves symbols in
|
|
* declaration order and preserves each declaration form's diagnostic. */
|
|
function declaredTypeParameterSymbols(
|
|
lowerer: Lowerer,
|
|
parameters: readonly ts.TypeParameterDeclaration[],
|
|
blame: ts.Node,
|
|
detail: string,
|
|
): ts.Symbol[] {
|
|
return parameters.map((parameter) => {
|
|
const symbol = lowerer.checker.getSymbolAtLocation(parameter.name);
|
|
if (!symbol) lowerer.unsupported("SC1090", blame, detail);
|
|
return symbol;
|
|
});
|
|
}
|
|
|
|
/** Pattern parameters are admitted by declarations and value bindings;
|
|
* object-literal generic methods retain their identifier-only boundary. */
|
|
function checkGenericParameterNames(
|
|
lowerer: Lowerer,
|
|
parameters: readonly ts.ParameterDeclaration[],
|
|
allowPatterns: boolean,
|
|
): void {
|
|
for (const parameter of parameters) {
|
|
if (
|
|
!ts.isIdentifier(parameter.name) &&
|
|
!(
|
|
allowPatterns &&
|
|
(ts.isObjectBindingPattern(parameter.name) || ts.isArrayBindingPattern(parameter.name))
|
|
)
|
|
) {
|
|
lowerer.unsupported("SC1031", parameter);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Static generic values must keep their initializer for their lifetime.
|
|
* Merged var declarations count as writes even when the assignment scan
|
|
* cannot see them; const bindings need no whole-file write scan. */
|
|
function requireStableGenericBinding(
|
|
lowerer: Lowerer,
|
|
decl: ts.VariableDeclaration,
|
|
symbol: ts.Symbol,
|
|
name: string,
|
|
): void {
|
|
const isConst = (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0;
|
|
const redeclared = symbol.declarations.some(
|
|
(d) => d !== decl && ts.isVariableDeclaration(d) && d.initializer !== undefined,
|
|
);
|
|
if (!isConst && (redeclared || !bindingNeverReassigned(lowerer, symbol, decl))) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
decl.name,
|
|
`generic function values in reassigned bindings (calls of '${name}' resolve statically against this initializer, so the binding must provably hold it — a const, or a let/var nothing in its declaring file writes)`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/** Registers a top-level generic function. Only the SYNTAX is checked
|
|
* here — parameter/return types mention the type parameters and cannot
|
|
* map yet; the body is lowered per instantiation, on demand (an unused
|
|
* generic function requires no body lowering). Called inside
|
|
* collectSignature's poison catch. */
|
|
export function collectGenericSignature(lowerer: Lowerer, decl: ts.FunctionDeclaration): void {
|
|
const typeParams = declaredTypeParameterSymbols(
|
|
lowerer,
|
|
decl.typeParameters!.nodes,
|
|
decl,
|
|
"this function form",
|
|
);
|
|
checkGenericParameterNames(lowerer, decl.parameters.nodes, true);
|
|
const nameText = decl.name?.text ?? "%default"; // nameless = the default export (checked by collectSignatureInner)
|
|
const symbol = declSymbolOf(lowerer, decl);
|
|
if (!symbol) lowerer.unsupported("SC1090", decl, "this function form");
|
|
lowerer.genericFnsBySymbol.set(symbol, {
|
|
decl,
|
|
baseName: nameText,
|
|
qualifiedName: lowerer.qualify(
|
|
ts.getSourceFileOfNode(decl),
|
|
nsPathPrefix(lowerer, decl) + nameText,
|
|
),
|
|
typeParams,
|
|
instances: new Map(),
|
|
});
|
|
}
|
|
|
|
export function genericFnOf(lowerer: Lowerer, ident: ts.Identifier): GenericFnInfo | null {
|
|
const symbol = lowerer.resolveValueSymbol(ident);
|
|
return symbol ? (lowerer.genericFnsBySymbol.get(symbol) ?? null) : null;
|
|
}
|
|
|
|
/** Call of a generic top-level function. The checker already inferred (or
|
|
* was told, via explicit type arguments) the concrete signature —
|
|
* getResolvedSignature returns it with type arguments substituted. The
|
|
* mapped param+return IR types form the INSTANTIATION KEY; the first call
|
|
* with a new key queues the body for monomorphic lowering as
|
|
* `<qualifiedName>%<n>`, and every call lowers to a direct `call` of that
|
|
* instance. */
|
|
export function lowerGenericCall(
|
|
lowerer: Lowerer,
|
|
expr: ts.CallExpression,
|
|
info: GenericFnInfo,
|
|
): IrExpr {
|
|
const loc = locOf(expr);
|
|
const instance = info.implicitParams
|
|
? implicitCallInstance(lowerer, expr, info)
|
|
: genericCallInstance(lowerer, expr, info);
|
|
const args = lowerer.completeArgs(expr.arguments.nodes, instance.params, loc, expr);
|
|
const call: IrExpr & { kind: "call" } = {
|
|
kind: "call",
|
|
callee: instance.name,
|
|
args,
|
|
type: instance.returnType,
|
|
loc,
|
|
};
|
|
return guardEarlyBindingCall(lowerer, info, expr, call);
|
|
}
|
|
|
|
/** The instance a CALL of a generic function-like names: resolved
|
|
* signature → mapped param shapes/return → interned instance. Shared by
|
|
* top-level generic calls, class generic-method calls (the caller
|
|
* prepends the receiver), and object-literal generic-method calls. */
|
|
export function genericCallInstance(
|
|
lowerer: Lowerer,
|
|
expr: ts.CallExpression,
|
|
info: GenericFnInfo,
|
|
): GenericInstance {
|
|
const rsig = lowerer.checker.getResolvedSignature(expr);
|
|
// A GENERIC function with overload signatures: the call resolved to a
|
|
// signature that is not the implementation's, so the per-instantiation
|
|
// body lowering would type the body against parameter/return types it
|
|
// was never checked under. Named fence until generic overloads get an
|
|
// honest story (monomorphize per implementation signature with the
|
|
// reconcile bridge, like the non-generic path).
|
|
{
|
|
const rdecl = rsig ? rsig.declaration : undefined;
|
|
if (
|
|
rsig &&
|
|
rdecl &&
|
|
(ts.isFunctionDeclaration(rdecl) || ts.isMethodDeclaration(rdecl)) &&
|
|
!rdecl.body
|
|
) {
|
|
const overload = overloadCallInstance(lowerer, expr, info, rsig, rdecl);
|
|
if (overload) return overload;
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
expr,
|
|
`calls selecting an overload signature of a generic ${ts.isMethodDeclaration(rdecl) ? "method" : "function"} (only the implementation signature monomorphizes)`,
|
|
);
|
|
}
|
|
}
|
|
if (!rsig || rsig.getParameters().length !== info.decl.parameters.nodes.length) {
|
|
lowerer.unsupported("SC1090", expr, "this call form");
|
|
}
|
|
// Per-param shapes from the RESOLVED signature (types substituted) plus
|
|
// the declaration's modes: rest stays the resolved array, a default's
|
|
// ABI union is synthesized over the resolved body type — exactly the
|
|
// paramShape rules, applied to post-substitution types.
|
|
const params = paramShapes(
|
|
lowerer,
|
|
info.decl.parameters.nodes,
|
|
rsig,
|
|
(_param, i) => expr.arguments.nodes[i] ?? expr,
|
|
);
|
|
const retTs = lowerer.checker.getReturnTypeOfSignature(rsig);
|
|
const local = localClassResultInstance(lowerer, expr, info, rsig, params, retTs);
|
|
if (local) return local;
|
|
// A result instantiated at `any` (a type argument inferred from an
|
|
// untyped value, such as an untyped forwarding call) is checked-dynamic,
|
|
// and so is a JavaScript result whose JSDoc names a type the file never
|
|
// declares or imports (`@returns {MiddlewareHandler<E>}` in a published
|
|
// bundle): the checker's error type is documentation it could not
|
|
// resolve, not a contract.
|
|
const mappedReturn =
|
|
lowerer.mapTypeOf(retTs) ??
|
|
((retTs.flags & ts.TypeFlags.Any) !== 0 &&
|
|
(!isJsSourceFile(ts.getSourceFileOfNode(info.decl)) || retTs.isErrorType())
|
|
? DYN
|
|
: null);
|
|
if (!mappedReturn) {
|
|
fenceGenericSignatureResult(lowerer, expr, retTs);
|
|
lowerer.badType(expr, retTs);
|
|
}
|
|
const returnType = runtimeReturnType(lowerer, info, mappedReturn);
|
|
|
|
// keyof-constrained type parameters (`K extends keyof T`): the bound
|
|
// LITERAL is semantic — the instance body reads the named field — so
|
|
// the bindings compute EAGERLY (the key needs them) and each literal
|
|
// keys its own instance (`pick(o, "a")` and `pick(o, "b")` map to the
|
|
// same IR signature when the fields agree, but their bodies read
|
|
// different fields). Non-literal bindings (a key union, plain string)
|
|
// share one runtime-keyed instance per IR signature, exactly the
|
|
// widened discipline.
|
|
const named = staticNameBindings(lowerer, expr, info);
|
|
if (keyofConstrainedTypeParams(info).size > 0) {
|
|
const tsBindings = new Map<ts.Symbol, ts.Type>();
|
|
const bindings = lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings);
|
|
return internGenericInstance(lowerer, expr, info, params, returnType, () => bindings, {
|
|
extraKey: `${keyofLiteralKey(info, tsBindings)}${named?.key ?? ""}`,
|
|
tsBindings,
|
|
...(named ? { staticFnNames: named.names } : {}),
|
|
});
|
|
}
|
|
return internGenericInstance(
|
|
lowerer,
|
|
expr,
|
|
info,
|
|
params,
|
|
returnType,
|
|
(tsBindings) => lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings),
|
|
named ? { extraKey: named.key, staticFnNames: named.names } : undefined,
|
|
);
|
|
}
|
|
|
|
/** A call that resolved to an OVERLOAD signature of a generic function
|
|
* or method with an implementation (`request<T>(p: WithHandler<T>):
|
|
* Promise<T>; request(p: Plain): Promise<Res>; request<T>(p: Params<T>)
|
|
* {...}`): the implementation monomorphizes under the bindings the
|
|
* overload's resolved signature proves for its type parameters (the
|
|
* structural unification every generic call uses, run against the
|
|
* overload; unbound parameters take their declared defaults). The
|
|
* instance's parameters and result are the IMPLEMENTATION's under those
|
|
* bindings — the body was checked against them — and the call's
|
|
* arguments and result coerce across, exactly as a non-generic overload
|
|
* call reconciles with its implementation (parameters the overload omits
|
|
* must be optional in the implementation). Null when the overload does
|
|
* not belong to this implementation or the arities do not line up. */
|
|
function overloadCallInstance(
|
|
lowerer: Lowerer,
|
|
expr: ts.CallExpression,
|
|
info: GenericFnInfo,
|
|
rsig: ts.Signature,
|
|
rdecl: ts.FunctionDeclaration | ts.MethodDeclaration,
|
|
): GenericInstance | null {
|
|
const impl = info.decl;
|
|
if (
|
|
rdecl.parent !== impl.parent ||
|
|
rdecl.name === undefined ||
|
|
impl.name === undefined ||
|
|
rdecl.name.getText() !== impl.name.getText() ||
|
|
rdecl.parameters.nodes.length > impl.parameters.nodes.length ||
|
|
impl.parameters.nodes
|
|
.slice(rdecl.parameters.nodes.length)
|
|
.some((p) => p.questionToken === undefined && p.initializer === undefined) ||
|
|
rdecl.parameters.nodes.some((p) => p.dotDotDotToken !== undefined)
|
|
)
|
|
return null;
|
|
const declSig = lowerer.checker.getSignatureFromDeclaration(impl);
|
|
if (!declSig) return null;
|
|
const tsBindings = new Map<ts.Symbol, ts.Type>();
|
|
const bindings = new Map<ts.Symbol, IrType>();
|
|
unifySignatureBindings(lowerer, info, rsig, bindings, tsBindings);
|
|
// Explicit type arguments name the OVERLOAD's type parameters, which
|
|
// need not match the implementation's in order: the resolved overload
|
|
// already carries them, so unification binds first, and an explicit
|
|
// argument binds only a still-unbound implementation parameter of the
|
|
// same name.
|
|
const overloadTps = rdecl.typeParameters?.nodes ?? [];
|
|
expr.typeArguments?.nodes.forEach((ta, i) => {
|
|
const name = overloadTps[i]?.name.text;
|
|
const tp = info.typeParams.find((p) => ts.symbolName(p) === name);
|
|
if (!tp || bindings.has(tp)) return;
|
|
const taT = lowerer.checker.getTypeFromTypeNode(ta);
|
|
const mapped = lowerer.mapTypeOf(taT);
|
|
if (mapped) {
|
|
bindings.set(tp, mapped);
|
|
tsBindings.set(tp, taT);
|
|
}
|
|
});
|
|
bindDefaultTypeParams(lowerer, info.typeParams, impl.typeParameters?.nodes, bindings, tsBindings);
|
|
// A type parameter the overload never mentions (a handler the overload
|
|
// omits) is `unknown`, the checker's own answer for an uninferred one.
|
|
for (const tp of info.typeParams) {
|
|
if (bindings.has(tp)) continue;
|
|
bindings.set(tp, DYN);
|
|
tsBindings.set(tp, lowerer.checker.getUnknownType());
|
|
}
|
|
const previous = lowerer.typeParamBindings;
|
|
const previousTs = lowerer.typeParamTsBindings;
|
|
lowerer.typeParamBindings = bindings;
|
|
lowerer.typeParamTsBindings = tsBindings;
|
|
let params: ParamShape[];
|
|
let returnType: IrType | null;
|
|
try {
|
|
params = paramShapes(
|
|
lowerer,
|
|
impl.parameters.nodes,
|
|
declSig,
|
|
(_param, i) => expr.arguments.nodes[i] ?? expr,
|
|
);
|
|
returnType = lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(declSig));
|
|
} finally {
|
|
lowerer.typeParamBindings = previous;
|
|
lowerer.typeParamTsBindings = previousTs;
|
|
}
|
|
if (!returnType) return null;
|
|
// keyof-constrained parameters key their instance on the bound literal,
|
|
// as on the implementation-signature path.
|
|
const runtimeReturn = runtimeReturnType(lowerer, info, returnType);
|
|
return internGenericInstance(lowerer, expr, info, params, runtimeReturn, () => bindings, {
|
|
...(keyofConstrainedTypeParams(info).size > 0
|
|
? { extraKey: keyofLiteralKey(info, tsBindings) }
|
|
: {}),
|
|
tsBindings,
|
|
});
|
|
}
|
|
|
|
/** An instance's result type: a body that can return an indexed read
|
|
* that misses returns undefined there, as in JavaScript, whatever the
|
|
* declared result says. An async body's promise is always present and
|
|
* settles with undefined there. */
|
|
function runtimeReturnType(lowerer: Lowerer, info: GenericFnInfo, mapped: IrType): IrType {
|
|
if (!lowerer.runtimeOptionalGenericReturns.has(info.decl)) return mapped;
|
|
return mapped.kind === "promise"
|
|
? { kind: "promise", inner: lowerer.runtimeOptionalType(mapped.inner) }
|
|
: lowerer.runtimeOptionalType(mapped);
|
|
}
|
|
|
|
/** A generic factory whose result is one of its own local classes
|
|
* (`function base<T>(v: T) { return class { value: T = v }; }`). Each
|
|
* instance has its own class, so the instance is keyed on its bindings and
|
|
* the result maps inside it, where the local class takes the instance's
|
|
* name and bindings. */
|
|
function localClassResultInstance(
|
|
lowerer: Lowerer,
|
|
expr: ts.CallExpression,
|
|
info: GenericFnInfo,
|
|
rsig: ts.Signature,
|
|
params: ParamShape[],
|
|
retTs: ts.Type,
|
|
): GenericInstance | null {
|
|
const resultClass = localClassOfResult(lowerer, info, retTs);
|
|
if (!resultClass) return null;
|
|
const tsBindings = new Map<ts.Symbol, ts.Type>();
|
|
const bindings = lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings);
|
|
const bindingKey = info.typeParams
|
|
.map((tp) => {
|
|
const bound = bindings.get(tp);
|
|
return bound ? typeKey(bound) : "?";
|
|
})
|
|
.join(",");
|
|
const pending: IrType = { kind: "void" };
|
|
const inst = internGenericInstance(lowerer, expr, info, params, pending, () => bindings, {
|
|
extraKey: `@class<${bindingKey}>`,
|
|
tsBindings,
|
|
});
|
|
if (inst.returnType !== pending) return inst;
|
|
// A result typed by a declared constructor type rather than the class
|
|
// (`Struct<S>`) is the class value of the local class the body returns.
|
|
const declaredResult = info.decl.type
|
|
? lowerer.checker.getTypeFromTypeNode(info.decl.type)
|
|
: undefined;
|
|
const viaConstructorType = retTs.getSymbol()?.valueDeclaration !== resultClass;
|
|
const returnType = withInstanceScope(lowerer, info, inst, () => {
|
|
if (!viaConstructorType) return lowerer.mapTypeOf(retTs);
|
|
// The local class's own constructor type: a class expression's type is
|
|
// its constructor type, named or not (`return class { ... } as Struct<S>`).
|
|
const classSymbol =
|
|
!ts.isClassExpression(resultClass) && resultClass.name
|
|
? lowerer.checker.getSymbolAtLocation(resultClass.name)
|
|
: undefined;
|
|
const classType = ts.isClassExpression(resultClass)
|
|
? lowerer.checker.getTypeAtLocation(resultClass)
|
|
: classSymbol
|
|
? lowerer.checker.getTypeOfSymbol(classSymbol)
|
|
: undefined;
|
|
return classType ? lowerer.mapTypeOf(classType) : null;
|
|
});
|
|
if (!returnType) {
|
|
fenceGenericSignatureResult(lowerer, expr, retTs);
|
|
lowerer.badType(expr, retTs);
|
|
}
|
|
inst.returnType = returnType;
|
|
if (viaConstructorType && returnType.kind === "classval") {
|
|
if (declaredResult) inst.resultClass = { declared: declaredResult.id, value: returnType };
|
|
if (lowerer.typeParamBindings === null) lowerer.localClassValueTypes.set(retTs.id, returnType);
|
|
return inst;
|
|
}
|
|
// Values of the class outside the factory (`const b = new B()`, `b.read()`)
|
|
// carry the instantiated checker type; record which class it names. A
|
|
// call inside another generic instance can have a type that depends on
|
|
// that instance's parameters, which no single class answers.
|
|
if (lowerer.typeParamBindings !== null) return inst;
|
|
const instance =
|
|
returnType.kind === "classval"
|
|
? lowerer.checker
|
|
.getConstructSignatures(retTs)
|
|
.map((sig) => lowerer.checker.getReturnTypeOfSignature(sig))
|
|
: [retTs];
|
|
const className =
|
|
returnType.kind === "classval" || returnType.kind === "object" ? returnType.className : null;
|
|
if (className !== null)
|
|
for (const type of instance) lowerer.localClassInstanceTypes.set(type.id, className);
|
|
return inst;
|
|
}
|
|
|
|
/** The class value a call of a generic factory returns when the result is
|
|
* one of the factory's local classes (`class C extends base(42) {}`), or
|
|
* null for any other call. */
|
|
export function genericLocalClassResult(lowerer: Lowerer, expr: ts.CallExpression): IrType | null {
|
|
if (!ts.isIdentifier(expr.expression)) return null;
|
|
const info = lowerer.genericFnOf(expr.expression);
|
|
if (!info || info.implicitParams) return null;
|
|
const rsig = lowerer.checker.getResolvedSignature(expr);
|
|
if (!rsig || !localClassOfResult(lowerer, info, lowerer.checker.getReturnTypeOfSignature(rsig)))
|
|
return null;
|
|
return genericCallInstance(lowerer, expr, info).returnType;
|
|
}
|
|
|
|
/** The type of a `const` binding whose initializer calls a generic factory
|
|
* returning one of its local classes (`const B = base(7)`), or constructs
|
|
* such a class (`const b = new B()`); null for any other binding. */
|
|
export function genericLocalClassBinding(lowerer: Lowerer, node: ts.Node): IrType | null {
|
|
const decl = node.parent;
|
|
if (
|
|
!ts.isIdentifier(node) ||
|
|
!decl ||
|
|
!ts.isVariableDeclaration(decl) ||
|
|
decl.name !== node ||
|
|
!decl.initializer ||
|
|
(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) === 0
|
|
)
|
|
return null;
|
|
// An `any` binding keeps the checked-dynamic representation.
|
|
if ((lowerer.typeOf(node).flags & ts.TypeFlags.Any) !== 0) return null;
|
|
let init = decl.initializer;
|
|
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
|
if (ts.isCallExpression(init)) return genericLocalClassResult(lowerer, init);
|
|
if (!ts.isNewExpression(init)) return null;
|
|
const constructed = storedClassValueType(lowerer, init.expression);
|
|
if (constructed?.kind !== "classval") return null;
|
|
const classDecl = lowerer.classes.get(constructed.className)?.decl;
|
|
return classDecl && insideGenericFunction(classDecl)
|
|
? { kind: "object", className: constructed.className }
|
|
: null;
|
|
}
|
|
|
|
/** Whether `node` is declared inside a function with type parameters. */
|
|
function insideGenericFunction(node: ts.Node): boolean {
|
|
for (let parent = node.parent; parent && !ts.isSourceFile(parent); parent = parent.parent)
|
|
if (ts.isFunctionLike(parent) && (parent.typeParameters?.nodes.length ?? 0) > 0) return true;
|
|
return false;
|
|
}
|
|
|
|
/** The class declared inside a generic function that `type` is the
|
|
* constructor or instance type of, or null. */
|
|
function localClassOfResult(
|
|
lowerer: Lowerer,
|
|
info: GenericFnInfo,
|
|
type: ts.Type,
|
|
): ts.ClassLikeDeclaration | null {
|
|
const symbol = type.getSymbol();
|
|
const decl = symbol && symbol.flags & ts.SymbolFlags.Class ? symbol.valueDeclaration : undefined;
|
|
if (decl && (ts.isClassExpression(decl) || ts.isClassDeclaration(decl)))
|
|
return declaredInside(decl, info.decl) ? decl : null;
|
|
// A result typed by a constructor type (`Struct<S>`, `new () => S`) whose
|
|
// every return gives back the one local class, through parentheses and
|
|
// assertions (`return Base as Struct<S>`).
|
|
// A mixin (a class extending one of the function's parameters) keeps the
|
|
// mixin instantiation, which keys on the argument class.
|
|
if (lowerer.checker.getConstructSignatures(type).length === 0) return null;
|
|
const returned = returnedLocalClass(lowerer, info);
|
|
return returned && !extendsParameter(lowerer, returned, info) ? returned : null;
|
|
}
|
|
|
|
function extendsParameter(
|
|
lowerer: Lowerer,
|
|
decl: ts.ClassLikeDeclaration,
|
|
info: GenericFnInfo,
|
|
): boolean {
|
|
const params = new Set<ts.Symbol>();
|
|
for (const param of info.decl.parameters.nodes) {
|
|
const symbol = ts.isIdentifier(param.name)
|
|
? lowerer.checker.getSymbolAtLocation(param.name)
|
|
: undefined;
|
|
if (symbol !== undefined) params.add(symbol);
|
|
}
|
|
for (const clause of decl.heritageClauses?.nodes ?? [])
|
|
for (const base of clause.types.nodes) {
|
|
const target = heritageElementTarget(base);
|
|
const symbol = ts.isIdentifier(target)
|
|
? lowerer.checker.getSymbolAtLocation(target)
|
|
: undefined;
|
|
if (symbol !== undefined && params.has(symbol)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function declaredInside(node: ts.Node, owner: ts.Node): boolean {
|
|
for (let parent = node.parent; parent && !ts.isSourceFile(parent); parent = parent.parent)
|
|
if (parent === owner) return true;
|
|
return false;
|
|
}
|
|
|
|
/** The local class every return of `info`'s body gives back, or null when a
|
|
* return gives anything else (or nothing). Nested functions and classes keep
|
|
* their own returns. */
|
|
function returnedLocalClass(lowerer: Lowerer, info: GenericFnInfo): ts.ClassLikeDeclaration | null {
|
|
const body = info.decl.body;
|
|
if (!body) return null;
|
|
const classOf = (node: ts.Expression): ts.ClassLikeDeclaration | null => {
|
|
let expr = node;
|
|
while (
|
|
ts.isParenthesizedExpression(expr) ||
|
|
ts.isAsExpression(expr) ||
|
|
ts.isSatisfiesExpression(expr) ||
|
|
ts.isNonNullExpression(expr)
|
|
)
|
|
expr = expr.expression;
|
|
if (ts.isClassExpression(expr)) return declaredInside(expr, info.decl) ? expr : null;
|
|
if (!ts.isIdentifier(expr)) return null;
|
|
const declaration = lowerer.checker.getSymbolAtLocation(expr)?.valueDeclaration;
|
|
if (!declaration || !declaredInside(declaration, info.decl)) return null;
|
|
if (ts.isClassDeclaration(declaration)) return declaration;
|
|
// A `const` bound to a class expression (`const C = class { ... }; return C`).
|
|
if (
|
|
ts.isVariableDeclaration(declaration) &&
|
|
ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const &&
|
|
declaration.initializer
|
|
) {
|
|
let init: ts.Expression = declaration.initializer;
|
|
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
|
if (ts.isClassExpression(init)) return init;
|
|
}
|
|
return null;
|
|
};
|
|
if (!ts.isBlock(body)) return classOf(body as ts.Expression);
|
|
let found: ts.ClassLikeDeclaration | null = null;
|
|
let valid = true;
|
|
const visit = (node: ts.Node): void => {
|
|
if (
|
|
!valid ||
|
|
ts.isFunctionLike(node) ||
|
|
ts.isClassDeclaration(node) ||
|
|
ts.isClassExpression(node)
|
|
)
|
|
return;
|
|
if (ts.isReturnStatement(node)) {
|
|
const returned = node.expression ? classOf(node.expression) : null;
|
|
if (!returned || (found !== null && found !== returned)) valid = false;
|
|
else found = returned;
|
|
return;
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
ts.forEachChild(body, visit);
|
|
return valid ? found : null;
|
|
}
|
|
|
|
/** Runs `fn` under an instance's type bindings and local-class names, as
|
|
* its body lowers. */
|
|
function withInstanceScope<T>(
|
|
lowerer: Lowerer,
|
|
info: GenericFnInfo,
|
|
inst: GenericInstance,
|
|
fn: () => T,
|
|
): T {
|
|
const prevBindings = lowerer.typeParamBindings;
|
|
const prevTsBindings = lowerer.typeParamTsBindings;
|
|
const previousPath = lowerer.genericInstantiationPath;
|
|
const prevContext = lowerer.instantiationContext;
|
|
const prevResultClass = lowerer.instanceResultClass;
|
|
lowerer.typeParamBindings = inst.bindings;
|
|
lowerer.typeParamTsBindings = inst.tsBindings ?? null;
|
|
lowerer.genericInstantiationPath = inst.path;
|
|
lowerer.instantiationContext = `instantiating '${info.baseName}' with ${inst.typeArgsText}`;
|
|
lowerer.instanceResultClass = inst.resultClass ?? null;
|
|
lowerer.localClassInstantiations.push({ owner: info.decl, name: inst.name });
|
|
try {
|
|
return fn();
|
|
} finally {
|
|
lowerer.instanceResultClass = prevResultClass;
|
|
lowerer.localClassInstantiations.pop();
|
|
lowerer.typeParamBindings = prevBindings;
|
|
lowerer.typeParamTsBindings = prevTsBindings;
|
|
lowerer.genericInstantiationPath = previousPath;
|
|
lowerer.instantiationContext = prevContext;
|
|
}
|
|
}
|
|
|
|
/** The one instance table both instantiation routes share: key identity IS
|
|
* signature identity, so a call (`identity(1)`) and a pinned VALUE
|
|
* (`const f: (x: number) => number = identity`) reuse one compiled
|
|
* instance. `makeBindings` runs only for a NEW key (binding inference
|
|
* costs checker walks). */
|
|
function internGenericInstance(
|
|
lowerer: Lowerer,
|
|
blame: ts.Node,
|
|
info: GenericFnInfo,
|
|
params: ParamShape[],
|
|
returnType: IrType,
|
|
makeBindings: (tsBindings: Map<ts.Symbol, ts.Type>) => Map<ts.Symbol, IrType>,
|
|
opts?: {
|
|
extraKey?: string;
|
|
tsBindings?: Map<ts.Symbol, ts.Type>;
|
|
staticFnNames?: Map<ts.Symbol, string | null>;
|
|
},
|
|
): GenericInstance {
|
|
// keyof-constrained instantiations append their literal keys
|
|
// (extraKey): the IR signature alone under-discriminates there — two
|
|
// literals can map to one IR signature while their bodies read
|
|
// different fields.
|
|
const key = `${params.map((s) => typeKey(s.type)).join(",")}=>${typeKey(returnType)}${opts?.extraKey ?? ""}`;
|
|
let inst = info.instances.get(key);
|
|
if (!inst) {
|
|
const path = extendInstantiationPath(lowerer.genericInstantiationPath, info.decl);
|
|
if (!path) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
blame,
|
|
`unbounded generic instantiation ('${info.baseName}' exceeded ` +
|
|
`${MAX_INSTANTIATION_RECURSION} recursive specializations on one demand path)`,
|
|
);
|
|
}
|
|
const tsBindings = opts?.tsBindings ?? new Map<ts.Symbol, ts.Type>();
|
|
const bindings = makeBindings(tsBindings);
|
|
const rendered = info.typeParams
|
|
.map((tp) => {
|
|
// A literal-bound keyof parameter renders its literal — the
|
|
// instance is per-literal, and '<…, string>' would misname it.
|
|
const tsBound = tsBindings.get(tp);
|
|
if (info.keyofTps?.has(tp) && tsBound?.isStringLiteralType()) {
|
|
return JSON.stringify(tsBound.value);
|
|
}
|
|
const bound = bindings.get(tp);
|
|
return bound ? lowerer.fmt(bound) : ts.symbolName(tp);
|
|
})
|
|
.join(", ");
|
|
// Deep polymorphic recursion renders unbounded types — keep messages sane.
|
|
const typeArgsText = `<${rendered.length > 80 ? rendered.slice(0, 77) + "..." : rendered}>`;
|
|
inst = {
|
|
path,
|
|
name: keyedHelperName(lowerer, `${info.qualifiedName}%`, key, ""),
|
|
ordinal: info.instances.size,
|
|
params,
|
|
returnType,
|
|
bindings,
|
|
tsBindings,
|
|
typeArgsText,
|
|
...(opts?.staticFnNames ? { staticFnNames: opts.staticFnNames } : {}),
|
|
};
|
|
info.instances.set(key, inst);
|
|
lowerer.instantiationQueue.push({ info, inst });
|
|
}
|
|
lowerer.noteGenericInstanceDemand(inst);
|
|
return inst;
|
|
}
|
|
|
|
/** The type parameters of `info` declared with a `keyof` CONSTRAINT
|
|
* (`K extends keyof T`) — the parameters whose bound literal is semantic
|
|
* (the body's `o[k]` reads the named field), computed once per info from
|
|
* the declaration's syntax. */
|
|
function keyofConstrainedTypeParams(info: GenericFnInfo): Set<ts.Symbol> {
|
|
if (info.keyofTps) return info.keyofTps;
|
|
const out = new Set<ts.Symbol>();
|
|
info.decl.typeParameters?.nodes.forEach((tpDecl, i) => {
|
|
const sym = info.typeParams[i];
|
|
if (!sym || tpDecl.constraint === undefined) return;
|
|
if (
|
|
ts.isTypeOperatorNode(tpDecl.constraint) &&
|
|
tpDecl.constraint.operator === ts.SyntaxKind.KeyOfKeyword
|
|
) {
|
|
out.add(sym);
|
|
}
|
|
});
|
|
info.keyofTps = out;
|
|
return out;
|
|
}
|
|
|
|
/** The instance-key suffix of the literals bound to `info`'s
|
|
* keyof-constrained type parameters (`@"a"`; `*` for a non-literal
|
|
* binding): two literals can map to one IR signature while their bodies
|
|
* read different fields. */
|
|
function keyofLiteralKey(info: GenericFnInfo, tsBindings: Map<ts.Symbol, ts.Type>): string {
|
|
const keyofTps = keyofConstrainedTypeParams(info);
|
|
const literals = info.typeParams
|
|
.filter((tp) => keyofTps.has(tp))
|
|
.map((tp) => {
|
|
const bound = tsBindings.get(tp);
|
|
return bound?.isStringLiteralType()
|
|
? JSON.stringify(bound.value)
|
|
: bound?.isNumberLiteralType()
|
|
? String(bound.value)
|
|
: "*";
|
|
});
|
|
return `@${literals.join(",")}`;
|
|
}
|
|
|
|
/** Type-parameter symbol → concrete IR type for one instantiation.
|
|
* Explicit type arguments bind directly; the rest come from structurally
|
|
* matching each DECLARED param/return type (which mentions the type
|
|
* parameters) against the checker's INSTANTIATED one — the latter is the
|
|
* former with the substitution applied, so the shapes are parallel by
|
|
* construction. A type parameter left unbound only matters if the body
|
|
* mentions it, where mapType fails and badType names the shape
|
|
* (carrying the instantiation context). */
|
|
export function inferTypeParamBindings(
|
|
lowerer: Lowerer,
|
|
expr: ts.CallExpression,
|
|
info: GenericFnInfo,
|
|
rsig: ts.Signature,
|
|
tsBindings?: Map<ts.Symbol, ts.Type>,
|
|
): Map<ts.Symbol, IrType> {
|
|
const bindings = new Map<ts.Symbol, IrType>();
|
|
expr.typeArguments?.nodes.forEach((ta, i) => {
|
|
const tp = info.typeParams[i];
|
|
if (!tp) return;
|
|
const taT = lowerer.checker.getTypeFromTypeNode(ta);
|
|
const mapped = lowerer.mapTypeOf(taT);
|
|
if (mapped) {
|
|
bindings.set(tp, mapped);
|
|
tsBindings?.set(tp, taT);
|
|
}
|
|
});
|
|
unifySignatureBindings(lowerer, info, rsig, bindings, tsBindings);
|
|
bindDefaultTypeParams(
|
|
lowerer,
|
|
info.typeParams,
|
|
info.decl.typeParameters?.nodes,
|
|
bindings,
|
|
tsBindings,
|
|
);
|
|
return bindings;
|
|
}
|
|
|
|
/** Type parameters still unbound after unification take their declared
|
|
* DEFAULT (`<T = number>`), mapped — the checker already substituted the
|
|
* default into every resolved signature, so this only fills the bindings
|
|
* an instance body's mapType consults. */
|
|
function bindDefaultTypeParams(
|
|
lowerer: Lowerer,
|
|
typeParams: readonly ts.Symbol[],
|
|
typeParamDecls: readonly ts.TypeParameterDeclaration[] | undefined,
|
|
bindings: Map<ts.Symbol, IrType>,
|
|
tsBindings?: Map<ts.Symbol, ts.Type>,
|
|
): void {
|
|
typeParamDecls?.forEach((tpDecl, i) => {
|
|
const tp = typeParams[i];
|
|
if (!tp || bindings.has(tp) || !tpDecl.defaultType) return;
|
|
const defT = lowerer.checker.getTypeFromTypeNode(tpDecl.defaultType);
|
|
const mapped = lowerer.mapTypeOf(defT);
|
|
if (mapped) {
|
|
bindings.set(tp, mapped);
|
|
tsBindings?.set(tp, defT);
|
|
}
|
|
});
|
|
}
|
|
|
|
/** The structural half of binding inference: unify the DECLARED signature
|
|
* (whose types mention the type parameters) against a TARGET signature
|
|
* with the substitution applied — a call's resolved signature, or the
|
|
* completed signature a VALUE reference is pinned to (the contextual
|
|
* type's one call signature). Mutates `bindings`; already-bound
|
|
* parameters (explicit type arguments) win. */
|
|
function unifySignatureBindings(
|
|
lowerer: Lowerer,
|
|
info: GenericFnInfo,
|
|
rsig: ts.Signature,
|
|
bindings: Map<ts.Symbol, IrType>,
|
|
tsBindings?: Map<ts.Symbol, ts.Type>,
|
|
): void {
|
|
const tpSet = new Set(info.typeParams);
|
|
|
|
const seen = new Set<ts.Type>(); // recursive declared types must not loop
|
|
// The identity-keyed set cannot catch LAZILY INFINITE anonymous types
|
|
// (`function rec<T>(x: T) { return { deeper: <U>(y: U) => rec<[T, U]>(...) }; }`
|
|
// — every property/signature walk instantiates FRESH type objects, and
|
|
// no Reference target exists to shortcut on), so a depth cap bounds the
|
|
// walk. Stopping only stops INFERENCE: a type parameter left unbound
|
|
// surfaces as an ordinary mapping diagnostic later, never a wrong
|
|
// binding — and every practical signature binds its parameters within
|
|
// a few levels.
|
|
const MAX_UNIFY_DEPTH = 24;
|
|
const unify = (declared: ts.Type, inst: ts.Type, depth = 0): void => {
|
|
if (depth > MAX_UNIFY_DEPTH) return;
|
|
if (declared.flags & ts.TypeFlags.TypeParameter) {
|
|
const sym: ts.Symbol | undefined = declared.getSymbol();
|
|
if (sym && tpSet.has(sym)) {
|
|
// The checker type records even when an explicit type argument
|
|
// already bound the IrType: the raw type is the SAME binding
|
|
// pre-widening, and first-hit-wins keeps the two maps parallel.
|
|
// A generic body FORWARDING its own parameter (`pluck`'s
|
|
// `pick(it, key)` binds pick's K to pluck's K) resolves through
|
|
// the enclosing instantiation's ts bindings first — the literal
|
|
// carries through the chain.
|
|
if (tsBindings && !tsBindings.has(sym)) {
|
|
tsBindings.set(sym, lowerer.typeParamTsResolver(inst) ?? inst);
|
|
}
|
|
if (!bindings.has(sym)) {
|
|
const mapped =
|
|
lowerer.mapTypeOf(inst) ??
|
|
((inst.flags & ts.TypeFlags.Any) !== 0 &&
|
|
!isJsSourceFile(ts.getSourceFileOfNode(info.decl))
|
|
? DYN
|
|
: null);
|
|
// A visitor's inferred T can be void. It still binds the
|
|
// return convention of `(node) => T`; discarding it leaves
|
|
// the callback's return type spuriously uninstantiated.
|
|
if (mapped) bindings.set(sym, mapped);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (seen.has(declared)) return;
|
|
seen.add(declared);
|
|
// Optional-flavored unions (`x?: T` declares `T | undefined`): strip
|
|
// the unit parts from both sides and unify the lone remaining pair.
|
|
// Multi-part unions have no positional correspondence — skipped (an
|
|
// unbound type parameter surfaces as a mapping diagnostic later).
|
|
if (declared.isUnionType()) {
|
|
const unitFlags = ts.TypeFlags.Undefined | ts.TypeFlags.Null;
|
|
const declaredParts = ts.constituentTypes(declared);
|
|
const declaredUnits = declaredParts.reduce((flags, t) => flags | (t.flags & unitFlags), 0);
|
|
const dParts = declaredParts.filter((t) => !(t.flags & unitFlags));
|
|
const iParts: readonly ts.Type[] = inst.isUnionType()
|
|
? ts.constituentTypes(inst).filter((t) => !(t.flags & declaredUnits))
|
|
: [inst];
|
|
if (dParts.length === 1 && iParts.length === 1) unify(dParts[0]!, iParts[0]!, depth + 1);
|
|
else if (
|
|
dParts.length === 1 &&
|
|
iParts.length > 1 &&
|
|
iParts.every((t) => (t.flags & unitFlags) === 0)
|
|
) {
|
|
// T | undefined can receive boolean | undefined: boolean itself
|
|
// has two literal arms. There is still one declared data slot,
|
|
// so match it against the whole remaining union. Removing null
|
|
// too is safe only when no undeclared null arm remains.
|
|
unify(dParts[0]!, lowerer.checker.getNonNullableType(inst), depth + 1);
|
|
} else if (dParts.length > 1 && iParts.length === dParts.length) {
|
|
// `T | Promise<T>` against `Res | Promise<Res>`: each declared
|
|
// part that names a generic target (Promise, an interface, an
|
|
// alias) pairs with the one instantiated part of the same target;
|
|
// a single remaining bare type parameter takes the single
|
|
// remaining instantiated part.
|
|
const remaining = [...iParts];
|
|
const bare: ts.Type[] = [];
|
|
for (const d of dParts) {
|
|
if (d.flags & ts.TypeFlags.TypeParameter) {
|
|
bare.push(d);
|
|
continue;
|
|
}
|
|
const target = d.getSymbol() ?? d.getAliasSymbol();
|
|
if (target === undefined) return;
|
|
const matches = remaining.filter((i) => (i.getSymbol() ?? i.getAliasSymbol()) === target);
|
|
if (matches.length !== 1) return;
|
|
remaining.splice(remaining.indexOf(matches[0]!), 1);
|
|
unify(d, matches[0]!, depth + 1);
|
|
}
|
|
if (bare.length === 1 && remaining.length === 1) unify(bare[0]!, remaining[0]!, depth + 1);
|
|
}
|
|
return;
|
|
}
|
|
// Instantiations of the SAME generic ALIAS (Partial<T> vs
|
|
// Partial<Config>) unify by alias arguments: instantiation preserves
|
|
// aliasSymbol/aliasTypeArguments, and the two argument lists are
|
|
// parallel by construction. Without this, a mapped-type parameter
|
|
// leaves T unbound — the declared `Partial<T>` has no resolvable
|
|
// members for the property walk below (keyof T is unknown).
|
|
const dAlias = declared.getAliasSymbol();
|
|
const dAliasArgs = declared.getAliasTypeArguments();
|
|
const iAliasArgs = inst.getAliasTypeArguments();
|
|
if (
|
|
dAlias &&
|
|
dAlias === inst.getAliasSymbol() &&
|
|
dAliasArgs.length &&
|
|
iAliasArgs.length === dAliasArgs.length
|
|
) {
|
|
if (tsBindings && declared !== inst) {
|
|
let entries = lowerer.boundAliasInstances.get(declared);
|
|
if (!entries) lowerer.boundAliasInstances.set(declared, (entries = []));
|
|
// A body forwarding its own alias (`inner(g)` returning `Catalog<G,
|
|
// "x">` inside an instance) resolves through the enclosing one.
|
|
if (!entries.some((entry) => entry.tsBindings === tsBindings))
|
|
entries.push({ tsBindings, instance: lowerer.boundAliasTsResolver(inst) ?? inst });
|
|
}
|
|
dAliasArgs.forEach((da, i) => {
|
|
const ia = iAliasArgs[i];
|
|
if (ia) unify(da, ia, depth + 1);
|
|
});
|
|
return;
|
|
}
|
|
// References to the SAME generic (Promise<T> vs Promise<string>, or
|
|
// any interface reference) unify by type ARGUMENTS only. Walking
|
|
// members instead diverges: a self-referential member like Promise's
|
|
// `then<U>(...): Promise<U>` instantiates a FRESH type object on
|
|
// every property read, so an identity-keyed visited set never trips.
|
|
const dRef = declared as ts.TypeReference;
|
|
const iRef = inst as ts.TypeReference;
|
|
if (
|
|
declared.flags & ts.TypeFlags.Object &&
|
|
inst.flags & ts.TypeFlags.Object &&
|
|
(declared as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference &&
|
|
(inst as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference &&
|
|
dRef.getTarget() === iRef.getTarget()
|
|
) {
|
|
const dArgs = lowerer.checker.getTypeArguments(dRef);
|
|
const iArgs = lowerer.checker.getTypeArguments(iRef);
|
|
dArgs.forEach((da, i) => {
|
|
const ia = iArgs[i];
|
|
if (ia) unify(da, ia, depth + 1);
|
|
});
|
|
return;
|
|
}
|
|
if (lowerer.checker.isArrayType(declared) && lowerer.checker.isArrayType(inst)) {
|
|
const dElem = lowerer.checker.getTypeArguments(declared as ts.TypeReference)[0];
|
|
const iElem = lowerer.checker.getTypeArguments(inst as ts.TypeReference)[0];
|
|
if (dElem && iElem) unify(dElem, iElem, depth + 1);
|
|
return;
|
|
}
|
|
const dSigs = lowerer.checker.getCallSignatures(declared);
|
|
const iSigs = lowerer.checker.getCallSignatures(inst);
|
|
if (dSigs.length === 1 && iSigs.length === 1) {
|
|
const ds = dSigs[0]!;
|
|
const is = iSigs[0]!;
|
|
const instanceParams = is.getParameters();
|
|
ds.getParameters().forEach((dp, i) => {
|
|
const ip = instanceParams[i];
|
|
if (ip)
|
|
unify(
|
|
lowerer.checker.getTypeOfSymbol(dp),
|
|
lowerer.checker.getTypeOfSymbol(ip),
|
|
depth + 1,
|
|
);
|
|
});
|
|
unify(
|
|
lowerer.checker.getReturnTypeOfSignature(ds),
|
|
lowerer.checker.getReturnTypeOfSignature(is),
|
|
depth + 1,
|
|
);
|
|
return;
|
|
}
|
|
if (declared.flags & ts.TypeFlags.Object) {
|
|
for (const dp of lowerer.checker.getPropertiesOfType(declared)) {
|
|
const ip = lowerer.checker.getPropertyOfType(inst, ts.symbolName(dp));
|
|
if (ip)
|
|
unify(
|
|
lowerer.checker.getTypeOfSymbol(dp),
|
|
lowerer.checker.getTypeOfSymbol(ip),
|
|
depth + 1,
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
const declSig = lowerer.checker.getSignatureFromDeclaration(info.decl);
|
|
if (declSig) {
|
|
const instanceParams = rsig.getParameters();
|
|
declSig.getParameters().forEach((dp, i) => {
|
|
const ip = instanceParams[i];
|
|
if (ip) unify(lowerer.checker.getTypeOfSymbol(dp), lowerer.checker.getTypeOfSymbol(ip));
|
|
});
|
|
unify(
|
|
lowerer.checker.getReturnTypeOfSignature(declSig),
|
|
lowerer.checker.getReturnTypeOfSignature(rsig),
|
|
);
|
|
}
|
|
}
|
|
|
|
/** Lowers ONE monomorphic instance of a generic function: the same body
|
|
* AST, re-lowered with the type parameters bound (threaded into every
|
|
* mapType call via typeParamResolver — the checker keeps reporting the
|
|
* unsubstituted `T`s inside the body). Coverage stats count a base
|
|
* function's statements once: only the FIRST instance contributes. */
|
|
export function lowerGenericInstance(
|
|
lowerer: Lowerer,
|
|
info: GenericFnInfo,
|
|
inst: GenericInstance,
|
|
): IrFunction {
|
|
const decl = info.decl;
|
|
const cls = info.member?.cls ?? null;
|
|
const prevBindings = lowerer.typeParamBindings;
|
|
const previousPath = lowerer.genericInstantiationPath;
|
|
const prevContext = lowerer.instantiationContext;
|
|
const prevSuppress = lowerer.suppressStats;
|
|
const prevClass = lowerer.currentClass;
|
|
const prevImplicit = lowerer.implicitParamTypes;
|
|
const prevStaticFnNames = lowerer.staticFnNames;
|
|
lowerer.staticFnNames = inst.staticFnNames ?? null;
|
|
// A generic METHOD of a generic-class INSTANTIATION lowers under BOTH
|
|
// binding sets: the receiver instantiation's class type parameters
|
|
// underneath, the method instantiation's own on top (disjoint symbol
|
|
// sets — tsc rejects shadowing a class type parameter in a method).
|
|
const clsBindings = cls?.genericInstance?.bindings;
|
|
lowerer.typeParamBindings = clsBindings
|
|
? new Map([...clsBindings, ...inst.bindings])
|
|
: inst.bindings;
|
|
// The ts-level bindings ride along: `T[K]` and literal-keyed `o[k]`
|
|
// reads inside the body resolve through the bound CHECKER types
|
|
// (typeParamTsResolver). Generic-class instantiations carry no
|
|
// ts-level bindings (their type arguments widened at the reference),
|
|
// so only the method instantiation's own map installs.
|
|
const prevTsBindings = lowerer.typeParamTsBindings;
|
|
lowerer.typeParamTsBindings = inst.tsBindings ?? null;
|
|
// Implicit-any instances thread their param bindings through typeOf
|
|
// (the checker reports `any` inside the body — there is no T for
|
|
// mapType to substitute); see the implicit-monomorphization section.
|
|
lowerer.implicitParamTypes =
|
|
info.implicitParams !== undefined ? (inst.implicitArgTypes ?? new Map()) : null;
|
|
lowerer.instantiationContext = `instantiating '${info.baseName}' with ${inst.typeArgsText}`;
|
|
// Coverage counts a generic source body once: re-instantiations of the
|
|
// method AND re-instantiations of the declaring generic class re-visit
|
|
// the same source lines.
|
|
lowerer.suppressStats = inst.ordinal > 0 || (cls?.genericInstance?.ordinal ?? 0) > 0;
|
|
// A generic ASYNC instance is an async IrFunction like any other: the
|
|
// body returns the resolved promise's INNER type, calls enter through
|
|
// the instance's own spawn wrapper (the emitter routes by fn.async),
|
|
// and awaits park this instance's fibers.
|
|
const isAsync = decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true;
|
|
const nameBlame: ts.Node = (ts.isArrowFunction(decl) ? undefined : decl.name) ?? decl;
|
|
const isGenerator = decl.asteriskToken !== undefined;
|
|
if (isAsync && !isGenerator && inst.returnType.kind !== "promise") {
|
|
lowerer.badType(nameBlame, lowerer.checker.getTypeAtLocation(nameBlame));
|
|
}
|
|
// A generic GENERATOR instance mirrors async: the body returns the
|
|
// resolved TReturn channel, calls enter through the instance's own
|
|
// gen-spawn wrapper (the emitter routes by fn.generator).
|
|
if (
|
|
isGenerator &&
|
|
(inst.returnType.kind !== "generator" || (inst.returnType.async === true) !== isAsync)
|
|
) {
|
|
lowerer.badType(nameBlame, lowerer.checker.getTypeAtLocation(nameBlame));
|
|
}
|
|
let bodyReturn = isGenerator
|
|
? lowerer.genBodyReturnType(inst.returnType)
|
|
: lowerer.bodyReturnType(isAsync, inst.returnType);
|
|
const fnCtx = newFnCtx(false, null, null, bodyReturn);
|
|
fnCtx.isAsync = isAsync;
|
|
// Instances lower on demand, on top of whichever caller reached them.
|
|
fnCtx.thisBoundary = true;
|
|
// Implicit-any instances whose declared return did not map lower in
|
|
// return-INFERENCE mode: `return` statements record here bare, and the
|
|
// post-pass (resolveInferredReturn) settles the type and wraps them.
|
|
if (inst.implicitInferReturn) fnCtx.inferReturn = { entries: [] };
|
|
if (cls && info.member!.kind === "method") lowerer.currentClass = cls;
|
|
if (isGenerator && inst.returnType.kind === "generator") {
|
|
fnCtx.generator = generatorMeta(lowerer, inst.returnType);
|
|
}
|
|
lowerer.fnStack.push(fnCtx);
|
|
lowerer.localClassInstantiations.push({ owner: decl, name: inst.name });
|
|
lowerer.genericInstantiationPath = inst.path;
|
|
if (inst.erased) lowerer.erasingDepth++;
|
|
try {
|
|
// Static specializations bind lexical this only when the caller
|
|
// proved its exact receiver. Other instances retain the fence;
|
|
// super remains unsupported. Arrows inherit the lexical receiver.
|
|
if (info.member?.kind === "static" && decl.body) {
|
|
rejectStaticThis(
|
|
lowerer,
|
|
decl.body,
|
|
(keyword) =>
|
|
`'${keyword}' in static methods (it names the RECEIVER class — a dynamic value; reference the class by name instead)`,
|
|
false,
|
|
info.staticReceiver !== undefined,
|
|
);
|
|
}
|
|
const params: IrParam[] = [];
|
|
const receiverInit: IrStmt[] = [];
|
|
if (info.staticReceiver) {
|
|
const value = lowerer.classValueRef(info.staticReceiver, decl);
|
|
const local = lowerer.declareThis(value.type);
|
|
receiverInit.push({ kind: "varDecl", localId: local.id, init: value, loc: locOf(decl) });
|
|
}
|
|
if (cls && info.member!.kind === "method") {
|
|
// Instance methods take `this` as param 0, exactly like plain
|
|
// `%C.method` functions (lowerClassMethodMemberInner).
|
|
const thisType: IrType = { kind: "object", className: cls.def.name };
|
|
const thisLocal = lowerer.declareThis(thisType);
|
|
params.push({ localId: thisLocal.id, name: "this", type: thisType });
|
|
}
|
|
// Default-param initializers lower per instance, with the bindings
|
|
// threaded — a default mentioning T resolves like any body expression.
|
|
const declared = lowerer.declareParams(decl.parameters.nodes, inst.params);
|
|
params.push(...declared.params);
|
|
const body = [...receiverInit, ...declared.prologue];
|
|
const bodyBlock = blockBodyOf(decl);
|
|
if (bodyBlock) {
|
|
body.push(...lowerer.lowerStmts(bodyBlock.statements.nodes));
|
|
if (fnCtx.inferReturn) {
|
|
bodyReturn = resolveInferredReturn(lowerer, inst, fnCtx.inferReturn, body, decl);
|
|
inst.returnType = bodyReturn;
|
|
}
|
|
appendImplicitUndefinedReturn(lowerer, body, bodyReturn, locOf(decl));
|
|
} else if (ts.isArrowFunction(decl) && decl.body !== undefined && !ts.isBlock(decl.body)) {
|
|
// A concise arrow body: the expression IS the return value —
|
|
// generic properties (`id: <T>(x: T) => x`), and implicit-any
|
|
// local arrows (`(cmd) => [cmd.name()].concat(cmd.aliases())`),
|
|
// whose inferred return is simply the expression's own type.
|
|
if (fnCtx.inferReturn) {
|
|
const value = lowerer.lowerExpr(decl.body);
|
|
if (value.type.kind === "void") {
|
|
body.push({ kind: "exprStmt", expr: value, loc: locOf(decl.body) });
|
|
bodyReturn = resolveInferredReturn(lowerer, inst, fnCtx.inferReturn, body, decl);
|
|
inst.returnType = bodyReturn;
|
|
appendImplicitUndefinedReturn(lowerer, body, bodyReturn, locOf(decl));
|
|
} else {
|
|
const stmt: IrStmt = { kind: "return", value, loc: locOf(decl.body) };
|
|
fnCtx.inferReturn.entries.push({ stmt, node: decl.body });
|
|
body.push(stmt);
|
|
bodyReturn = resolveInferredReturn(lowerer, inst, fnCtx.inferReturn, body, decl);
|
|
inst.returnType = bodyReturn;
|
|
}
|
|
} else {
|
|
const value = lowerer.lowerExprExpecting(decl.body, bodyReturn);
|
|
if (bodyReturn.kind === "void") {
|
|
body.push({ kind: "exprStmt", expr: value, loc: locOf(decl.body) });
|
|
} else {
|
|
body.push({ kind: "return", value, loc: locOf(decl.body) });
|
|
}
|
|
}
|
|
} else {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
decl,
|
|
"function declarations whose block body the frontend cannot locate",
|
|
);
|
|
}
|
|
const fn: IrFunction = {
|
|
name: inst.name,
|
|
sourceName: info.baseName,
|
|
...(!isAsync &&
|
|
!decl.asteriskToken &&
|
|
(ts.isFunctionDeclaration(decl) || ts.isFunctionExpression(decl))
|
|
? { ownsPrototype: true as const }
|
|
: {}),
|
|
params,
|
|
returnType: bodyReturn,
|
|
locals: lowerer.ctx.locals,
|
|
body,
|
|
loc: locOf(decl),
|
|
};
|
|
if (isAsync) fn.async = true;
|
|
if (fnCtx.generator) fn.generator = fnCtx.generator;
|
|
return fn;
|
|
} finally {
|
|
lowerer.fnStack.pop();
|
|
lowerer.genericInstantiationPath = previousPath;
|
|
lowerer.currentClass = prevClass;
|
|
lowerer.typeParamBindings = prevBindings;
|
|
lowerer.typeParamTsBindings = prevTsBindings;
|
|
lowerer.implicitParamTypes = prevImplicit;
|
|
lowerer.staticFnNames = prevStaticFnNames;
|
|
lowerer.instantiationContext = prevContext;
|
|
lowerer.localClassInstantiations.pop();
|
|
lowerer.suppressStats = prevSuppress;
|
|
if (inst.erased) lowerer.erasingDepth--;
|
|
}
|
|
}
|
|
|
|
/** A generic function taken as a VALUE, monomorphized by flow. A function
|
|
* value needs ONE concrete signature; tsc pins one at exactly two
|
|
* reference shapes — an instantiation EXPRESSION (`identity<number>`,
|
|
* whose own checker type is the substituted signature) and a reference
|
|
* whose CONTEXTUAL type completes the signature (`const f: (x: number) =>
|
|
* number = identity`, `take(identity)`). The declared signature unifies
|
|
* against the pinned one to recover the bindings; the instance then
|
|
* registers in the SAME table call sites use (one compiled copy per
|
|
* signature however it is reached), and the value is the instance's
|
|
* zero-capture closure — `f === f` holds within an instantiation, the
|
|
* declared-function identity rule. References with no pinning context
|
|
* (the slot keeps `<T>(x: T) => T`) fence by name. */
|
|
export function lowerGenericFnValue(
|
|
lowerer: Lowerer,
|
|
ref: ts.Expression,
|
|
info: GenericFnInfo,
|
|
): IrExpr {
|
|
const loc = locOf(ref);
|
|
// An IMPLICIT-ANY function taken as a VALUE: indirect calls carry no
|
|
// per-site types to bind, so the value is the all-dyn DEFAULT
|
|
// instance's closure — today's compiled body exactly (one interned
|
|
// closure per function, so `f === f` holds like any declaration).
|
|
if (info.implicitParams) {
|
|
const inst = implicitDefaultInstance(lowerer, ref, info);
|
|
const funcType: IrType = funcTypeFromParamShapes(inst.params, inst.returnType);
|
|
lowerer.requireExactArityValue(ref, ref, inst.params, funcType);
|
|
lowerer.noteEdge(inst.name);
|
|
return { kind: "closure", fnName: inst.name, captures: [], type: funcType, loc };
|
|
}
|
|
// Each call of the enclosing function makes a new function object for a
|
|
// binding declared inside it (`make() !== make()` when make returns
|
|
// it), which one shared compiled instance cannot give.
|
|
if (info.functionScoped) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
ref,
|
|
`generic arrow/function-expression bindings declared inside functions as values (each call of the enclosing function makes a new '${info.baseName}' function object, while its compiled instances are shared module functions — call '${info.baseName}' directly, or declare it at module scope)`,
|
|
);
|
|
}
|
|
const fenceUnpinned: () => never = () =>
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
ref,
|
|
`generic functions as values without a pinned concrete signature (annotate the destination — e.g. 'const f: (x: number) => number = ${info.baseName}' — instantiate explicitly ('${info.baseName}<number>'), or call '${info.baseName}' directly)`,
|
|
);
|
|
// The PINNING type: an instantiation expression's own checker type
|
|
// (explicit type arguments applied), else the reference's contextual
|
|
// type — the slot or argument the value flows into. Namespace/CJS
|
|
// member paths delegate the member NAME here (`lib.tag` hands over
|
|
// `tag`), and the checker hangs the contextual type on the whole
|
|
// property access — hop to it.
|
|
const ctxNode =
|
|
ref.parent !== undefined && ts.isPropertyAccessExpression(ref.parent) && ref.parent.name === ref
|
|
? ref.parent
|
|
: ref;
|
|
const pinT = ts.isExpressionWithTypeArguments(ref)
|
|
? lowerer.typeOf(ref)
|
|
: lowerer.checker.getContextualType(ctxNode);
|
|
let target: ts.Signature | null = null;
|
|
if (pinT) {
|
|
const sigs = lowerer.checker.getCallSignatures(pinT);
|
|
if (sigs.length === 1) target = sigs[0]!;
|
|
else if (sigs.length === 0 && pinT.isUnionType()) {
|
|
// A `Fn | undefined`-flavored slot: the value can only inhabit the
|
|
// one callable arm — judge by it (the requireExactArityValue union
|
|
// rule).
|
|
const callable = ts
|
|
.constituentTypes(pinT)
|
|
.map((t) => lowerer.checker.getCallSignatures(t))
|
|
.filter((s) => s.length === 1);
|
|
if (callable.length === 1) target = callable[0]![0]!;
|
|
}
|
|
}
|
|
if (!target) fenceUnpinned();
|
|
return guardEarlyBindingValue(
|
|
lowerer,
|
|
info,
|
|
ref,
|
|
pinnedGenericClosure(lowerer, ref, info, target, fenceUnpinned),
|
|
);
|
|
}
|
|
|
|
/** The value a generic binding declared inside a function stores when a
|
|
* nested function read it before the declaration lowered (`const read =
|
|
* () => id; const id: (x: number) => number = <T>(x: T): T => x`): such
|
|
* a read resolves to an ordinary TDZ local, so the declaration assigns it
|
|
* the instance its concrete declared signature pins. Each evaluation of
|
|
* the declaration makes a new function object, as in JavaScript: the
|
|
* value is a closure over a forwarder to the instance, not the instance's
|
|
* interned closure. */
|
|
export function genericBindingStoredValue(
|
|
lowerer: Lowerer,
|
|
decl: ts.VariableDeclaration,
|
|
info: GenericFnInfo,
|
|
): IrExpr {
|
|
const name = decl.name as ts.Identifier;
|
|
const fenceUnpinned = (): never =>
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
name,
|
|
`generic function bindings read before their declaration without a concrete declared signature (annotate '${info.baseName}' with one, or declare it before its uses)`,
|
|
);
|
|
const sigs = lowerer.checker.getCallSignatures(lowerer.typeOf(name));
|
|
if (sigs.length !== 1) fenceUnpinned();
|
|
const pinned = pinnedGenericClosure(lowerer, name, info, sigs[0]!, fenceUnpinned);
|
|
const async = ts.getModifiers(info.decl)?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword);
|
|
if (pinned.kind !== "closure" || pinned.type.kind !== "func" || pinned.type.rest || async)
|
|
return lowerer.unsupported(
|
|
"SC1090",
|
|
name,
|
|
`async or rest-parameter generic function bindings read before their declaration (each evaluation of '${info.baseName}' makes a new function object)`,
|
|
);
|
|
return {
|
|
...pinned,
|
|
fnName: genericValueForwarder(lowerer, info, pinned.fnName, pinned.type, pinned.loc),
|
|
};
|
|
}
|
|
|
|
/** A lifted function that calls the instance `instance` (of type `type`)
|
|
* with its own arguments. Lifted functions get a new closure object per
|
|
* evaluation; module functions, which instances are, get one interned
|
|
* closure. */
|
|
function genericValueForwarder(
|
|
lowerer: Lowerer,
|
|
info: GenericFnInfo,
|
|
instance: string,
|
|
type: IrType & { kind: "func" },
|
|
at: SrcLoc,
|
|
): string {
|
|
info.valueForwarders ??= new Map();
|
|
const existing = info.valueForwarders.get(instance);
|
|
if (existing !== undefined) return existing;
|
|
const name = `${instance}%value`;
|
|
const loc = helperLoc(at);
|
|
const params: IrParam[] = type.params.map((paramType, i) => ({
|
|
localId: `%arg${i}.0`,
|
|
name: `%arg${i}`,
|
|
type: paramType,
|
|
}));
|
|
const call: IrExpr = {
|
|
kind: "call",
|
|
callee: instance,
|
|
args: params.map((p): IrExpr => ({ kind: "varRef", localId: p.localId, type: p.type, loc })),
|
|
type: type.ret,
|
|
loc,
|
|
};
|
|
lowerer.liftedFns.push({
|
|
name,
|
|
params,
|
|
returnType: type.ret,
|
|
locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
|
|
captures: [],
|
|
...(ts.isFunctionExpression(info.decl) ? { ownsPrototype: true } : {}),
|
|
body:
|
|
type.ret.kind === "void"
|
|
? [
|
|
{ kind: "exprStmt", expr: call, loc },
|
|
{ kind: "return", value: null, loc },
|
|
]
|
|
: [{ kind: "return", value: call, loc }],
|
|
loc,
|
|
});
|
|
lowerer.noteEdge(name);
|
|
info.valueForwarders.set(instance, name);
|
|
return name;
|
|
}
|
|
|
|
/** The closure of the instance of `info` that `target` pins, for a value
|
|
* read at `ref`. `fenceUnpinned` reports a signature that leaves a type
|
|
* parameter unbound. */
|
|
function pinnedGenericClosure(
|
|
lowerer: Lowerer,
|
|
ref: ts.Expression,
|
|
info: GenericFnInfo,
|
|
target: ts.Signature,
|
|
fenceUnpinned: () => never,
|
|
): IrExpr {
|
|
const loc = locOf(ref);
|
|
const bindings = new Map<ts.Symbol, IrType>();
|
|
unifySignatureBindings(lowerer, info, target, bindings);
|
|
const hofBinding = runtimeOptionalHofGenericBinding(lowerer, ref, info);
|
|
if (hofBinding) bindings.set(hofBinding.typeParam, hofBinding.type);
|
|
bindDefaultTypeParams(lowerer, info.typeParams, info.decl.typeParameters?.nodes, bindings);
|
|
// A pinning signature that itself keeps type parameters (`let g: <T>(x:
|
|
// T) => T = identity` — storing the generic signature as such) binds
|
|
// nothing: mapType answers null for an unsubstituted parameter.
|
|
if (info.typeParams.some((tp) => !bindings.get(tp))) fenceUnpinned();
|
|
const inst = genericValueInstance(lowerer, ref, info, bindings);
|
|
// The value's type is the completed ABI signature. Typed rest keeps its
|
|
// packed-array slot; dynRest stays hidden behind the rest marker.
|
|
const funcType: IrType = funcTypeFromParamShapes(inst.params, inst.returnType);
|
|
lowerer.requireExactArityValue(ref, ref, inst.params, funcType);
|
|
lowerer.noteEdge(inst.name);
|
|
return { kind: "closure", fnName: inst.name, captures: [], type: funcType, loc };
|
|
}
|
|
|
|
function runtimeOptionalHofGenericBinding(
|
|
lowerer: Lowerer,
|
|
ref: ts.Expression,
|
|
info: GenericFnInfo,
|
|
): { typeParam: ts.Symbol; type: IrType } | null {
|
|
let argument = ref;
|
|
while (argument.parent && ts.isParenthesizedExpression(argument.parent))
|
|
argument = argument.parent;
|
|
const call = argument.parent;
|
|
if (!ts.isCallExpression(call) || call.arguments.nodes[0] !== argument) return null;
|
|
if (!ts.isPropertyAccessExpression(call.expression) || call.expression.name.text !== "map")
|
|
return null;
|
|
if (!lowerer.isStdlibMember(call.expression)) return null;
|
|
const receiver = lowerer.mapTypeOf(lowerer.typeOf(call.expression.expression));
|
|
if (receiver?.kind !== "array") return null;
|
|
const parameter = info.decl.parameters.nodes[0];
|
|
if (!parameter || !ts.isIdentifier(parameter.name)) return null;
|
|
const parameterType = lowerer.typeOf(parameter.name);
|
|
if ((parameterType.flags & ts.TypeFlags.TypeParameter) === 0) return null;
|
|
const typeParam = parameterType.getSymbol();
|
|
if (!typeParam || !info.typeParams.includes(typeParam)) return null;
|
|
return { typeParam, type: lowerer.runtimeOptionalType(receiver.elem) };
|
|
}
|
|
|
|
/** The erased instance of a generic declaration: every type parameter
|
|
* bound to `unknown`, the function JavaScript runs for any type arguments.
|
|
* Values of generic methods (dynamic dispatch, interface views) call it. */
|
|
export function erasedGenericInstance(
|
|
lowerer: Lowerer,
|
|
blame: ts.Node,
|
|
info: GenericFnInfo,
|
|
): GenericInstance {
|
|
lowerer.erasingDepth++;
|
|
try {
|
|
const instance = genericValueInstance(
|
|
lowerer,
|
|
blame,
|
|
info,
|
|
new Map(info.typeParams.map((tp) => [tp, DYN])),
|
|
);
|
|
instance.erased = true;
|
|
return instance;
|
|
} finally {
|
|
lowerer.erasingDepth--;
|
|
}
|
|
}
|
|
|
|
/** The instance a pinned VALUE reference names: the declaration's modes
|
|
* over the DECLARED types mapped under the bindings (mapType's resolver
|
|
* substitutes — the instance-body trick), which is the same result the
|
|
* call path computes from the resolved signature, so both routes land on
|
|
* one instance per key. */
|
|
function genericValueInstance(
|
|
lowerer: Lowerer,
|
|
ref: ts.Node,
|
|
info: GenericFnInfo,
|
|
bindings: Map<ts.Symbol, IrType>,
|
|
): GenericInstance {
|
|
const prevBindings = lowerer.typeParamBindings;
|
|
const prevContext = lowerer.instantiationContext;
|
|
const rendered = info.typeParams
|
|
.map((tp) => {
|
|
const bound = bindings.get(tp);
|
|
return bound ? lowerer.fmt(bound) : ts.symbolName(tp);
|
|
})
|
|
.join(", ");
|
|
lowerer.typeParamBindings = bindings;
|
|
lowerer.instantiationContext = `instantiating '${info.baseName}' with <${rendered.length > 80 ? rendered.slice(0, 77) + "..." : rendered}>`;
|
|
try {
|
|
const declSig = lowerer.checker.getSignatureFromDeclaration(info.decl);
|
|
if (!declSig) lowerer.unsupported("SC1090", ref, "this function form");
|
|
const params = paramShapes(lowerer, info.decl.parameters.nodes, declSig);
|
|
const retTs = lowerer.checker.getReturnTypeOfSignature(declSig);
|
|
const returnType = lowerer.mapTypeOf(retTs);
|
|
if (!returnType) {
|
|
fenceGenericSignatureResult(lowerer, ref, retTs);
|
|
lowerer.badType(ref, retTs);
|
|
}
|
|
return internGenericInstance(lowerer, ref, info, params, returnType, () => bindings);
|
|
} finally {
|
|
lowerer.typeParamBindings = prevBindings;
|
|
lowerer.instantiationContext = prevContext;
|
|
}
|
|
}
|
|
|
|
/* ── implicit-any monomorphization (npm-static JS) ─────────────────────
|
|
*
|
|
* A JS function whose signature carries UNTYPED parameters is, morally, a
|
|
* generic function: the author wrote it for whatever the call sites pass.
|
|
* Inside an opted-in npm-static package the frontend treats each bindable
|
|
* implicit-any parameter as an implicit TYPE parameter and instantiates
|
|
* the body per call site over the WIDENED checker types of the arguments —
|
|
* the generic-binding machinery verbatim, with two twists:
|
|
*
|
|
* 1. The checker reports `any` INSIDE the body (there is no `T` for
|
|
* mapType to substitute), so the binding threads through the
|
|
* Lowerer's typeOf instead: an identifier reference to a bound param
|
|
* whose checker answer is still `any` answers the bound ts.Type, and
|
|
* every receiver-typed lowering downstream (field targets, method
|
|
* dispatch, narrowing) sees the concrete type. Where tsc's own
|
|
* flow analysis DID narrow the `any` (typeof/instanceof guards), a
|
|
* narrow CONSISTENT with the binding wins (it is the binding, or an
|
|
* arm of it); a contradicting narrow — the statically-dead branch of
|
|
* a typeof dispatch this instantiation cannot take — answers the
|
|
* bound type, so dead branches fence honestly instead of lowering
|
|
* the live value under a lying type.
|
|
* 2. The instance's RETURN type cannot come from the checker when the
|
|
* params poisoned it to `any`: instances lower EAGERLY at first
|
|
* demand (nested body lowering, the lambda discipline) and infer the
|
|
* return from the lowered return statements; same-key recursion
|
|
* observes the checker-fallback type ("pinned") and the post-pass
|
|
* coerces every return to the settled type — per-return fences where
|
|
* a value cannot ride it. Recursive demands are bounded per
|
|
* function, the polymorphic-recursion cap.
|
|
*
|
|
* Bindings are SOUND by construction: the bound type is the argument's own
|
|
* checker type at the call site (never a guess), a param the body ever
|
|
* WRITES is not bindable (it stays checked-dynamic — `options = options
|
|
* || {}` keeps today's story), and an argument whose type does not map
|
|
* statically binds the checked-dynamic DYN — the all-dyn instance IS
|
|
* today's compiled body, so nothing regresses where nothing binds. */
|
|
|
|
/** The npm-static gate: implicit-any monomorphization applies to functions
|
|
* DECLARED in an opted-in package's JS files (user JS keeps today's
|
|
* checked-dynamic story until the corpus is re-baselined). */
|
|
export function implicitMonoFile(sf: ts.SourceFile): boolean {
|
|
return isJsSourceFile(sf) && npmStaticPackageOfPath(sf.fileName) !== null;
|
|
}
|
|
|
|
/** True when the body (or a nested function capturing it) ever WRITES the
|
|
* parameter symbol — assignment, compound assignment, ++/--, a
|
|
* destructuring-assignment target, or a for-in/of cursor. A written
|
|
* param's binding could lie after the write, so it stays dyn. */
|
|
export function paramWrittenInBody(
|
|
lowerer: Lowerer,
|
|
body: ts.Node,
|
|
sym: ts.Symbol,
|
|
name: string,
|
|
): boolean {
|
|
let written = false;
|
|
const targetsSym = (e: ts.Expression): boolean => {
|
|
let n: ts.Expression = e;
|
|
while (ts.isParenthesizedExpression(n)) n = n.expression;
|
|
if (ts.isIdentifier(n) && n.text === name) {
|
|
return lowerer.checker.getSymbolAtLocation(n) === sym;
|
|
}
|
|
// Destructuring-assignment patterns ([a] = xs, {a} = o): any
|
|
// identifier inside the target literal counts (conservative — a
|
|
// nested `a.b` member write through the pattern is a write THROUGH,
|
|
// not a rebind, but patterns are rare enough to over-approximate).
|
|
if (ts.isArrayLiteralExpression(n) || ts.isObjectLiteralExpression(n)) {
|
|
let hit = false;
|
|
const scan = (m: ts.Node): void => {
|
|
if (hit) return;
|
|
if (
|
|
ts.isIdentifier(m) &&
|
|
m.text === name &&
|
|
lowerer.checker.getSymbolAtLocation(m) === sym
|
|
) {
|
|
hit = true;
|
|
return;
|
|
}
|
|
ts.forEachChild(m, scan);
|
|
};
|
|
scan(n);
|
|
return hit;
|
|
}
|
|
return false;
|
|
};
|
|
const walk = (n: ts.Node): void => {
|
|
if (written) return;
|
|
if (ts.isBinaryExpression(n)) {
|
|
const k = n.operatorToken.kind;
|
|
const isAssign = k >= ts.SyntaxKind.FirstAssignment && k <= ts.SyntaxKind.LastAssignment;
|
|
if (isAssign && targetsSym(n.left)) {
|
|
written = true;
|
|
return;
|
|
}
|
|
}
|
|
if (
|
|
(ts.isPrefixUnaryExpression(n) || ts.isPostfixUnaryExpression(n)) &&
|
|
(n.operator === ts.SyntaxKind.PlusPlusToken ||
|
|
n.operator === ts.SyntaxKind.MinusMinusToken) &&
|
|
targetsSym(n.operand)
|
|
) {
|
|
written = true;
|
|
return;
|
|
}
|
|
if (
|
|
(ts.isForInStatement(n) || ts.isForOfStatement(n)) &&
|
|
!ts.isVariableDeclarationList(n.initializer) &&
|
|
ts.isExpression(n.initializer) &&
|
|
targetsSym(n.initializer)
|
|
) {
|
|
written = true;
|
|
return;
|
|
}
|
|
ts.forEachChild(n, walk);
|
|
};
|
|
walk(body);
|
|
return written;
|
|
}
|
|
|
|
function broadPromiseParam(lowerer: Lowerer, type: ts.Type): boolean {
|
|
const mapped = lowerer.mapTypeOf(type);
|
|
const arms =
|
|
mapped?.kind === "union"
|
|
? lowerer.unions.get(mapped.unionId)?.arms
|
|
: mapped === null
|
|
? undefined
|
|
: [mapped];
|
|
return (
|
|
!!arms &&
|
|
arms.some((arm) => arm.kind === "promise" && arm.inner.kind === "dyn") &&
|
|
arms.every((arm) => isUnitType(arm) || (arm.kind === "promise" && arm.inner.kind === "dyn"))
|
|
);
|
|
}
|
|
|
|
/** A destructured object parameter (`function f({ Base, name })`) whose
|
|
* names bind the call site argument's property types. */
|
|
export interface ImplicitPattern {
|
|
elements: { property: string; symbol: ts.Symbol }[];
|
|
}
|
|
|
|
/** The destructured object parameters of a JS function-like that bind like
|
|
* implicit type parameters: every element a plain name (optionally renamed
|
|
* from an identifier or string key) with no default, no rest element, and
|
|
* no write in the body, under a broad inferred type. Parallel to
|
|
* decl.parameters. */
|
|
export function implicitPatternsOf(
|
|
lowerer: Lowerer,
|
|
decl: ts.FunctionDeclaration | ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction,
|
|
): (ImplicitPattern | null)[] {
|
|
return decl.parameters.nodes.map((param): ImplicitPattern | null => {
|
|
if (!ts.isObjectBindingPattern(param.name) || !decl.body) return null;
|
|
if (param.dotDotDotToken || param.questionToken || param.initializer) return null;
|
|
const elements: ImplicitPattern["elements"] = [];
|
|
for (const element of param.name.elements.nodes) {
|
|
if (element.dotDotDotToken || element.initializer || !ts.isIdentifier(element.name))
|
|
return null;
|
|
const key = element.propertyName ?? element.name;
|
|
if (!ts.isIdentifier(key) && !ts.isStringLiteral(key)) return null;
|
|
const symbol = lowerer.checker.getSymbolAtLocation(element.name);
|
|
if (!symbol || paramWrittenInBody(lowerer, decl.body, symbol, element.name.text)) return null;
|
|
// A JSDoc or contextual type names the properties: keep its ABI.
|
|
if ((lowerer.typeOf(element.name).flags & ts.TypeFlags.Any) === 0) return null;
|
|
elements.push({ property: key.text, symbol });
|
|
}
|
|
return elements.length > 0 ? { elements } : null;
|
|
});
|
|
}
|
|
|
|
/** The type a destructured parameter takes at one call site (the object's
|
|
* own representation, a record or a checked value) and the checker type
|
|
* each destructured name binds. An object literal argument types the names
|
|
* from its property expressions, which see the enclosing instance's
|
|
* bindings (`makeClass({ Base, name })` inside a specialized
|
|
* `extend(Base, name)`); any other argument uses its checker type. Null
|
|
* when the argument lacks a destructured property, or passes a class the
|
|
* callee itself defines: each such class would instantiate the callee
|
|
* again (a static `extend` calling `makeClass({ Base: this })`). */
|
|
function implicitPatternBinding(
|
|
lowerer: Lowerer,
|
|
callee: ts.Node,
|
|
pattern: ImplicitPattern,
|
|
arg: ts.Expression,
|
|
): { record: IrType; types: Map<ts.Symbol, ts.Type> } | null {
|
|
let literal: ts.Expression = arg;
|
|
while (ts.isParenthesizedExpression(literal)) literal = literal.expression;
|
|
const types = new Map<ts.Symbol, ts.Type>();
|
|
if (ts.isObjectLiteralExpression(literal)) {
|
|
const values = new Map<string, ts.Expression>();
|
|
/** Shorthand properties name the binding they read. */
|
|
const shorthand = new Map<string, ts.Symbol>();
|
|
for (const property of literal.properties.nodes) {
|
|
if (ts.isShorthandPropertyAssignment(property) && ts.isIdentifier(property.name)) {
|
|
values.set(property.name.text, property.name);
|
|
const read = lowerer.checker.getShorthandAssignmentValueSymbol(property);
|
|
if (read) shorthand.set(property.name.text, read);
|
|
} else if (
|
|
ts.isPropertyAssignment(property) &&
|
|
(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))
|
|
)
|
|
values.set(property.name.text, property.initializer);
|
|
else return null;
|
|
}
|
|
for (const element of pattern.elements) {
|
|
const value = values.get(element.property);
|
|
if (!value) return null;
|
|
const read = shorthand.get(element.property);
|
|
const type = (read && lowerer.implicitParamTypes?.get(read)) ?? lowerer.typeOf(value);
|
|
types.set(element.symbol, lowerer.checker.getBaseTypeOfLiteralType(type));
|
|
}
|
|
// The object keeps the representation the call site lowers it to; the
|
|
// destructured names read their static types back out of it.
|
|
const lowered = tryLowerExpression(lowerer, literal);
|
|
if (lowered?.type.kind !== "record" && lowered?.type.kind !== "dyn") return null;
|
|
if (lowered.type.kind === "record") {
|
|
for (const field of lowerer.shapes.get(lowered.type.shapeId)?.fields ?? [])
|
|
if (field.type.kind === "classval" && classDefinedIn(lowerer, field.type.className, callee))
|
|
return null;
|
|
}
|
|
return { record: lowered.type, types };
|
|
}
|
|
const argType = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(arg));
|
|
const record = lowerer.mapTypeOf(argType);
|
|
if (record?.kind !== "record") return null;
|
|
for (const element of pattern.elements) {
|
|
const property = lowerer.checker.getPropertyOfType(argType, element.property);
|
|
if (!property) return null;
|
|
types.set(element.symbol, lowerer.checker.getTypeOfSymbol(property));
|
|
}
|
|
return { record, types };
|
|
}
|
|
|
|
/** The class is declared inside `scope`. */
|
|
function classDefinedIn(lowerer: Lowerer, className: string, scope: ts.Node): boolean {
|
|
for (
|
|
let n: ts.Node | undefined = lowerer.classes.get(className)?.decl ?? undefined;
|
|
n;
|
|
n = n.parent
|
|
)
|
|
if (n === scope) return true;
|
|
return false;
|
|
}
|
|
|
|
/** The implicit-type-parameter slots of a JS function-like: parallel to
|
|
* decl.parameters, the param SYMBOL where the slot is a bindable
|
|
* implicit param (identifier-named, broad inferred/JSDoc type, not
|
|
* rest/optional/defaulted, never written), null elsewhere. Package JS
|
|
* methods may also specialize class parameters: JSDoc names a nominal
|
|
* class even when the body accepts other objects with the same members.
|
|
* Null overall when nothing qualifies — the declaration keeps its ABI. */
|
|
export function implicitAnyParamSymbolsOf(
|
|
lowerer: Lowerer,
|
|
decl: ts.FunctionDeclaration | ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction,
|
|
classParams = false,
|
|
): (ts.Symbol | null)[] | null {
|
|
if (!decl.body) return null;
|
|
if (decl.asteriskToken) return null;
|
|
if (decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null;
|
|
if (decl.typeParameters !== undefined) return null; // real generics own the machinery
|
|
if (decl.parameters.nodes.length === 0) return null;
|
|
// The variadic-`arguments` form keeps its dynRest story whole.
|
|
if (bodyReadsArguments(decl)) return null;
|
|
let hasImplicitParams = false;
|
|
const overlayTyped = shapeOverlayAnnotated(decl);
|
|
const out = decl.parameters.nodes.map((param): ts.Symbol | null => {
|
|
if (!ts.isIdentifier(param.name)) return null;
|
|
if (param.dotDotDotToken || param.questionToken || param.initializer) return null;
|
|
if (param.name.text === "this") return null;
|
|
// A parameter typed by shape inference's overlay is a proven type,
|
|
// not an implicit type parameter.
|
|
if (overlayTyped && documentedParameter(param)) return null;
|
|
const t = lowerer.typeOf(param.name);
|
|
const broadFunction = lowerer.checker.typeToString(t) === "Function";
|
|
const broadArray = lowerer.checkerAnyArrayType(t);
|
|
const broadPromise = broadPromiseParam(lowerer, t);
|
|
const declaredKind = classParams ? lowerer.mapTypeOf(t)?.kind : undefined;
|
|
const classParam = classParams && (declaredKind === "object" || declaredKind === "array");
|
|
if (
|
|
(t.flags & ts.TypeFlags.Any) === 0 &&
|
|
!broadFunction &&
|
|
!broadArray &&
|
|
!broadPromise &&
|
|
!classParam
|
|
)
|
|
return null;
|
|
const sym = lowerer.checker.getSymbolAtLocation(param.name);
|
|
if (!sym) return null;
|
|
if (paramWrittenInBody(lowerer, decl.body!, sym, param.name.text)) return null;
|
|
hasImplicitParams = true;
|
|
return sym;
|
|
});
|
|
// Destructured object parameters monomorphize too (implicitPatternsOf);
|
|
// their slots stay null here.
|
|
if (!hasImplicitParams && isJsSourceFile(ts.getSourceFileOfNode(decl)))
|
|
hasImplicitParams = implicitPatternsOf(lowerer, decl).some((pattern) => pattern !== null);
|
|
return hasImplicitParams ? out : null;
|
|
}
|
|
|
|
/** The checker-fallback return type an implicit instance PROMISES before
|
|
* its body lowers: the declared/inferred return when it maps statically
|
|
* (JSDoc @returns, `void`, concrete inference the any-params didn't
|
|
* poison) — used as the expected return, no inference — or null, which
|
|
* selects return INFERENCE with DYN as the recursion pin. */
|
|
function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType | null {
|
|
try {
|
|
// Static JS helpers can accept a structurally compatible class despite
|
|
// narrower JSDoc (for example a Vector2 target documented as Vector3).
|
|
// Infer their result from the specialized body as well.
|
|
if (
|
|
info.member?.kind === "static" &&
|
|
info.decl.parameters.nodes.some(
|
|
(param, index) =>
|
|
info.implicitParams?.[index] &&
|
|
lowerer.mapTypeOf(lowerer.typeOf(param.name))?.kind === "object",
|
|
)
|
|
)
|
|
return null;
|
|
const broadCallbackSymbols = new Set(
|
|
info.decl.parameters.nodes.flatMap((param, index) => {
|
|
const symbol = info.implicitParams?.[index];
|
|
return symbol !== null &&
|
|
symbol !== undefined &&
|
|
lowerer.checker.typeToString(lowerer.typeOf(param.name)) === "Function"
|
|
? [symbol]
|
|
: [];
|
|
}),
|
|
);
|
|
let returnsBroadCallback = false;
|
|
if (broadCallbackSymbols.size > 0 && info.decl.body !== undefined) {
|
|
ts.walkPreorder(info.decl.body, (node) => {
|
|
if (node !== info.decl.body && ts.isFunctionLike(node)) return "skip";
|
|
if (!ts.isReturnStatement(node) || node.expression === undefined) return undefined;
|
|
let expression = node.expression;
|
|
while (ts.isParenthesizedExpression(expression)) expression = expression.expression;
|
|
if (
|
|
ts.isCallExpression(expression) &&
|
|
ts.isIdentifier(expression.expression) &&
|
|
(() => {
|
|
const symbol = lowerer.checker.getSymbolAtLocation(expression.expression);
|
|
return symbol !== undefined && broadCallbackSymbols.has(symbol);
|
|
})()
|
|
) {
|
|
returnsBroadCallback = true;
|
|
return "stop";
|
|
}
|
|
return undefined;
|
|
});
|
|
}
|
|
if (returnsBroadCallback) return null;
|
|
const declSig = lowerer.checker.getSignatureFromDeclaration(info.decl);
|
|
if (!declSig) return null;
|
|
const retTs = lowerer.checker.getReturnTypeOfSignature(declSig);
|
|
if (retTs.flags & ts.TypeFlags.Any) return null;
|
|
if (retTs.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) return DYN;
|
|
if (lowerer.checker.isArrayType(retTs)) {
|
|
// Inferred JavaScript arrays can hold values selected through a
|
|
// mutable constructor registry. The checker may retain only one
|
|
// constructor's element type; settle the result from the lowered
|
|
// container instead of checking every element against that guess.
|
|
if (!hasExplicitJsDocReturn(info.decl)) return null;
|
|
if (
|
|
info.decl.parameters.nodes.some(
|
|
(param, index) =>
|
|
info.implicitParams?.[index] &&
|
|
lowerer.mapTypeOf(lowerer.typeOf(param.name))?.kind === "array",
|
|
)
|
|
)
|
|
return null;
|
|
const elem = lowerer.checker.getTypeArguments(retTs as ts.TypeReference)[0];
|
|
// An implicit-any parameter poisons an inferred array result to
|
|
// any[] even when this instance's body produces one concrete
|
|
// element type (`knownBy(cmd)` in commander). Let the existing
|
|
// lowered-return unifier recover that type. Explicit unknown[]
|
|
// remains a declared checked-dynamic contract and stays pinned.
|
|
if (elem && (elem.flags & ts.TypeFlags.Any) !== 0) return null;
|
|
}
|
|
const mapped = lowerer.mapTypeOf(retTs);
|
|
// Mixed inferred results can select a closure or a class instance.
|
|
// Their callable ABI is settled by the lowered body, including native
|
|
// classes whose reflected calls return checked values.
|
|
if (
|
|
mapped?.kind === "union" &&
|
|
!hasExplicitJsDocReturn(info.decl) &&
|
|
lowerer.unions.get(mapped.unionId)?.arms.some((arm) => arm.kind === "func")
|
|
)
|
|
return null;
|
|
if (mapped?.kind === "symbol" && !hasExplicitJsDocReturn(info.decl)) return null;
|
|
if (mapped?.kind === "date" && !hasExplicitJsDocReturn(info.decl)) return DYN;
|
|
if (!hasExplicitJsDocReturn(info.decl) && info.decl.body) {
|
|
let openReturn = false;
|
|
ts.walkPreorder(info.decl.body, (node) => {
|
|
if (node !== info.decl.body && ts.isFunctionLike(node)) return "skip";
|
|
if (!ts.isReturnStatement(node) || !node.expression || !ts.isIdentifier(node.expression))
|
|
return undefined;
|
|
const symbol = lowerer.resolveValueSymbol(node.expression);
|
|
if (
|
|
symbol &&
|
|
symbol.declarations.some(
|
|
(decl) => ts.isVariableDeclaration(decl) && jsBindingHasOpenWrites(lowerer, decl),
|
|
)
|
|
)
|
|
openReturn = true;
|
|
return undefined;
|
|
});
|
|
if (openReturn) return null;
|
|
}
|
|
// A JavaScript result inferred from an initial record can later
|
|
// carry additional fields. Settle its representation from the
|
|
// lowered returns instead of copying it back into that initial shape.
|
|
const resultArms =
|
|
mapped?.kind === "union" ? lowerer.unions.get(mapped.unionId)?.arms : mapped ? [mapped] : [];
|
|
if (
|
|
resultArms?.some(
|
|
(arm) => arm.kind === "record" || arm.kind === "object" || arm.kind === "classval",
|
|
) &&
|
|
resultArms.every(
|
|
(arm) =>
|
|
isUnitType(arm) ||
|
|
arm.kind === "record" ||
|
|
arm.kind === "object" ||
|
|
arm.kind === "classval",
|
|
) &&
|
|
!hasExplicitJsDocReturn(info.decl)
|
|
)
|
|
return null;
|
|
// Inferred variadic results carry runtime argument packs. Their
|
|
// factory must retain the lowered callable rather than extracting a
|
|
// fixed native signature from a checked value.
|
|
if (
|
|
mapped?.kind === "func" &&
|
|
mapped.rest === true &&
|
|
mapped.restAbi === undefined &&
|
|
!hasExplicitJsDocReturn(info.decl)
|
|
)
|
|
return null;
|
|
if (mapped !== null && promiseCarriesDyn(lowerer, mapped) && !hasExplicitJsDocReturn(info.decl))
|
|
return null;
|
|
// A JavaScript body that can complete without returning yields
|
|
// undefined there, whatever its JSDoc claims (bcryptjs's `hash`
|
|
// returns a promise only when no callback is passed), exactly as
|
|
// declaredReturnType widens non-generic JavaScript functions.
|
|
if (
|
|
mapped !== null &&
|
|
isJsSourceFile(ts.getSourceFileOfNode(info.decl)) &&
|
|
functionCanReturnUndefined(info.decl)
|
|
)
|
|
return lowerer.withUndefinedArmOf(mapped) ?? mapped;
|
|
return mapped;
|
|
} catch (e) {
|
|
if (!(e instanceof PoisonError)) throw e;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** True when a `this.method(...)` call targets an implicit npm-static
|
|
* method whose result is settled from its lowered body rather than the
|
|
* checker declaration. Uninitialized-local inference uses this to avoid
|
|
* freezing an evolving `any` slot to stale JSDoc. */
|
|
export function implicitMethodCallInfersReturn(lowerer: Lowerer, call: ts.CallExpression): boolean {
|
|
if (
|
|
!ts.isPropertyAccessExpression(call.expression) ||
|
|
call.expression.expression.kind !== ts.SyntaxKind.ThisKeyword ||
|
|
lowerer.currentClass === null
|
|
) {
|
|
return false;
|
|
}
|
|
const found = findGenericMethodOn(lowerer, lowerer.currentClass, call.expression.name.text);
|
|
return (
|
|
found?.info.implicitParams !== undefined && implicitDeclaredReturn(lowerer, found.info) === null
|
|
);
|
|
}
|
|
|
|
/** True when an IR type may BIND an implicit param (a concrete static
|
|
* type — the checked-dynamic kinds keep the dyn slot, units have no
|
|
* standalone representation). */
|
|
function bindableImplicitIr(t: IrType | null): t is IrType {
|
|
return (
|
|
t !== null &&
|
|
t.kind !== "void" &&
|
|
t.kind !== "dyn" &&
|
|
t.kind !== "caught" &&
|
|
t.kind !== "undefinedT" &&
|
|
t.kind !== "nullT"
|
|
);
|
|
}
|
|
|
|
/** A represented binding or direct callable result can be wider than the
|
|
* checker's inferred shape. Preserve that boundary when specializing JS. */
|
|
function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrType | null {
|
|
while (ts.isParenthesizedExpression(arg)) arg = arg.expression;
|
|
if (
|
|
isJsSourceFile(ts.getSourceFileOfNode(arg)) &&
|
|
(ts.isArrowFunction(arg) || ts.isFunctionExpression(arg))
|
|
) {
|
|
// A closure lowered here before (another call site's instance key, or
|
|
// a repeated demand) keeps its value for the argument's own lowering.
|
|
const known = lowerer.speculatedClosure(arg);
|
|
if (known !== null) return known.type;
|
|
const saved = lowerer.diagSink;
|
|
const diagnostics: typeof lowerer.diags = [];
|
|
lowerer.diagSink = diagnostics;
|
|
let value: IrExpr | null;
|
|
try {
|
|
value = tryLowerExpression(lowerer, arg);
|
|
} finally {
|
|
lowerer.diagSink = saved;
|
|
}
|
|
// Direct inferred helpers can receive native callbacks even when their
|
|
// result is an opaque handle that cannot cross checked storage. The
|
|
// argument's own lowering then takes this value instead of lowering
|
|
// the closure a second time.
|
|
if (diagnostics.length === 0 && value?.type.kind === "func") {
|
|
lowerer.speculatedClosures.set(arg, {
|
|
ctx: lowerer.ctx,
|
|
bindings: lowerer.typeParamBindings,
|
|
value,
|
|
});
|
|
return value.type;
|
|
}
|
|
}
|
|
if (arg.kind === ts.SyntaxKind.ThisKeyword) return lowerer.resolveThis()?.type ?? null;
|
|
if (ts.isAwaitExpression(arg)) {
|
|
const awaited = storedImplicitArgumentType(lowerer, arg.expression);
|
|
return awaited?.kind === "promise" ? awaited.inner : awaited;
|
|
}
|
|
if (
|
|
ts.isObjectLiteralExpression(arg) &&
|
|
isJsSourceFile(ts.getSourceFileOfNode(arg)) &&
|
|
(arg.properties.nodes.length === 0 ||
|
|
arg.properties.nodes.some(
|
|
(property) =>
|
|
ts.isMethodDeclaration(property) ||
|
|
ts.isAccessor(property) ||
|
|
(!ts.isSpreadAssignment(property) &&
|
|
property.name !== undefined &&
|
|
ts.isComputedPropertyName(property.name)),
|
|
))
|
|
)
|
|
return DYN;
|
|
if (ts.isIdentifier(arg))
|
|
return lowerer.peekLocal(arg)?.type ?? lowerer.globalOf(arg)?.type ?? null;
|
|
if (
|
|
isJsSourceFile(ts.getSourceFileOfNode(arg)) &&
|
|
(ts.isPropertyAccessExpression(arg) || ts.isElementAccessExpression(arg))
|
|
) {
|
|
// Mutable JS members can retain an initializer-only checker type (for
|
|
// example a linked-list slot initially null). Specialize to the value
|
|
// we actually represent, rather than checking it back into that type.
|
|
const represented = tryLowerExpression(lowerer, arg);
|
|
if (represented) return represented.type;
|
|
}
|
|
if (
|
|
isJsSourceFile(ts.getSourceFileOfNode(arg)) &&
|
|
(ts.isCallExpression(arg) ||
|
|
ts.isConditionalExpression(arg) ||
|
|
(ts.isBinaryExpression(arg) &&
|
|
(arg.operatorToken.kind === ts.SyntaxKind.BarBarToken ||
|
|
arg.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
|
|
arg.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken)))
|
|
) {
|
|
// Calls and fallback expressions can retain checked storage wider than
|
|
// the checker's inference (for example Object.assign on a JS instance).
|
|
const represented = tryLowerExpression(lowerer, arg);
|
|
if (represented?.type.kind === "dyn" || represented?.type.kind === "func")
|
|
return represented.type;
|
|
}
|
|
if (ts.isCallExpression(arg) && ts.isIdentifier(arg.expression)) {
|
|
const callee = storedImplicitArgumentType(lowerer, arg.expression);
|
|
if (callee?.kind === "dyn") return DYN;
|
|
if (callee?.kind === "func") return callee.ret;
|
|
return lowerer.fnSigOf(arg.expression)?.returnType ?? null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** The instance a CALL of an implicit-any function-like names: each
|
|
* bindable implicit param takes the call's WIDENED argument checker type
|
|
* when it maps statically (DYN otherwise — today's slot), typed params
|
|
* keep their declared shapes, and the param-type tuple is the
|
|
* instantiation key. New keys lower EAGERLY (return inference — see the
|
|
* section comment); a same-key re-demand mid-lowering pins the fallback
|
|
* return type. */
|
|
export function implicitCallInstance(
|
|
lowerer: Lowerer,
|
|
call: ts.CallExpression,
|
|
info: GenericFnInfo,
|
|
): GenericInstance {
|
|
const shapes: ParamShape[] = [];
|
|
const argTypes = new Map<ts.Symbol, ts.Type>();
|
|
const patterns = (info.implicitPatterns ??= implicitPatternsOf(lowerer, info.decl));
|
|
/** The destructured names' bound types, which the object's own
|
|
* representation does not always carry. */
|
|
const patternKey: string[] = [];
|
|
info.decl.parameters.nodes.forEach((param, i) => {
|
|
const pattern = patterns[i];
|
|
const patternArg = call.arguments.nodes[i];
|
|
if (pattern && patternArg && !ts.isSpreadElement(patternArg)) {
|
|
// The argument's object type binds each destructured name, so a
|
|
// class passed as `{ Base }` keeps its static class type.
|
|
const bound = implicitPatternBinding(lowerer, info.decl, pattern, patternArg);
|
|
if (bound) {
|
|
for (const [symbol, type] of bound.types) {
|
|
argTypes.set(symbol, type);
|
|
const mapped = lowerer.mapTypeOf(type);
|
|
patternKey.push(mapped ? typeKey(mapped) : "?");
|
|
}
|
|
shapes.push({ type: bound.record, mode: "required" });
|
|
return;
|
|
}
|
|
}
|
|
const sym = info.implicitParams![i];
|
|
if (!sym) {
|
|
shapes.push(lowerer.paramShape(param));
|
|
return;
|
|
}
|
|
let bound: IrType = DYN;
|
|
const declared = lowerer.paramShape(param);
|
|
if ((lowerer.typeOf(param.name).flags & ts.TypeFlags.Any) === 0) bound = declared.type;
|
|
const arg = call.arguments.nodes[i];
|
|
if (arg && !ts.isSpreadElement(arg)) {
|
|
const stored = storedImplicitArgumentType(lowerer, arg);
|
|
if (stored?.kind === "dyn") {
|
|
bound = DYN;
|
|
argTypes.set(sym, lowerer.checker.getUnknownType());
|
|
shapes.push({ type: bound, mode: "required" });
|
|
return;
|
|
}
|
|
// `this` in a static method specialized for its receiver class is
|
|
// that class: bind it as a call naming the class would.
|
|
if (stored?.kind === "classval" && arg.kind === ts.SyntaxKind.ThisKeyword) {
|
|
const classDecl = lowerer.classes.get(stored.className)?.decl;
|
|
const classType = classDecl?.name ? lowerer.typeOf(classDecl.name) : null;
|
|
if (classType) {
|
|
argTypes.set(sym, classType);
|
|
shapes.push({ type: stored, mode: "required" });
|
|
return;
|
|
}
|
|
}
|
|
// A represented native array must keep its storage when a direct
|
|
// inferred helper receives it. Re-boxing a checker-any array would
|
|
// copy it, losing writes (and attempting to read unfilled slots).
|
|
if (stored?.kind === "array") {
|
|
argTypes.set(sym, lowerer.typeOf(arg));
|
|
shapes.push({ type: stored, mode: "required" });
|
|
return;
|
|
}
|
|
if (stored?.kind === "func") {
|
|
// The lowered closure may have an inferred JS destructuring
|
|
// parameter widened from the checker's exact tuple to dyn.
|
|
// Preserve that signature throughout the specialization.
|
|
const checkerType = lowerer.mapTypeOf(lowerer.typeOf(arg));
|
|
argTypes.set(
|
|
sym,
|
|
checkerType && typeEquals(checkerType, stored)
|
|
? lowerer.typeOf(arg)
|
|
: lowerer.checker.getUnknownType(),
|
|
);
|
|
shapes.push({ type: stored, mode: "required" });
|
|
return;
|
|
}
|
|
// The argument's own checker type, literal-widened ('add' binds
|
|
// string) — typeOf consults the ACTIVE instance's bindings, so a
|
|
// bound param forwarded into another implicit call transitively
|
|
// instantiates it (this._initCommandGroup(command)).
|
|
const t = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(arg));
|
|
const mapped =
|
|
(neverTaintedJsType(lowerer, arg, t) ? null : lowerer.mapTypeOf(t)) ??
|
|
dynFallbackType(lowerer, arg, t);
|
|
if (
|
|
isJsSourceFile(ts.getSourceFileOfNode(arg)) &&
|
|
ts.isArrayLiteralExpression(arg) &&
|
|
mapped?.kind === "array" &&
|
|
(mapped.elem.kind === "classval" ||
|
|
(mapped.elem.kind === "union" &&
|
|
lowerer.unions.get(mapped.elem.unionId)?.arms.every((arm) => arm.kind === "classval")))
|
|
) {
|
|
argTypes.set(sym, lowerer.checker.getUnknownType());
|
|
shapes.push({ type: DYN, mode: "required" });
|
|
return;
|
|
}
|
|
// JavaScript arguments-based callables travel as checked functions.
|
|
// Specializing a parameter to the inferred rest signature would
|
|
// require extracting a native variadic ABI that the value does not
|
|
// expose. Its checked invocation preserves the complete argument pack.
|
|
if (mapped?.kind === "func" && mapped.rest === true && mapped.restAbi === undefined) {
|
|
argTypes.set(sym, lowerer.checker.getUnknownType());
|
|
shapes.push({ type: DYN, mode: "required" });
|
|
return;
|
|
}
|
|
// JS inference can absorb heterogeneous loop values into `{}`.
|
|
// That checker supertype is not an empty-record runtime layout:
|
|
// preserve represented arms through the checked boundary instead
|
|
// of specializing a call to a union that would discard them.
|
|
if (
|
|
isJsSourceFile(ts.getSourceFileOfNode(arg)) &&
|
|
mapped?.kind === "func" &&
|
|
!lowerer.dynConvertible(mapped)
|
|
) {
|
|
argTypes.set(sym, lowerer.checker.getUnknownType());
|
|
shapes.push({ type: DYN, mode: "required" });
|
|
return;
|
|
}
|
|
const mappedArms =
|
|
mapped?.kind === "union"
|
|
? (lowerer.unions.get(mapped.unionId)?.arms ?? [])
|
|
: mapped
|
|
? [mapped]
|
|
: [];
|
|
const hasTopObject = mappedArms.some((arm) => {
|
|
const shape = arm.kind === "record" ? lowerer.shapes.get(arm.shapeId) : undefined;
|
|
return shape && !shape.tuple && !shape.indexValue && shape.fields.length === 0;
|
|
});
|
|
if (
|
|
stored?.kind === "union" &&
|
|
hasTopObject &&
|
|
lowerer.dynConvertible(stored) &&
|
|
lowerer.unions
|
|
.get(stored.unionId)
|
|
?.arms.some(
|
|
(arm) =>
|
|
!isUnitType(arm) &&
|
|
!mappedArms.some((target) => lowerer.widthLiftPlan(arm, target) !== null),
|
|
)
|
|
) {
|
|
argTypes.set(sym, lowerer.checker.getUnknownType());
|
|
shapes.push({ type: DYN, mode: "required" });
|
|
return;
|
|
}
|
|
if (
|
|
ts.isArrowFunction(arg) &&
|
|
arg.type === undefined &&
|
|
mapped?.kind === "func" &&
|
|
promiseCarriesDyn(lowerer, mapped.ret)
|
|
) {
|
|
bound = { ...mapped, ret: DYN };
|
|
lowerer.runtimeOptionalFunctionReturns.set(arg, DYN);
|
|
argTypes.set(sym, t);
|
|
} else if (mapped?.kind === "dyn") {
|
|
bound = DYN;
|
|
argTypes.set(sym, t);
|
|
} else if (bindableImplicitIr(mapped)) {
|
|
bound = mapped;
|
|
argTypes.set(sym, t);
|
|
}
|
|
}
|
|
shapes.push({ type: bound, mode: "required" });
|
|
});
|
|
return internImplicitInstance(lowerer, call, info, shapes, argTypes, patternKey.join(","));
|
|
}
|
|
|
|
/** The all-dyn DEFAULT instance — today's compiled body exactly: what a
|
|
* VALUE reference of an implicit-any function names (indirect calls
|
|
* carry no per-site types to bind). */
|
|
export function implicitDefaultInstance(
|
|
lowerer: Lowerer,
|
|
blame: ts.Node,
|
|
info: GenericFnInfo,
|
|
): GenericInstance {
|
|
const shapes: ParamShape[] = info.decl.parameters.nodes.map((param, i) =>
|
|
info.implicitParams![i] ? { type: DYN, mode: "required" as const } : lowerer.paramShape(param),
|
|
);
|
|
const argTypes = new Map<ts.Symbol, ts.Type>();
|
|
// Newly bindable nominal class parameters must not retain their JSDoc
|
|
// receiver type in the checked fallback. Keep existing broad JS inference.
|
|
for (let i = 0; i < info.decl.parameters.nodes.length; i++) {
|
|
const symbol = info.implicitParams?.[i];
|
|
const kind = lowerer.mapTypeOf(lowerer.typeOf(info.decl.parameters.nodes[i]!.name))?.kind;
|
|
if (symbol && (kind === "object" || kind === "array")) {
|
|
argTypes.set(symbol, lowerer.checker.getUnknownType());
|
|
}
|
|
}
|
|
return internImplicitInstance(lowerer, blame, info, shapes, argTypes);
|
|
}
|
|
|
|
export function internImplicitInstance(
|
|
lowerer: Lowerer,
|
|
blame: ts.Node,
|
|
info: GenericFnInfo,
|
|
shapes: ParamShape[],
|
|
argTypes: Map<ts.Symbol, ts.Type>,
|
|
patternKey = "",
|
|
declaredReturn?: IrType,
|
|
): GenericInstance {
|
|
return internImplicitInstanceOr(
|
|
lowerer,
|
|
blame,
|
|
info,
|
|
shapes,
|
|
argTypes,
|
|
patternKey,
|
|
declaredReturn,
|
|
false,
|
|
)!;
|
|
}
|
|
|
|
/** internImplicitInstance; with `fallible`, a body that does not lower
|
|
* leaves no instance and no diagnostics, and the answer is null (the caller
|
|
* keeps its own lowering). */
|
|
export function internImplicitInstanceOr(
|
|
lowerer: Lowerer,
|
|
blame: ts.Node,
|
|
info: GenericFnInfo,
|
|
shapes: ParamShape[],
|
|
argTypes: Map<ts.Symbol, ts.Type>,
|
|
patternKey: string,
|
|
declaredReturn: IrType | undefined,
|
|
fallible: boolean,
|
|
): GenericInstance | null {
|
|
const key = shapes.map((s) => typeKey(s.type)).join(",") + (patternKey ? `|${patternKey}` : "");
|
|
let inst = info.instances.get(key);
|
|
if (inst?.implicitState === "failed") {
|
|
if (fallible) return null;
|
|
lowerer.unsupported("SC1090", blame, `the failed specialization of '${info.baseName}'`);
|
|
}
|
|
if (inst) {
|
|
// A recursive caller has observed this ABI before inference settles.
|
|
// Keep the promised return slot and coerce the completed body's returns.
|
|
if (inst.implicitState === "lowering") inst.returnPinned = true;
|
|
return inst;
|
|
}
|
|
const path = extendInstantiationPath(lowerer.genericInstantiationPath, info.decl);
|
|
if (!path) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
blame,
|
|
`unbounded implicit-any instantiation ('${info.baseName}' exceeded ` +
|
|
`${MAX_INSTANTIATION_RECURSION} recursive specializations on one demand path)`,
|
|
);
|
|
}
|
|
const rendered = shapes.map((s) => lowerer.fmt(s.type)).join(", ");
|
|
const dynamicBroadPromise = info.decl.parameters.nodes.some(
|
|
(param, index) =>
|
|
info.implicitParams?.[index] !== null &&
|
|
info.implicitParams?.[index] !== undefined &&
|
|
shapes[index]?.type.kind === "dyn" &&
|
|
broadPromiseParam(lowerer, lowerer.typeOf(param.name)),
|
|
);
|
|
const declared =
|
|
declaredReturn ?? (dynamicBroadPromise ? null : implicitDeclaredReturn(lowerer, info));
|
|
inst = {
|
|
path,
|
|
name: keyedHelperName(lowerer, `${info.qualifiedName}%`, key, ""),
|
|
ordinal: info.instances.size,
|
|
params: shapes,
|
|
// The promise callers rely on before the body settles it: the
|
|
// declared truth when it maps, else DYN (the recursion pin — and
|
|
// exactly today's checked-dynamic result slot).
|
|
returnType: declared ?? DYN,
|
|
bindings: new Map(),
|
|
typeArgsText: `(${rendered.length > 80 ? rendered.slice(0, 77) + "..." : rendered})`,
|
|
implicitArgTypes: argTypes,
|
|
implicitState: "lowering",
|
|
...(declared === null ? { implicitInferReturn: true as const } : {}),
|
|
};
|
|
info.instances.set(key, inst);
|
|
// Eager lowering settles the return ABI before callers use it. A failed
|
|
// JS body must still have a throwing implementation: later references
|
|
// (including recursive ones) can already hold its cached signature.
|
|
const diagsBefore = lowerer.diags.length;
|
|
const fencesBefore = lowerer.runtimeFences.length;
|
|
const totalBefore = lowerer.stats.statementsTotal;
|
|
const failedBefore = lowerer.stats.statementsFailed;
|
|
const skippedBefore = lowerer.stats.functionsSkipped;
|
|
const blockedBefore = fallible ? new Set(lowerer.blockedBindings) : null;
|
|
const deferredBefore = fallible ? new Set(lowerer.deferredDiags.keys()) : null;
|
|
/** Undoes what a failed fallible body recorded: the instance's
|
|
* statements share their symbols with the declaration's own body. */
|
|
const rollback = (): void => {
|
|
lowerer.diags.splice(diagsBefore);
|
|
lowerer.runtimeFences.splice(fencesBefore);
|
|
lowerer.stats.statementsTotal = totalBefore;
|
|
lowerer.stats.statementsFailed = failedBefore;
|
|
lowerer.stats.functionsSkipped = skippedBefore;
|
|
for (const symbol of [...lowerer.blockedBindings])
|
|
if (!blockedBefore!.has(symbol)) lowerer.blockedBindings.delete(symbol);
|
|
for (const symbol of [...lowerer.deferredDiags.keys()])
|
|
if (!deferredBefore!.has(symbol)) lowerer.deferredDiags.delete(symbol);
|
|
};
|
|
// The instance is cached even when a receiver probe requested it.
|
|
// Its body must retain statement fences instead of dropping failures
|
|
// into that probe's temporary diagnostic sink and caching empty code.
|
|
const previousSink = lowerer.diagSink;
|
|
lowerer.diagSink = null;
|
|
try {
|
|
// Implicit-any instances lower EAGERLY at the call site so their
|
|
// inferred return type is available immediately. They therefore do
|
|
// not pass through emitReachable's queued generic-instance wave,
|
|
// which normally batches checker work before lowering a body. Prime
|
|
// this exact committed instance body here; repeated instantiations of
|
|
// the same declaration find the facade memos warm.
|
|
lowerer.checker.prefetchRoots([
|
|
...info.decl.parameters.nodes.flatMap((param) =>
|
|
param.initializer ? [param.initializer] : [],
|
|
),
|
|
...(info.decl.body ? [info.decl.body] : []),
|
|
]);
|
|
const fn = lowerer.lowerGenericInstance(info, inst);
|
|
if (
|
|
fallible &&
|
|
(lowerer.diags.length > diagsBefore || lowerer.runtimeFences.length > fencesBefore)
|
|
) {
|
|
inst.implicitState = "failed";
|
|
rollback();
|
|
return null;
|
|
}
|
|
lowerer.implicitFns.push(fn);
|
|
} catch (e) {
|
|
if (!(e instanceof PoisonError)) throw e;
|
|
inst.implicitState = "failed";
|
|
if (fallible) {
|
|
rollback();
|
|
return null;
|
|
}
|
|
const types: IrType[] = shapes.map((shape) => shape.type);
|
|
if (info.member?.kind === "method")
|
|
types.unshift({ kind: "object", className: info.member.cls.def.name });
|
|
const fn = lowerer.deferToRuntimeFence(diagsBefore, info.decl, {
|
|
kind: "function",
|
|
name: inst.name,
|
|
returnType: inst.returnType,
|
|
params: types.map((type, index) => ({ localId: `p.${index}`, name: `p${index}`, type })),
|
|
});
|
|
if (!fn) throw e;
|
|
lowerer.implicitFns.push(fn);
|
|
} finally {
|
|
lowerer.diagSink = previousSink;
|
|
}
|
|
inst.implicitState = "done";
|
|
return inst;
|
|
}
|
|
|
|
/** The implicit-any twin of bindingGenericFnNodeOf, for LOCAL and module
|
|
* bindings alike (`const knownBy = (cmd) => [cmd.name()].concat(...)`
|
|
* inside a method body — commander's _registerCommand shape): the
|
|
* initializer function-like when the WHOLE declaration qualifies for
|
|
* implicit monomorphization, else null — non-qualifying shapes keep
|
|
* today's closure story without a diagnostic (no fence: the flag must not make
|
|
* working code worse). Qualification: an npm-static JS file, a const (or
|
|
* never-reassigned, never-redeclared) identifier binding, an
|
|
* arrow/function-expression initializer with bindable implicit-any
|
|
* params, and a body with NO captures — no `this`/`super`, and no
|
|
* reference to a function-scoped declaration outside itself (compiled
|
|
* instances are module functions; module-scope references are fine).
|
|
* Cached per declaration on lowerer.implicitLocalFns. */
|
|
export function implicitLocalFnNodeOf(
|
|
lowerer: Lowerer,
|
|
decl: ts.VariableDeclaration,
|
|
): ts.FunctionExpression | ts.ArrowFunction | null {
|
|
const cached = lowerer.implicitLocalFns.get(decl);
|
|
if (cached !== undefined)
|
|
return cached ? (cached.decl as ts.FunctionExpression | ts.ArrowFunction) : null;
|
|
const probe = (): ts.FunctionExpression | ts.ArrowFunction | null => {
|
|
if (!implicitMonoFile(ts.getSourceFileOfNode(decl))) return null;
|
|
if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return null;
|
|
let init: ts.Expression = decl.initializer;
|
|
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
|
if (!ts.isArrowFunction(init) && !ts.isFunctionExpression(init)) return null;
|
|
if (init.typeParameters !== undefined || init.body === undefined) return null;
|
|
if (!implicitAnyParamSymbolsOf(lowerer, init)) return null;
|
|
const sym = lowerer.checker.getSymbolAtLocation(decl.name);
|
|
if (!sym) return null;
|
|
const isConst = (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0;
|
|
const redeclared = sym.declarations.some(
|
|
(d) => d !== decl && ts.isVariableDeclaration(d) && d.initializer !== undefined,
|
|
);
|
|
if (redeclared) return null;
|
|
if (!isConst && !bindingNeverReassigned(lowerer, sym, decl)) return null;
|
|
// Instances are module functions with no frame.
|
|
return capturesEnclosingFrame(lowerer, init, decl, init.body) ? null : init;
|
|
};
|
|
const node = probe();
|
|
if (node === null) {
|
|
lowerer.implicitLocalFns.set(decl, null);
|
|
return null;
|
|
}
|
|
return node;
|
|
}
|
|
|
|
/** True when `fn`, bound by `decl`, uses its enclosing function's frame
|
|
* somewhere in `scope`: `this`, `super`, `arguments` or `new.target`, or
|
|
* a value or type declared in an enclosing function (a local, a parameter,
|
|
* an enclosing generic's type parameter). Module-scope declarations and
|
|
* the binding itself are reachable from the module functions the
|
|
* instances compile to. */
|
|
function capturesEnclosingFrame(
|
|
lowerer: Lowerer,
|
|
fn: ts.FunctionExpression | ts.ArrowFunction,
|
|
decl: ts.VariableDeclaration,
|
|
scope: ts.Node,
|
|
): boolean {
|
|
const file = ts.getSourceFileOfNode(decl);
|
|
const self = lowerer.checker.getSymbolAtLocation(decl.name);
|
|
let captures = false;
|
|
const scan = (n: ts.Node): void => {
|
|
if (captures) return;
|
|
if (
|
|
n.kind === ts.SyntaxKind.ThisKeyword ||
|
|
n.kind === ts.SyntaxKind.SuperKeyword ||
|
|
ts.isMetaProperty(n)
|
|
) {
|
|
// Arrow bodies see the ENCLOSING this; function expressions
|
|
// rebind their own — but a bare `this` there is untyped JS
|
|
// dynamism either way. Reject both, cheaply and soundly.
|
|
captures = true;
|
|
return;
|
|
}
|
|
if (ts.isIdentifier(n)) {
|
|
const s = lowerer.checker.getSymbolAtLocation(n);
|
|
if (s === undefined) {
|
|
if (n.text === "arguments") captures = true;
|
|
} else if (s !== self) {
|
|
for (const d of s.declarations) {
|
|
if (ts.getSourceFileOfNode(d) !== file || (d.pos >= fn.pos && d.end <= fn.end)) continue;
|
|
// Declared outside the function, in this file: a capture
|
|
// exactly when some enclosing FUNCTION scope declares it.
|
|
for (
|
|
let p: ts.Node | undefined = d.parent;
|
|
p !== undefined && !ts.isSourceFile(p);
|
|
p = p.parent
|
|
) {
|
|
if (ts.isFunctionLike(p)) {
|
|
captures = true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ts.forEachChild(n, scan);
|
|
};
|
|
scan(scope);
|
|
return captures;
|
|
}
|
|
|
|
/** Registers (or returns) the GenericFnInfo of a qualifying implicit-any
|
|
* function-value binding — implicitLocalFnNodeOf's companion, the
|
|
* bindingGenericFnInfoOf shape: the info enters genericFnsBySymbol under
|
|
* the binding's symbol (and a named function expression's inner name),
|
|
* so calls and value references resolve through genericFnOf; the
|
|
* declaration statement emits nothing and the binding has no runtime
|
|
* value. The declaration's source position joins the qualified name —
|
|
* two same-named locals in one file stay distinct. */
|
|
export function implicitLocalFnInfoOf(
|
|
lowerer: Lowerer,
|
|
decl: ts.VariableDeclaration,
|
|
fnNode: ts.FunctionExpression | ts.ArrowFunction,
|
|
): GenericFnInfo {
|
|
const existing = lowerer.implicitLocalFns.get(decl);
|
|
if (existing) return existing;
|
|
const name = (decl.name as ts.Identifier).text;
|
|
const sym = lowerer.checker.getSymbolAtLocation(decl.name);
|
|
if (!sym) lowerer.unsupported("SC1090", decl.name, "this binding form");
|
|
const implicit = implicitAnyParamSymbolsOf(lowerer, fnNode);
|
|
if (!implicit) lowerer.unsupported("SC1090", fnNode, "this function form"); // defensive: the probe proved it
|
|
const stmt = decl.parent?.parent;
|
|
const info: GenericFnInfo = {
|
|
decl: fnNode,
|
|
baseName: name,
|
|
qualifiedName: lowerer.qualify(
|
|
ts.getSourceFileOfNode(decl),
|
|
nsPathPrefix(lowerer, stmt ?? decl, decl) + `${name}%l${positionTag(lowerer, decl)}`,
|
|
),
|
|
typeParams: [],
|
|
instances: new Map(),
|
|
implicitParams: implicit,
|
|
};
|
|
lowerer.implicitLocalFns.set(decl, info);
|
|
lowerer.genericFnsBySymbol.set(sym, info);
|
|
if (ts.isFunctionExpression(fnNode) && fnNode.name !== undefined) {
|
|
const inner = lowerer.checker.getSymbolAtLocation(fnNode.name);
|
|
if (inner) lowerer.genericFnsBySymbol.set(inner, info);
|
|
}
|
|
return info;
|
|
}
|
|
|
|
/** The function-like node behind an object-literal generic-method member:
|
|
* the MethodDeclaration itself (`{ m<T>(x: T) {...} }`) or a generic
|
|
* arrow/function-expression property's initializer (`{ m: <T>(x: T) =>
|
|
* ... }`). Null when the property's declaration isn't that shape. */
|
|
export function objLitGenericFnNodeOf(
|
|
lowerer: Lowerer,
|
|
propSym: ts.Symbol,
|
|
): {
|
|
fnNode: ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction;
|
|
literal: ts.ObjectLiteralExpression;
|
|
} | null {
|
|
const decl = propSym.valueDeclaration;
|
|
if (!decl) return null;
|
|
if (ts.isMethodDeclaration(decl) && ts.isObjectLiteralExpression(decl.parent)) {
|
|
return decl.typeParameters !== undefined && decl.body !== undefined
|
|
? { fnNode: decl, literal: decl.parent }
|
|
: null;
|
|
}
|
|
if (ts.isPropertyAssignment(decl) && ts.isObjectLiteralExpression(decl.parent)) {
|
|
let init: ts.Expression = decl.initializer;
|
|
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
|
if (
|
|
(ts.isArrowFunction(init) || ts.isFunctionExpression(init)) &&
|
|
init.typeParameters !== undefined &&
|
|
init.body !== undefined
|
|
) {
|
|
return { fnNode: init, literal: decl.parent };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** The interned GenericFnInfo for one object-literal generic method, with
|
|
* the supportability fences applied ONCE per declaration: the defining
|
|
* literal must sit at module scope (the compiled instance is a plain
|
|
* module function — an enclosing frame would need captures), and
|
|
* async/generator forms keep the method fences. The name is source-
|
|
* position-derived (`%ol<start>.<name>`, qualified per file) —
|
|
* deterministic across the discovery and emit passes. */
|
|
export function objLitGenericFnInfoOf(
|
|
lowerer: Lowerer,
|
|
blame: ts.Node,
|
|
name: string,
|
|
found: {
|
|
fnNode: ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction;
|
|
literal: ts.ObjectLiteralExpression;
|
|
},
|
|
): GenericFnInfo {
|
|
const { fnNode, literal } = found;
|
|
const existing = lowerer.objLitGenericFns.get(fnNode);
|
|
if (existing) return existing;
|
|
if (fnNode.asteriskToken) lowerer.unsupported("SC1071", blame);
|
|
if (fnNode.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) {
|
|
lowerer.unsupported("SC1090", blame, "async object-literal generic methods");
|
|
}
|
|
// `this` is the receiver object — records don't model it (the
|
|
// lowerObjectLiteral fence, applied at registration because
|
|
// arrow/function-expression properties skip that walk and the compiled
|
|
// instances are plain module functions).
|
|
if (fnNode.body) lowerer.rejectThisInObjectMethod(fnNode.body);
|
|
for (let n: ts.Node | undefined = literal.parent; n && !ts.isSourceFile(n); n = n.parent) {
|
|
if (ts.isFunctionLike(n)) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
blame,
|
|
`object-literal generic methods declared inside functions (the compiled instantiations of '${name}' are module functions and cannot capture the enclosing frame — declare the object at module scope)`,
|
|
);
|
|
}
|
|
}
|
|
const typeParams = declaredTypeParameterSymbols(
|
|
lowerer,
|
|
fnNode.typeParameters!.nodes,
|
|
blame,
|
|
"this method form",
|
|
);
|
|
checkGenericParameterNames(lowerer, fnNode.parameters.nodes, false);
|
|
const info: GenericFnInfo = {
|
|
decl: fnNode,
|
|
baseName: name,
|
|
qualifiedName: lowerer.qualify(
|
|
ts.getSourceFileOfNode(fnNode),
|
|
`%ol${positionTag(lowerer, fnNode)}.${name}`,
|
|
),
|
|
typeParams,
|
|
instances: new Map(),
|
|
objectLiteral: true,
|
|
};
|
|
lowerer.objLitGenericFns.set(fnNode, info);
|
|
return info;
|
|
}
|
|
|
|
/** The generic function-like INITIALIZER behind a binding declaration —
|
|
* `const f = <T>(x: T) => x` or `const f = function g<T>(x: T) {...}`
|
|
* (parens stripped). Null when the declaration isn't that shape; the
|
|
* SHAPE only — whether the binding qualifies (module scope, never
|
|
* reassigned) is bindingGenericFnInfoOf's business. */
|
|
export function bindingGenericFnNodeOf(
|
|
decl: ts.VariableDeclaration,
|
|
): ts.FunctionExpression | ts.ArrowFunction | null {
|
|
if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return null;
|
|
// Assertion wrappers strip like parens: `const r = (<T>(x: T) => x) as
|
|
// Mapper` evaluates the arrow — the cast only renames its type.
|
|
const init = stripValueWrappers(decl.initializer);
|
|
if (
|
|
(ts.isArrowFunction(init) || ts.isFunctionExpression(init)) &&
|
|
init.typeParameters !== undefined &&
|
|
init.body !== undefined
|
|
) {
|
|
return init;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** The CONTEXTUAL twin of bindingGenericFnNodeOf: `const g: Mapper = (x)
|
|
* => x` where `type Mapper = <T>(x: T) => T` — the initializer declares
|
|
* no type parameters of its own, but the ANNOTATION's one call signature
|
|
* does, and the checker types the arrow's parameters by those (`x: T`).
|
|
* Such a binding monomorphizes exactly like `const g = <T>(x: T) => x`;
|
|
* bindingGenericFnInfoOf reads the type parameters off the annotation's
|
|
* signature. Null when the shape doesn't match (a concrete annotation, a
|
|
* generic arrow — the syntactic probe's case, an overloaded alias). */
|
|
export function bindingContextualGenericFnNodeOf(
|
|
lowerer: Lowerer,
|
|
decl: ts.VariableDeclaration,
|
|
): ts.FunctionExpression | ts.ArrowFunction | null {
|
|
if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return null;
|
|
// The generic signature can arrive as an ANNOTATION or as a type
|
|
// ASSERTION on the initializer (`var r = < <T>(x: T) => T >((x) => x)`
|
|
// — the checker contextually types the operand's parameters by the
|
|
// asserted signature exactly like an annotation would).
|
|
const asserted = ts.isAsExpression(decl.initializer) || ts.isTypeAssertion(decl.initializer);
|
|
if (decl.type === undefined && !asserted) return null;
|
|
const init = stripValueWrappers(decl.initializer);
|
|
if (
|
|
!(ts.isArrowFunction(init) || ts.isFunctionExpression(init)) ||
|
|
init.typeParameters !== undefined ||
|
|
init.body === undefined
|
|
) {
|
|
return null;
|
|
}
|
|
const sigs = lowerer.checker.getCallSignatures(lowerer.typeOf(decl.name));
|
|
if (sigs.length !== 1 || sigs[0]!.getTypeParameters().length === 0) return null;
|
|
return init;
|
|
}
|
|
|
|
/** The interned GenericFnInfo for one generic arrow/function-expression
|
|
* binding initializer, with the supportability fences applied ONCE per
|
|
* declaration: the binding must sit at module scope (the compiled
|
|
* instances are plain module functions — an enclosing frame would need
|
|
* captures) and must provably HOLD the initializer once initialized — a
|
|
* const, or a let/var nothing in its declaring file ever writes (ESM
|
|
* import bindings are read-only, so the file scan is the whole story;
|
|
* observing the UNINITIALIZED state needs a hoisted early call, the
|
|
* same temporal hole const TDZ leaves — the object-literal generic-
|
|
* method receiver stance). Successful registration enters the info
|
|
* in genericFnsBySymbol under the binding's symbol — and under a named
|
|
* function expression's own inner name (it binds itself inside the
|
|
* body, the class-expression rule) — so every genericFnOf consumer
|
|
* (calls, pinned values, instantiation expressions, namespace and CJS
|
|
* member paths) resolves it like a top-level generic declaration. */
|
|
export function bindingGenericFnInfoOf(
|
|
lowerer: Lowerer,
|
|
decl: ts.VariableDeclaration,
|
|
fnNode: ts.FunctionExpression | ts.ArrowFunction,
|
|
): GenericFnInfo {
|
|
const existing = lowerer.bindingGenericFns.get(fnNode);
|
|
if (existing) return existing;
|
|
const name = (decl.name as ts.Identifier).text;
|
|
if (fnNode.asteriskToken) lowerer.unsupported("SC1071", fnNode);
|
|
// Inside a function, the compiled instances are module functions, so
|
|
// the initializer may not use the enclosing frame.
|
|
let functionScoped = false;
|
|
for (
|
|
let n: ts.Node | undefined = decl.parent;
|
|
n !== undefined && !ts.isSourceFile(n);
|
|
n = n.parent
|
|
) {
|
|
if (ts.isFunctionLike(n)) {
|
|
if (capturesEnclosingFrame(lowerer, fnNode, decl, fnNode)) {
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
fnNode,
|
|
`generic arrow/function-expression bindings that use the frames of their enclosing functions (the compiled instantiations of '${name}' are module functions and cannot capture its locals, parameters, type parameters or 'this' — pass them as arguments, or declare the binding at module scope)`,
|
|
);
|
|
}
|
|
functionScoped = true;
|
|
break;
|
|
}
|
|
}
|
|
const sym = lowerer.checker.getSymbolAtLocation(decl.name);
|
|
if (!sym) lowerer.unsupported("SC1090", decl.name, "this binding form");
|
|
requireStableGenericBinding(lowerer, decl, sym, name);
|
|
const typeParams: ts.Symbol[] = [];
|
|
if (fnNode.typeParameters !== undefined) {
|
|
typeParams.push(
|
|
...declaredTypeParameterSymbols(
|
|
lowerer,
|
|
fnNode.typeParameters.nodes,
|
|
fnNode,
|
|
"this function form",
|
|
),
|
|
);
|
|
} else {
|
|
// The CONTEXTUAL shape (bindingContextualGenericFnNodeOf): the type
|
|
// parameters live on the annotation's one call signature, and the
|
|
// checker types the initializer's parameters by them — the same
|
|
// symbols the instance bodies resolve through.
|
|
const sigs = lowerer.checker.getCallSignatures(lowerer.typeOf(decl.name));
|
|
const tps = sigs.length === 1 ? sigs[0]!.getTypeParameters() : [];
|
|
if (tps.length === 0) lowerer.unsupported("SC1090", fnNode, "this function form");
|
|
for (const tp of tps) {
|
|
const tpSym: ts.Symbol | undefined = tp.getSymbol();
|
|
if (!tpSym) lowerer.unsupported("SC1090", fnNode, "this function form");
|
|
typeParams.push(tpSym);
|
|
}
|
|
}
|
|
checkGenericParameterNames(lowerer, fnNode.parameters.nodes, true);
|
|
const stmt = decl.parent?.parent; // declarator → list → statement (nsPathPrefix wants the statement)
|
|
// A binding in a function or block can share its name with another; its
|
|
// instances take the declaration's position.
|
|
const topLevel =
|
|
stmt?.parent !== undefined && (ts.isSourceFile(stmt.parent) || ts.isModuleBlock(stmt.parent));
|
|
const info: GenericFnInfo = {
|
|
decl: fnNode,
|
|
baseName: name,
|
|
qualifiedName: lowerer.qualify(
|
|
ts.getSourceFileOfNode(decl),
|
|
nsPathPrefix(lowerer, stmt ?? decl, decl) +
|
|
(topLevel ? name : `${name}%l${ts.tokenStartOfNode(decl)}`),
|
|
),
|
|
typeParams,
|
|
instances: new Map(),
|
|
...(functionScoped ? { functionScoped: true as const } : {}),
|
|
bindingDecl: decl,
|
|
};
|
|
lowerer.bindingGenericFns.set(fnNode, info);
|
|
lowerer.genericFnsBySymbol.set(sym, info);
|
|
// Module-scope uses resolve to the instances from the start, so those
|
|
// that can run first need the flag. Inside a function the declaration
|
|
// registers before any later use lowers, and earlier uses read a TDZ
|
|
// box (genericBindingStoredValue).
|
|
if (!functionScoped) {
|
|
const presence = bindingPresenceOf(lowerer, info, decl, sym);
|
|
if (presence !== undefined) info.presence = presence;
|
|
}
|
|
if (ts.isFunctionExpression(fnNode) && fnNode.name !== undefined) {
|
|
const inner = lowerer.checker.getSymbolAtLocation(fnNode.name);
|
|
if (inner) lowerer.genericFnsBySymbol.set(inner, info);
|
|
}
|
|
return info;
|
|
}
|
|
|
|
/** Whether `use`, a reference to the binding `info` declares, can only
|
|
* run after the declaration has run: it follows the declaration in its
|
|
* statement list (closures count where they are created), sits inside the
|
|
* initializer itself, or is inside a function declaration whose every
|
|
* reference does. References from other modules run after this module's
|
|
* body, and export specifiers only export. A use the check cannot place
|
|
* answers false. */
|
|
export function runsAfterDeclaration(lowerer: Lowerer, info: GenericFnInfo, use: ts.Node): boolean {
|
|
const decl = info.bindingDecl;
|
|
if (decl === undefined) return true;
|
|
info.ordering ??= new Map();
|
|
return useRunsAfter(lowerer, decl, use, info.ordering, new Set());
|
|
}
|
|
|
|
function useRunsAfter(
|
|
lowerer: Lowerer,
|
|
decl: ts.VariableDeclaration,
|
|
use: ts.Node,
|
|
memo: Map<ts.Node, boolean>,
|
|
inProgress: Set<ts.Node>,
|
|
): boolean {
|
|
if (ts.getSourceFileOfNode(use) !== ts.getSourceFileOfNode(decl)) return true;
|
|
const init = decl.initializer;
|
|
if (init !== undefined && use.pos >= init.pos && use.end <= init.end) return true;
|
|
const stmt = decl.parent?.parent;
|
|
const list = stmt?.parent;
|
|
if (stmt === undefined || list === undefined) return false;
|
|
for (let n: ts.Node = use; n.parent !== undefined; n = n.parent) {
|
|
if (n.parent === list) return n === stmt ? use.pos >= decl.end : n.pos >= stmt.end;
|
|
if (ts.isFunctionDeclaration(n)) return functionRunsAfter(lowerer, decl, n, memo, inProgress);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Whether every call of the function declaration `fn` runs after `decl`
|
|
* has run. A function in progress counts as running after: its calls of
|
|
* itself, directly or through other functions, add no earlier caller.
|
|
* Only answers that depend on no function in progress are memoized. */
|
|
function functionRunsAfter(
|
|
lowerer: Lowerer,
|
|
decl: ts.VariableDeclaration,
|
|
fn: ts.FunctionDeclaration,
|
|
memo: Map<ts.Node, boolean>,
|
|
inProgress: Set<ts.Node>,
|
|
): boolean {
|
|
const known = memo.get(fn);
|
|
if (known !== undefined) return known;
|
|
if (inProgress.has(fn)) return true;
|
|
// `export default function () {}`: only importers call it, after this
|
|
// module's body has run.
|
|
if (fn.name === undefined) return true;
|
|
inProgress.add(fn);
|
|
let result = true;
|
|
try {
|
|
const symbol = lowerer.checker.getSymbolAtLocation(fn.name);
|
|
if (symbol === undefined) result = false;
|
|
else {
|
|
for (const ref of bindingReferences(lowerer, ts.getSourceFileOfNode(fn), symbol, fn.name)) {
|
|
if (ref.pos >= fn.pos && ref.end <= fn.end) continue;
|
|
if (!useRunsAfter(lowerer, decl, ref, memo, inProgress)) {
|
|
result = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
inProgress.delete(fn);
|
|
}
|
|
if (inProgress.size === 0) memo.set(fn, result);
|
|
return result;
|
|
}
|
|
|
|
/** The value references of `symbol` in `file` other than `name` (its
|
|
* declaration name): identifiers spelled like it that resolve to it,
|
|
* outside type positions and export specifiers. */
|
|
function bindingReferences(
|
|
lowerer: Lowerer,
|
|
file: ts.SourceFile,
|
|
symbol: ts.Symbol,
|
|
name: ts.Identifier,
|
|
): ts.Identifier[] {
|
|
let byName = lowerer.bindingIdentifiers.get(file);
|
|
if (byName === undefined) {
|
|
byName = new Map();
|
|
lowerer.bindingIdentifiers.set(file, byName);
|
|
}
|
|
let idents = byName.get(name.text);
|
|
if (idents === undefined) {
|
|
const found: ts.Identifier[] = [];
|
|
ts.walkPreorder(file, (node) => {
|
|
if (ts.isIdentifier(node) && node.text === name.text) found.push(node);
|
|
return undefined;
|
|
});
|
|
lowerer.checker.prefetchSymbolNodesExact(found);
|
|
byName.set(name.text, found);
|
|
idents = found;
|
|
}
|
|
return idents.filter((ident) => {
|
|
if (ident === name) return false;
|
|
const parent = ident.parent;
|
|
if (parent === undefined || ts.isExportSpecifier(parent) || ts.isExportAssignment(parent))
|
|
return false;
|
|
let resolved = lowerer.checker.getSymbolAtLocation(ident);
|
|
if (ts.isShorthandPropertyAssignment(parent) && parent.name === ident)
|
|
resolved = lowerer.checker.getShorthandAssignmentValueSymbol(parent) ?? resolved;
|
|
if (resolved !== symbol) return false;
|
|
for (let p: ts.Node | undefined = parent; p !== undefined; p = p.parent) {
|
|
if (
|
|
ts.isTypeNode(p) &&
|
|
p.kind !== ts.SyntaxKind.ExpressionWithTypeArguments &&
|
|
!isHeritageTypeReference(p)
|
|
)
|
|
return false;
|
|
if (ts.isStatement(p) || ts.isFunctionLike(p)) break;
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/** The presence flag of a module-scope generic binding that code can use
|
|
* before its declaration runs (a hoisted function or an earlier closure
|
|
* called during module evaluation), or undefined when every use runs
|
|
* after it. The declaration statement sets the flag. */
|
|
function bindingPresenceOf(
|
|
lowerer: Lowerer,
|
|
info: GenericFnInfo,
|
|
decl: ts.VariableDeclaration,
|
|
symbol: ts.Symbol,
|
|
): GenericFnInfo["presence"] {
|
|
const name = decl.name as ts.Identifier;
|
|
const early = bindingReferences(lowerer, ts.getSourceFileOfNode(decl), symbol, name).some(
|
|
(ref) => !runsAfterDeclaration(lowerer, info, ref),
|
|
);
|
|
if (!early) return undefined;
|
|
const flagId = `%g.${info.qualifiedName}%initialized`;
|
|
lowerer.globalsList.push({ id: flagId, name: "%initialized", type: BOOL, mutable: true });
|
|
return { flagId, lexical: !isVarDeclared(decl) };
|
|
}
|
|
|
|
/** The statement that records a generic binding's declaration as run,
|
|
* for a binding with a presence flag. */
|
|
export function genericBindingPresenceStore(
|
|
info: GenericFnInfo,
|
|
decl: ts.VariableDeclaration,
|
|
): IrStmt | null {
|
|
if (info.presence === undefined) return null;
|
|
const loc = locOf(decl);
|
|
return {
|
|
kind: "assign",
|
|
localId: info.presence.flagId,
|
|
value: { kind: "boolLit", value: true, type: BOOL, loc },
|
|
loc,
|
|
};
|
|
}
|
|
|
|
/** A call of a generic binding where the binding may not be initialized
|
|
* yet. A `let`/`const` binding in its temporal dead zone throws Node's
|
|
* ReferenceError before the arguments run; an unassigned `var` is
|
|
* undefined, so the arguments run and the call throws "is not a
|
|
* function". Calls that run after the declaration stay direct. */
|
|
function guardEarlyBindingCall(
|
|
lowerer: Lowerer,
|
|
info: GenericFnInfo,
|
|
expr: ts.CallExpression,
|
|
call: IrExpr & { kind: "call" },
|
|
): IrExpr {
|
|
if (info.bindingDecl === undefined || runsAfterDeclaration(lowerer, info, expr)) return call;
|
|
const loc = call.loc;
|
|
const presence = requirePresence(lowerer, info, expr.expression);
|
|
const flag: IrExpr = { kind: "varRef", localId: presence.flagId, type: BOOL, loc };
|
|
if (presence.lexical)
|
|
return {
|
|
kind: "seqExpr",
|
|
stmts: [tdzCheck(flag, info.baseName, loc)],
|
|
result: call,
|
|
type: call.type,
|
|
loc,
|
|
};
|
|
// Arguments first, in order, then the check: Node evaluates them
|
|
// before it finds the callee is not callable.
|
|
const stmts: IrStmt[] = [];
|
|
const args = call.args.map((arg): IrExpr => {
|
|
const id = `%earlyCallArg.${lowerer.ctx.locals.length}`;
|
|
lowerer.ctx.locals.push({ id, name: "%earlyCallArg", type: arg.type, mutable: false });
|
|
stmts.push({ kind: "varDecl", localId: id, init: arg, loc });
|
|
return { kind: "varRef", localId: id, type: arg.type, loc };
|
|
});
|
|
return {
|
|
kind: "seqExpr",
|
|
stmts,
|
|
result: {
|
|
kind: "ternary",
|
|
cond: flag,
|
|
then: { ...call, args },
|
|
else_: nodeThrowExpr(1, "", `${info.baseName} is not a function`, call.type, loc),
|
|
type: call.type,
|
|
loc,
|
|
},
|
|
type: call.type,
|
|
loc,
|
|
};
|
|
}
|
|
|
|
/** A generic binding read as a value where it may not be initialized
|
|
* yet: a `let`/`const` binding checks its temporal dead zone first. */
|
|
function guardEarlyBindingValue(
|
|
lowerer: Lowerer,
|
|
info: GenericFnInfo,
|
|
ref: ts.Expression,
|
|
value: IrExpr,
|
|
): IrExpr {
|
|
if (info.bindingDecl === undefined || runsAfterDeclaration(lowerer, info, ref)) return value;
|
|
const presence = requirePresence(lowerer, info, ref);
|
|
if (!presence.lexical)
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
ref,
|
|
`generic 'var' bindings read as values before their declaration runs (Node reads undefined there; declare '${info.baseName}' with const)`,
|
|
);
|
|
const loc = value.loc;
|
|
const flag: IrExpr = { kind: "varRef", localId: presence.flagId, type: BOOL, loc };
|
|
return {
|
|
kind: "seqExpr",
|
|
stmts: [tdzCheck(flag, info.baseName, loc)],
|
|
result: value,
|
|
type: value.type,
|
|
loc,
|
|
};
|
|
}
|
|
|
|
/** The presence flag a use that can run before the declaration checks.
|
|
* Only module-scope bindings have one: a binding inside a function would
|
|
* need a per-call flag that its closures share. */
|
|
function requirePresence(
|
|
lowerer: Lowerer,
|
|
info: GenericFnInfo,
|
|
blame: ts.Node,
|
|
): NonNullable<GenericFnInfo["presence"]> {
|
|
if (info.presence !== undefined) return info.presence;
|
|
return lowerer.unsupported(
|
|
"SC1090",
|
|
blame,
|
|
`generic arrow/function-expression bindings declared inside functions that code can call before their declaration runs (the binding '${info.baseName}' is a 'var' in a branch or block, or is called from a place that can run first — declare it with const before its uses)`,
|
|
);
|
|
}
|
|
|
|
function tdzCheck(flag: IrExpr, name: string, loc: SrcLoc): IrStmt {
|
|
return {
|
|
kind: "exprStmt",
|
|
expr: {
|
|
kind: "intrinsic",
|
|
name: "module.tdzCheck",
|
|
args: [flag, { kind: "strLit", value: name, type: STRING, loc }],
|
|
type: VOID,
|
|
loc,
|
|
},
|
|
loc,
|
|
};
|
|
}
|
|
|
|
/** `const h = id` — a binding ALIASING a generic function (a top-level
|
|
* declaration, a registered generic binding, or another alias — resolved
|
|
* left to right in declaration order). The alias registers the SAME info
|
|
* under its own symbol, so calls (`h(3)`) and pinned values (`take(h)`)
|
|
* resolve exactly like the target's own name, and the binding itself has
|
|
* no runtime value (a generic function value cannot materialize). Claims
|
|
* only bindings whose OWN type still keeps type parameters — a
|
|
* concrete-annotated alias (`const h: (x: number) => number = id`) is a
|
|
* pinned VALUE, the existing lowerGenericFnValue story. Null when the
|
|
* shape doesn't match or the target isn't a registered generic; fences
|
|
* (reassignment, var redeclaration) report by name inside. */
|
|
export function bindingGenericFnAliasInfoOf(
|
|
lowerer: Lowerer,
|
|
decl: ts.VariableDeclaration,
|
|
): GenericFnInfo | null {
|
|
if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return null;
|
|
let init: ts.Expression = decl.initializer;
|
|
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
|
if (!ts.isIdentifier(init)) return null;
|
|
const target = genericFnOf(lowerer, init);
|
|
if (!target) return null;
|
|
// A concrete annotation pins one signature — that value story
|
|
// (lowerGenericFnValue at the reference) stays untouched.
|
|
const ownSigs = lowerer.checker.getCallSignatures(lowerer.typeOf(decl.name));
|
|
if (ownSigs.length === 0 || !ownSigs.every((s) => s.getTypeParameters().length > 0)) return null;
|
|
const sym = lowerer.checker.getSymbolAtLocation(decl.name);
|
|
if (!sym) return null;
|
|
const existing = lowerer.genericFnsBySymbol.get(sym);
|
|
if (existing) return existing;
|
|
const name = decl.name.text;
|
|
requireStableGenericBinding(lowerer, decl, sym, name);
|
|
lowerer.genericFnsBySymbol.set(sym, target);
|
|
return target;
|
|
}
|
|
|
|
/** Static resolution stands in for the receiver's runtime value, so an
|
|
* object-literal generic-method receiver must provably HOLD the defining
|
|
* literal: a direct read of a binding whose initializer IS that literal
|
|
* and that nothing ever reassigns — a const, or a let with no write in
|
|
* its declaring file (ESM import bindings are read-only, so the file
|
|
* scan is the whole story). The read is pure — call and value sites skip
|
|
* evaluating it entirely. A reassignable binding could hold a
|
|
* structurally identical literal with a DIFFERENT body, which static
|
|
* resolution would miss without a diagnostic. */
|
|
export function requireObjLitGenericReceiver(
|
|
lowerer: Lowerer,
|
|
blame: ts.Node,
|
|
recvExpr: ts.Expression,
|
|
literal: ts.ObjectLiteralExpression,
|
|
name: string,
|
|
): void {
|
|
let recv: ts.Expression = recvExpr;
|
|
while (ts.isParenthesizedExpression(recv)) recv = recv.expression;
|
|
const fenceReceiver: () => never = () =>
|
|
lowerer.unsupported(
|
|
"SC1090",
|
|
blame,
|
|
`reaching the object-literal generic method '${name}' through this receiver (resolution is static, so the receiver must be a never-reassigned binding initialized with the defining literal)`,
|
|
);
|
|
if (!ts.isIdentifier(recv)) fenceReceiver();
|
|
const recvSym = lowerer.resolveValueSymbol(recv);
|
|
const recvDecl = recvSym ? recvSym.valueDeclaration : undefined;
|
|
if (
|
|
!recvDecl ||
|
|
!ts.isVariableDeclaration(recvDecl) ||
|
|
!ts.isVariableDeclarationList(recvDecl.parent) ||
|
|
recvDecl.initializer === undefined
|
|
) {
|
|
fenceReceiver();
|
|
}
|
|
if (
|
|
(recvDecl.parent.flags & ts.NodeFlags.Const) === 0 &&
|
|
!bindingNeverReassigned(lowerer, recvSym!, recvDecl)
|
|
) {
|
|
fenceReceiver();
|
|
}
|
|
let init: ts.Expression = recvDecl.initializer;
|
|
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
|
if (init !== literal) fenceReceiver();
|
|
}
|