mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Map generic alias casts inside generic bodies with the instance's arguments
A cast to a generic type alias spelled over the enclosing function's own type parameters (`return x as Catalog<G, P>`) stays symbolic in the checker inside the body, so a mapped type with key remapping, a template key filter or a conditional member refused with SC2001 in every instance. The checker interns alias instantiations, so the body's `Catalog<G, P>` is the same type the signature declares. Binding unification now records, per call-keyed instance, each declared generic alias reference together with the alias the call's resolved signature instantiates. When the symbolic form does not map, mapType maps that concrete instantiation instead. A body forwarding its own alias to another generic resolves through the enclosing instance. Types that already mapped are unaffected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8a22e0f515
commit
541b05ddea
@@ -921,6 +921,27 @@ function bindDefaultTypeParams(
|
||||
});
|
||||
}
|
||||
|
||||
/** 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">`). 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.
|
||||
* Keyed by the instance's ts-level bindings map, which every scope that
|
||||
* lowers the instance installs (typeParamTsBindings). */
|
||||
const boundAliasInstances = new WeakMap<Map<ts.Symbol, ts.Type>, Map<ts.Type, ts.Type>>();
|
||||
|
||||
/** The concrete instantiation of a generic alias reference inside the
|
||||
* instance whose ts-level bindings are `tsBindings` (boundAliasInstances),
|
||||
* or null when its signature never instantiated that reference. */
|
||||
export function boundAliasInstance(
|
||||
tsBindings: Map<ts.Symbol, ts.Type>,
|
||||
type: ts.Type,
|
||||
): ts.Type | null {
|
||||
return boundAliasInstances.get(tsBindings)?.get(type) ?? null;
|
||||
}
|
||||
|
||||
/** 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
|
||||
@@ -1040,6 +1061,13 @@ function unifySignatureBindings(
|
||||
dAliasArgs.length &&
|
||||
iAliasArgs.length === dAliasArgs.length
|
||||
) {
|
||||
if (tsBindings && declared !== inst) {
|
||||
let pairs = boundAliasInstances.get(tsBindings);
|
||||
if (!pairs) boundAliasInstances.set(tsBindings, (pairs = new Map()));
|
||||
// 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);
|
||||
}
|
||||
dAliasArgs.forEach((da, i) => {
|
||||
const ia = iAliasArgs[i];
|
||||
if (ia) unify(da, ia, depth + 1);
|
||||
|
||||
@@ -456,6 +456,7 @@ import {
|
||||
inferTypeParamBindings,
|
||||
lowerGenericInstance,
|
||||
genericLocalClassBinding,
|
||||
boundAliasInstance,
|
||||
} from "./generic-functions.js";
|
||||
import { bindingNeverReassigned } from "./binding-analysis.js";
|
||||
import type { InstantiationPath } from "./instantiation-path.js";
|
||||
@@ -3792,6 +3793,7 @@ export class Lowerer {
|
||||
classNamer: (decl) => this.classNamer(decl),
|
||||
resolveTypeParam: this.typeParamResolver,
|
||||
resolveTypeParamTs: this.typeParamTsResolver,
|
||||
resolveBoundAliasTs: this.boundAliasTsResolver,
|
||||
isErasing: () => this.erasingDepth > 0,
|
||||
genericClassInstance: (decl, ref, mapArgument) =>
|
||||
this.genericClassInstanceType(decl, ref, mapArgument),
|
||||
@@ -9550,6 +9552,14 @@ export class Lowerer {
|
||||
return (sym && this.typeParamTsBindings.get(sym)) ?? null;
|
||||
};
|
||||
|
||||
/** The concrete instantiation of a generic alias reference (`Catalog<G,
|
||||
* P>`) the current call-keyed instance's signature declares
|
||||
* (boundAliasInstance). Inert outside generic function instantiation. */
|
||||
readonly boundAliasTsResolver = (t: ts.Type): ts.Type | null => {
|
||||
if (!this.typeParamTsBindings || t.getAliasTypeArguments().length === 0) return null;
|
||||
return boundAliasInstance(this.typeParamTsBindings, t);
|
||||
};
|
||||
|
||||
irTypeOf(node: ts.Node): IrType {
|
||||
const t = this.typeOf(node);
|
||||
// JS inference can leave mutable fields, accessor parameters and
|
||||
|
||||
@@ -1266,6 +1266,11 @@ export interface TypeMapperCtx {
|
||||
* only where literal identity matters — indexed accesses (`T[K]`) inside
|
||||
* generic bodies whose K is bound to a literal key. */
|
||||
resolveTypeParamTs?: TypeParamTsResolver;
|
||||
/** A generic alias reference inside a generic body (`Catalog<G, P>`)
|
||||
* re-instantiated with the instance's bound type arguments: the concrete
|
||||
* checker type its signature instantiates, or null. Consulted only when
|
||||
* the symbolic form does not map. */
|
||||
resolveBoundAliasTs?: TypeParamTsResolver;
|
||||
/** GENERIC program classes (monomorphization by flow): the mapping of a
|
||||
* concrete instantiation reference (`Box<number>`) — the Lowerer
|
||||
* registers/reuses the instantiation (`Box%0`) and answers its object
|
||||
@@ -1830,6 +1835,16 @@ export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
if (!topLevel || !ts.isUnreadableConstituentsPanic(e)) throw e;
|
||||
mapped = null;
|
||||
}
|
||||
// A generic alias the body spells over its own type parameters (`x as
|
||||
// Catalog<G, P>`) stays symbolic in the checker; when that form does not
|
||||
// map, the instance's concrete instantiation of the same alias does.
|
||||
if (mapped === null && ctx.resolveBoundAliasTs) {
|
||||
const concrete = ctx.resolveBoundAliasTs(type);
|
||||
if (concrete !== null && concrete !== type) {
|
||||
mapped = mapType(concrete, ctx);
|
||||
if (mapped !== null) contextResolutions++;
|
||||
}
|
||||
}
|
||||
if (
|
||||
mapped !== null &&
|
||||
memoKey !== null &&
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/generic-alias-cast-instances.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Casts to a generic type alias spelled over the enclosing function's own
|
||||
// type parameters (`x as Catalog<G, P>`). The checker keeps such an alias
|
||||
// symbolic inside the generic body; each instance maps it with its own bound
|
||||
// type arguments.
|
||||
|
||||
type Groups = Record<string, Record<string, object>>;
|
||||
|
||||
type EntryKey<G extends Groups> = { [A in keyof G]: keyof G[A] }[keyof G] & string;
|
||||
type KeyOfKind<G extends Groups, K extends string> = Extract<EntryKey<G>, `${K}:${string}`>;
|
||||
type EntryId<K extends string> = K extends `${string}:${infer I}` ? I : never;
|
||||
|
||||
type Catalog<G extends Groups, P extends string, K extends string> = {
|
||||
[Key in KeyOfKind<G, K> as EntryId<Key>]: { id: EntryId<Key>; label: string; owner: P };
|
||||
};
|
||||
|
||||
function flatten(groups: Groups, kind: string): Record<string, object> {
|
||||
return Object.fromEntries(
|
||||
Object.values(groups)
|
||||
.flatMap((entries) => Object.entries(entries))
|
||||
.filter(([key]) => key.startsWith(kind + ":"))
|
||||
.map(([, entry]) => [(entry as { id: string }).id, entry]),
|
||||
);
|
||||
}
|
||||
|
||||
function toolCatalog<const P extends string, const G extends Groups>(
|
||||
_owner: P,
|
||||
groups: G,
|
||||
): Catalog<G, P, "tool"> {
|
||||
return flatten(groups, "tool") as Catalog<G, P, "tool">;
|
||||
}
|
||||
|
||||
function partCatalog<const P extends string, const G extends Groups>(
|
||||
_owner: P,
|
||||
groups: G,
|
||||
): Catalog<G, P, "part"> {
|
||||
return flatten(groups, "part") as Catalog<G, P, "part">;
|
||||
}
|
||||
|
||||
const workshop = {
|
||||
bench: {
|
||||
"tool:saw": { id: "saw", label: "Hand saw", owner: "acme" },
|
||||
"part:bolt": { id: "bolt", label: "Hex bolt", owner: "acme" },
|
||||
},
|
||||
shelf: {
|
||||
"tool:drill": { id: "drill", label: "Cordless drill", owner: "acme" },
|
||||
},
|
||||
};
|
||||
|
||||
const tools = toolCatalog("acme", workshop);
|
||||
console.log(tools.saw.label, tools.drill.id, tools.drill.owner);
|
||||
console.log(Object.keys(tools).join(","), JSON.stringify(tools.saw));
|
||||
|
||||
const parts = partCatalog("acme", workshop);
|
||||
console.log(parts.bolt.label, Object.keys(parts).length);
|
||||
|
||||
// A second instantiation with different groups gets its own layout.
|
||||
const garden = {
|
||||
shed: {
|
||||
"tool:rake": { id: "rake", label: "Leaf rake", owner: "green" },
|
||||
"tool:hoe": { id: "hoe", label: "Garden hoe", owner: "green" },
|
||||
},
|
||||
};
|
||||
const gardenTools = toolCatalog("green", garden);
|
||||
console.log(gardenTools.rake.label, gardenTools.hoe.label, Object.keys(gardenTools).join(","));
|
||||
|
||||
// No entry of the kind: the alias resolves to the empty record.
|
||||
const gardenParts = partCatalog("green", garden);
|
||||
console.log(Object.keys(gardenParts).length, JSON.stringify(gardenParts));
|
||||
|
||||
// A homomorphic mapped alias over the bound type, cast inside the body.
|
||||
type Boxed<T> = { [K in keyof T]: { value: T[K] } };
|
||||
|
||||
function boxAll<T extends Record<string, number>>(values: T): Boxed<T> {
|
||||
const out: Record<string, object> = {};
|
||||
for (const key of Object.keys(values)) out[key] = { value: values[key] };
|
||||
return out as Boxed<T>;
|
||||
}
|
||||
|
||||
const boxed = boxAll({ width: 3, height: 4 });
|
||||
console.log(boxed.width.value * boxed.height.value);
|
||||
|
||||
// The alias nested in an array type of the signature.
|
||||
function boxEach<T extends Record<string, number>>(rows: T[]): Boxed<T>[] {
|
||||
return rows.map((row) => {
|
||||
const out: Record<string, object> = {};
|
||||
for (const key of Object.keys(row)) out[key] = { value: row[key] };
|
||||
return out as Boxed<T>;
|
||||
});
|
||||
}
|
||||
|
||||
const rows = boxEach([
|
||||
{ x: 1, y: 2 },
|
||||
{ x: 5, y: 8 },
|
||||
]);
|
||||
console.log(rows.map((r) => r.x.value + r.y.value).join(" "));
|
||||
|
||||
// A generic body forwarding its parameters to another generic cast.
|
||||
function ownedTools<const G extends Groups>(groups: G): Catalog<G, "shared", "tool"> {
|
||||
return toolCatalog("shared", groups);
|
||||
}
|
||||
|
||||
const shared = ownedTools({ rack: { "tool:clamp": { id: "clamp", label: "C-clamp", owner: "shared" } } });
|
||||
console.log(shared.clamp.label, shared.clamp.owner);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3000, "san": 3600 }
|
||||
Reference in New Issue
Block a user