Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 137 additions & 14 deletions packages/compiler/src/frontend/lowering/generic-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
});
}
Expand All @@ -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,
});
}
Comment thread
vercel[bot] marked this conversation as resolved.

/** 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down
48 changes: 48 additions & 0 deletions packages/compiler/src/frontend/lowering/lower-calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down Expand Up @@ -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 };
Comment thread
vercel[bot] marked this conversation as resolved.
return lowerClassGenericMethodCall(lowerer, call, access, info, found, receiver);
}
4 changes: 4 additions & 0 deletions packages/compiler/src/frontend/type-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6495-generic-method-overloads.ts"
],
"diags": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6496-overload-signatures.ts"
],
"diags": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6497-generic-overload-bindings.ts"
],
"diags": []
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/pick-view-generic-methods.ts"
],
"diags": []
}
54 changes: 54 additions & 0 deletions tests/corpus/6495-generic-method-overloads.ts
Original file line number Diff line number Diff line change
@@ -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();
Original file line number Diff line number Diff line change
@@ -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[] {
Expand Down
34 changes: 34 additions & 0 deletions tests/corpus/6497-generic-overload-bindings.ts
Original file line number Diff line number Diff line change
@@ -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));
Loading
Loading