mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Map generic alias casts inside generic bodies with the instance's arguments (#2227)
This commit is contained in:
@@ -1040,6 +1040,14 @@ function unifySignatureBindings(
|
||||
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);
|
||||
|
||||
@@ -2233,6 +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 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
|
||||
@@ -3797,6 +3810,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),
|
||||
@@ -9813,6 +9827,19 @@ 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
|
||||
* (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;
|
||||
const tsBindings = this.typeParamTsBindings;
|
||||
return (
|
||||
this.boundAliasInstances.get(t)?.find((entry) => entry.tsBindings === tsBindings)?.instance ??
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
irTypeOf(node: ts.Node): IrType {
|
||||
const t = this.typeOf(node);
|
||||
// JS inference can leave mutable fields, accessor parameters and
|
||||
|
||||
@@ -1270,6 +1270,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
|
||||
@@ -1834,6 +1839,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