Key bound alias instances by the declared alias

The compiler compiles itself, and a Map keyed by another Map is outside
the key domains it compiles (SC2009 in the self-hosted frontend). The
Lowerer's table is now keyed by the declared alias reference: each entry
lists the instances that instantiate it with their ts-level bindings maps,
and the resolver picks the current instance's by identity.
This commit is contained in:
Chris Tate
2026-10-25 14:49:55 -05:00
parent 317d724a7b
commit c21fc01201
2 changed files with 24 additions and 15 deletions
@@ -1041,11 +1041,12 @@ function unifySignatureBindings(
iAliasArgs.length === dAliasArgs.length
) {
if (tsBindings && declared !== inst) {
let pairs = lowerer.boundAliasInstances.get(tsBindings);
if (!pairs) lowerer.boundAliasInstances.set(tsBindings, (pairs = new Map()));
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 (!pairs.has(declared)) pairs.set(declared, lowerer.boundAliasTsResolver(inst) ?? inst);
if (!entries.some((entry) => entry.tsBindings === tsBindings))
entries.push({ tsBindings, instance: lowerer.boundAliasTsResolver(inst) ?? inst });
}
dAliasArgs.forEach((da, i) => {
const ia = iAliasArgs[i];
@@ -2233,16 +2233,19 @@ export class Lowerer {
* the body needs — indexed accesses (`T[K]` needs K's literal key) and
* keyed record reads (`o[k]` where k's type is a literal-bound K). */
typeParamTsBindings: Map<ts.Symbol, ts.Type> | null = null;
/** Generic alias references of a call-keyed instance's declared signature
* (`Catalog<G, P>`), each paired with the same alias as the call's
* resolved signature instantiates it (`Catalog<{ g1: … }, "acme">`),
* keyed by the instance's ts-level bindings (typeParamTsBindings). The
* checker keeps the alias symbolic inside the generic body and offers no
* way to instantiate it with the bound arguments, but alias
* instantiations are interned: the body's `x as Catalog<G, P>` is the
* very type the signature declares, so the pair recorded during binding
* unification is that re-instantiation. */
readonly boundAliasInstances = new Map<Map<ts.Symbol, ts.Type>, Map<ts.Type, ts.Type>>();
/** Generic alias references of call-keyed instances' declared signatures
* (`Catalog<G, P>`), each with the same alias as every call's resolved
* signature instantiates it (`Catalog<{ g1: … }, "acme">`), together with
* that instance's ts-level bindings map (typeParamTsBindings), which every
* scope that lowers the instance installs. The checker keeps the alias
* symbolic inside the generic body and offers no way to instantiate it
* with the bound arguments, but alias instantiations are interned: the
* body's `x as Catalog<G, P>` is the very type the signature declares, so
* the pair recorded during unification is that re-instantiation. */
readonly boundAliasInstances = new Map<
ts.Type,
{ tsBindings: Map<ts.Symbol, ts.Type>; instance: ts.Type }[]
>();
/** Non-null while an IMPLICIT-ANY instance body lowers (npm-static JS;
* see generic-functions.ts): bound param symbol →
* the call site's checker type, consulted by typeOf for identifier
@@ -9563,10 +9566,15 @@ export class Lowerer {
/** The concrete instantiation of a generic alias reference (`Catalog<G,
* P>`) the current call-keyed instance's signature declares
* (boundAliasInstances). Inert outside generic function instantiation. */
* (boundAliasInstances), or null when its signature never instantiated
* that reference. Inert outside generic function instantiation. */
readonly boundAliasTsResolver = (t: ts.Type): ts.Type | null => {
if (!this.typeParamTsBindings || t.getAliasTypeArguments().length === 0) return null;
return this.boundAliasInstances.get(this.typeParamTsBindings)?.get(t) ?? null;
const tsBindings = this.typeParamTsBindings;
return (
this.boundAliasInstances.get(t)?.find((entry) => entry.tsBindings === tsBindings)?.instance ??
null
);
};
irTypeOf(node: ts.Node): IrType {