From c21fc0120180fb89bf6ff2251bba30d6739f3c7b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 14:49:55 -0500 Subject: [PATCH] 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. --- .../frontend/lowering/generic-functions.ts | 7 ++-- .../compiler/src/frontend/lowering/lowerer.ts | 32 ++++++++++++------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index a2eda839a..1769e8562 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -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` 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]; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 216851c9c..c77741997 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -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 | null = null; - /** Generic alias references of a call-keyed instance's declared signature - * (`Catalog`), 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` is the - * very type the signature declares, so the pair recorded during binding - * unification is that re-instantiation. */ - readonly boundAliasInstances = new Map, Map>(); + /** Generic alias references of call-keyed instances' declared signatures + * (`Catalog`), 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` 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; 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`) 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 {