mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
Monomorphize generic overload calls and call generic methods through Pick views (#1511)
This commit is contained in:
@@ -268,10 +268,13 @@ export function genericCallInstance(
|
||||
{
|
||||
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,
|
||||
@@ -317,23 +320,11 @@ export function genericCallInstance(
|
||||
// different fields). Non-literal bindings (a key union, plain string)
|
||||
// share one runtime-keyed instance per IR signature, exactly the
|
||||
// widened discipline.
|
||||
const keyofTps = keyofConstrainedTypeParams(info);
|
||||
if (keyofTps.size > 0) {
|
||||
if (keyofConstrainedTypeParams(info).size > 0) {
|
||||
const tsBindings = new Map<ts.Symbol, ts.Type>();
|
||||
const bindings = lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings);
|
||||
const litKey = 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)
|
||||
: "*";
|
||||
})
|
||||
.join(",");
|
||||
return internGenericInstance(lowerer, expr, info, params, returnType, () => bindings, {
|
||||
extraKey: `@${litKey}`,
|
||||
extraKey: keyofLiteralKey(info, tsBindings),
|
||||
tsBindings,
|
||||
});
|
||||
}
|
||||
@@ -342,6 +333,98 @@ export function genericCallInstance(
|
||||
);
|
||||
}
|
||||
|
||||
/** 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) => p.name === 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.
|
||||
return internGenericInstance(lowerer, expr, info, params, returnType, () => bindings, {
|
||||
...(keyofConstrainedTypeParams(info).size > 0
|
||||
? { extraKey: keyofLiteralKey(info, tsBindings) }
|
||||
: {}),
|
||||
tsBindings,
|
||||
});
|
||||
}
|
||||
|
||||
/** 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
|
||||
@@ -570,6 +653,25 @@ function keyofConstrainedTypeParams(info: GenericFnInfo): Set<ts.Symbol> {
|
||||
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
|
||||
@@ -705,6 +807,27 @@ function unifySignatureBindings(
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -3309,6 +3309,8 @@ function lowerDynReceiverMethodCall(
|
||||
// any-elemented array). Typed receivers keep their own lowerings.
|
||||
const recvTs = lowerer.typeOf(access.expression);
|
||||
if (lowerer.classImplementedProtocol(recvTs)) {
|
||||
const viewed = pickViewGenericMethodCall(lowerer, call, access, recvTs);
|
||||
if (viewed) return viewed;
|
||||
const property = lowerer.checker.getPropertyOfType(recvTs, access.name.text);
|
||||
if (
|
||||
property &&
|
||||
@@ -10092,3 +10094,49 @@ function maybePromiseHandlerResult(
|
||||
}
|
||||
return stmt;
|
||||
}
|
||||
|
||||
/** `view.m(args)` where `view` is a `Pick<C, K>` / `Omit<C, K>` view of a
|
||||
* program class C and `m` is one of C's GENERIC methods (its overloads
|
||||
* included): generic methods have no dispatchable method value, so the
|
||||
* view's live receiver is extracted as the C instance it holds (a checked
|
||||
* extraction — a value of another class throws Node-style TypeError
|
||||
* text naming C) and the call monomorphizes statically against C, the
|
||||
* same lowering a C-typed receiver gets. Null for other receivers. */
|
||||
function pickViewGenericMethodCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
recvTs: ts.Type,
|
||||
): IrExpr | null {
|
||||
const alias = recvTs.getAliasSymbol();
|
||||
if (alias === undefined || (alias.name !== "Pick" && alias.name !== "Omit")) return null;
|
||||
if (!alias.declarations.every((d) => d.getSourceFile().isDeclarationFile)) return null;
|
||||
const source = recvTs.getAliasTypeArguments()[0];
|
||||
const symbol = source?.getSymbol();
|
||||
const info = symbol ? lowerer.classBySymbol.get(symbol) : undefined;
|
||||
if (!info || call.questionDotToken || access.questionDotToken) return null;
|
||||
const found = findGenericMethodOn(lowerer, info, access.name.text);
|
||||
if (!found) return null;
|
||||
const loc = locOf(access.expression);
|
||||
const lowered = lowerer.lowerExpr(access.expression);
|
||||
const instance: IrType = { kind: "object", className: info.def.name };
|
||||
// A view that statically holds an instance of another class (a
|
||||
// structurally compatible one) calls that class's method in Node, not
|
||||
// C's: the call is refused rather than extracted as a C instance.
|
||||
if (
|
||||
lowered.type.kind === "object" &&
|
||||
lowered.type.className !== info.def.name &&
|
||||
!lowerer.isSubclassOf(lowered.type.className, info.def.name)
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
access,
|
||||
`calling the generic method '${access.name.text}' through a '${alias.name}' view of '${info.def.name}' that holds a '${lowerer.fmt(lowered.type)}'`,
|
||||
);
|
||||
}
|
||||
const receiver: IrExpr =
|
||||
lowered.type.kind === "object"
|
||||
? lowered
|
||||
: { kind: "dynCheck", value: lowerer.coerceToExpected(lowered, DYN), type: instance, loc };
|
||||
return lowerClassGenericMethodCall(lowerer, call, access, info, found, receiver);
|
||||
}
|
||||
|
||||
@@ -6340,6 +6340,10 @@ function dynSubsumableUnionArm(arm: IrType, ctx: TypeMapperCtx): boolean {
|
||||
return true;
|
||||
case "record":
|
||||
case "array":
|
||||
// `T | Promise<T>` with T bound to `unknown`: the checked-dynamic value
|
||||
// holds a promise by reference, so `unknown` absorbs the promise arm
|
||||
// exactly as the checker's own union reduction does.
|
||||
case "promise":
|
||||
return canConvertToDyn(
|
||||
arm,
|
||||
(id) => ctx.shapes.get(id),
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6495-generic-method-overloads.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6496-overload-signatures.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6497-generic-overload-bindings.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/overload-fences.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/pick-view-generic-methods.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Calls that resolve to OVERLOAD signatures of a generic method or
|
||||
// function: the implementation monomorphizes under what the overload
|
||||
// proves for its type parameters (a parameter the overload never mentions
|
||||
// is unknown), directly and through a `Pick` view of the class, whose
|
||||
// live receiver keeps the instance's state. `T | Promise<T>` callbacks
|
||||
// bind T from either arm.
|
||||
interface Reply {
|
||||
code: number;
|
||||
text: string;
|
||||
}
|
||||
type Mapper<T> = (reply: Reply) => T | Promise<T>;
|
||||
|
||||
class Store {
|
||||
loads = 0;
|
||||
async load<T>(key: string, map: Mapper<T>): Promise<T>;
|
||||
async load(key: string): Promise<Reply>;
|
||||
async load<T>(key: string, map?: Mapper<T>): Promise<T | Reply> {
|
||||
this.loads++;
|
||||
const reply: Reply = { code: 200, text: key };
|
||||
if (map === undefined) return reply;
|
||||
return await map(reply);
|
||||
}
|
||||
}
|
||||
|
||||
interface Services {
|
||||
store: Pick<Store, "load">;
|
||||
}
|
||||
|
||||
function pick<T>(items: T[], index: number): T;
|
||||
function pick<T>(items: T[]): T;
|
||||
function pick<T>(items: T[], index = 0): T {
|
||||
return items[index]!;
|
||||
}
|
||||
|
||||
async function settle<T>(reply: Reply, map?: Mapper<T>): Promise<T | Reply> {
|
||||
return map ? await map(reply) : reply;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const store = new Store();
|
||||
const plain = await store.load("alpha");
|
||||
console.log(plain.code, plain.text);
|
||||
const length = await store.load("beta", (reply) => reply.text.length);
|
||||
console.log(length * 2);
|
||||
const services: Services = { store };
|
||||
const viaView = await services.store.load("gamma");
|
||||
const upper = await services.store.load("delta", async (reply) => reply.text.toUpperCase());
|
||||
console.log(viaView.text, upper, store.loads);
|
||||
console.log(pick(["x", "y"]), pick([1, 2, 3], 2));
|
||||
console.log(JSON.stringify(await settle<Reply>({ code: 1, text: "s" })));
|
||||
console.log(await settle({ code: 2, text: "tt" }, (reply) => reply.code + reply.text.length));
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,7 +1,7 @@
|
||||
// The overload fences that REMAIN now that overload signatures lower:
|
||||
// generic overload calls (the per-instantiation body would be typed
|
||||
// against a signature it was never checked under), and the multi-
|
||||
// signature type surfacing where a binding needs it spelled.
|
||||
// Overload signatures: a generic overload call monomorphizes the
|
||||
// implementation under the bindings the overload proves (a parameter the
|
||||
// overload omits is optional in the implementation), and an immutable
|
||||
// alias of an overloaded declaration projects onto the implementation.
|
||||
function twice<T>(x: T): T[];
|
||||
function twice<T>(x: T, n: number): T[];
|
||||
function twice<T>(x: T, n?: number): T[] {
|
||||
@@ -0,0 +1,34 @@
|
||||
// Bindings of generic overload calls: explicit type arguments name the
|
||||
// overload's type parameters, which can be declared in another order than
|
||||
// the implementation's, and keyof-constrained parameters bound to
|
||||
// different literals through an overload read their own fields.
|
||||
function tag<B, A>(value: A, label: B): string;
|
||||
function tag<A, B>(value: A, label?: B): string {
|
||||
return `${typeof value}:${typeof label}:${String(value)}${String(label)}`;
|
||||
}
|
||||
console.log(tag<string, number>(5, "x"));
|
||||
console.log(tag<boolean, string>("y", true));
|
||||
|
||||
class Labels {
|
||||
prefix = "#";
|
||||
render<B, A>(value: A, label: B): string;
|
||||
render<A, B>(value: A, label?: B): string {
|
||||
return `${this.prefix}${typeof value}/${typeof label}`;
|
||||
}
|
||||
}
|
||||
console.log(new Labels().render<number, string>("z", 7));
|
||||
|
||||
interface Row {
|
||||
a: number;
|
||||
b: number;
|
||||
name: string;
|
||||
}
|
||||
function read<T, K extends keyof T>(row: T, key: K): T[K];
|
||||
function read<T, K extends keyof T>(row: T, key: K, fallback: T[K]): T[K];
|
||||
function read<T, K extends keyof T>(row: T, key: K, fallback?: T[K]): T[K] {
|
||||
const value = row[key];
|
||||
return value === undefined ? fallback! : value;
|
||||
}
|
||||
const row: Row = { a: 1, b: 2, name: "r" };
|
||||
console.log(read(row, "a"), read(row, "b"), read(row, "name"));
|
||||
console.log(read(row, "b", 0) + read(row, "a", 0));
|
||||
@@ -0,0 +1,16 @@
|
||||
// A generic method called through a `Pick` view of a class compiles
|
||||
// against that class. A view that holds an instance of a structurally
|
||||
// compatible, unrelated class is refused: Node calls that class's own
|
||||
// method.
|
||||
class Store {
|
||||
load<T>(key: string, map: (text: string) => T): T {
|
||||
return map(key);
|
||||
}
|
||||
}
|
||||
class Mirror {
|
||||
load<T>(key: string, map: (text: string) => T): T {
|
||||
return map(`${key}!`);
|
||||
}
|
||||
}
|
||||
const view: Pick<Store, "load"> = new Mirror();
|
||||
console.log(view.load("k", (text) => text.length));
|
||||
@@ -1,6 +0,0 @@
|
||||
overload-fences.ts:12:13 - error SC1090: calls selecting an overload signature of a generic function (only the implementation signature monomorphizes) is not supported yet
|
||||
|
||||
11 | }
|
||||
12 | console.log(twice("q").length);
|
||||
| ^~~~~~~~~~
|
||||
13 | function pick(x: "a"): string;
|
||||
@@ -0,0 +1,6 @@
|
||||
pick-view-generic-methods.ts:16:13 - error SC1090: calling the generic method 'load' through a 'Pick' view of 'Store' that holds a 'Mirror' is not supported yet
|
||||
|
||||
15 | const view: Pick<Store, "load"> = new Mirror();
|
||||
16 | console.log(view.load("k", (text) => text.length));
|
||||
| ^~~~~~~~~
|
||||
17 |
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2783, "san": 2211 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2032, "san": 1924 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2061, "san": 1900 }
|
||||
Reference in New Issue
Block a user