mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Close recursive record knots through erased generic method members (#2246)
This commit is contained in:
@@ -1255,6 +1255,10 @@ export interface TypeMapperCtx {
|
||||
* (erasedGenericMemberType): the signature's own type parameters
|
||||
* resolve to `unknown` instead of refusing the mapping. */
|
||||
erasingSignature?: ts.Signature;
|
||||
/** Set with erasingSignature: the signature's own type parameters, and
|
||||
* whether the erasure consulted an enclosing binding. An erasure that
|
||||
* resolved only its own parameters answers the same in every context. */
|
||||
erasedMember?: { own: ReadonlySet<ts.Type>; outerConsulted: boolean };
|
||||
/** True while an erased generic body lowers (its type parameters are
|
||||
* bound to `unknown`). */
|
||||
isErasing?: () => boolean;
|
||||
@@ -1835,6 +1839,7 @@ export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
memoKey !== null &&
|
||||
topLevel &&
|
||||
contextResolutions === sensitivityAtEntry &&
|
||||
ctx.erasingSignature === undefined &&
|
||||
(ctx.canMemoizeType?.() ?? true)
|
||||
) {
|
||||
ctx.typeMemo?.set(memoKey, mapped);
|
||||
@@ -1991,7 +1996,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
!ERASURE_KEPT_CONTAINERS.has(ts.symbolNameOf(type.getSymbol()) ?? "") &&
|
||||
mentionsErasedTypeParam(type, ctx, 0)
|
||||
) {
|
||||
contextResolutions++;
|
||||
if (!ctx.erasedMember || ctx.erasedMember.outerConsulted) contextResolutions++;
|
||||
return DYN;
|
||||
}
|
||||
// Readonly<C> over a program class (Object.freeze's result type) is the
|
||||
@@ -2001,7 +2006,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
if (resolveTypeParam && type.flags & ts.TypeFlags.TypeParameter) {
|
||||
const bound = resolveTypeParam(type);
|
||||
if (bound) {
|
||||
contextResolutions++;
|
||||
if (!ctx.erasedMember?.own.has(type)) contextResolutions++;
|
||||
return bound;
|
||||
}
|
||||
}
|
||||
@@ -7335,10 +7340,21 @@ export function erasedGenericMemberType(t: ts.Type, ctx: TypeMapperCtx): IrType
|
||||
const sig = sigs[0]!;
|
||||
const own = new Set<ts.Type>(sig.getTypeParameters());
|
||||
const outer = ctx.resolveTypeParam;
|
||||
// Resolving only the member's own parameters does not make the erasure
|
||||
// context-sensitive, so a recursive record holding a generic method
|
||||
// (`interface Sub extends Base { send<T>(m: Msg<T>): void }` reached
|
||||
// from Base) still closes its knot. Consulting an enclosing binding does.
|
||||
const erasedMember = { own, outerConsulted: false };
|
||||
const erased = mapType(t, {
|
||||
...ctx,
|
||||
erasingSignature: sig,
|
||||
resolveTypeParam: (param) => (own.has(param) ? DYN : (outer?.(param) ?? null)),
|
||||
erasedMember,
|
||||
resolveTypeParam: (param) => {
|
||||
if (own.has(param)) return DYN;
|
||||
if (outer === undefined) return null;
|
||||
erasedMember.outerConsulted = true;
|
||||
return outer(param);
|
||||
},
|
||||
});
|
||||
return erased?.kind === "func" ? erased : undefined;
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/recursive-generic-method-members.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// A recursive record that declares a generic method compiles. Erasing the
|
||||
// method's own type parameters does not depend on any enclosing generic
|
||||
// binding, so the record still closes its recursive knot: a parent
|
||||
// interface whose child is a subinterface declaring a generic method, and a
|
||||
// list node whose generic method sits beside a self-referencing field.
|
||||
|
||||
interface Message<T = unknown> {
|
||||
content: string;
|
||||
details?: T;
|
||||
}
|
||||
|
||||
interface TreeNode {
|
||||
name: string;
|
||||
child?: SendingNode;
|
||||
}
|
||||
|
||||
interface SendingNode extends TreeNode {
|
||||
send<T = unknown>(message: Message<T>): string;
|
||||
}
|
||||
|
||||
interface ListNode {
|
||||
value: number;
|
||||
next: ListNode | null;
|
||||
map<U>(f: (value: number) => U): string;
|
||||
}
|
||||
|
||||
function describe(node: TreeNode): string {
|
||||
const inner = node.child;
|
||||
return inner ? `${node.name} > ${describe(inner)}` : node.name;
|
||||
}
|
||||
|
||||
function leaf(name: string): SendingNode {
|
||||
return { name, send: () => `${name} got a message` };
|
||||
}
|
||||
|
||||
const tree: SendingNode = {
|
||||
name: "root",
|
||||
child: { name: "middle", child: leaf("leaf"), send: () => "middle got a message" },
|
||||
send: () => "root got a message",
|
||||
};
|
||||
console.log(describe(tree));
|
||||
console.log(tree.send({ content: "hello" }));
|
||||
const middle = tree.child;
|
||||
if (middle !== undefined) console.log(middle.send({ content: "hi", details: 42 }));
|
||||
|
||||
function cons(value: number, next: ListNode | null): ListNode {
|
||||
return { value, next, map: () => `node ${value}` };
|
||||
}
|
||||
|
||||
let list: ListNode | null = cons(1, cons(2, cons(3, null)));
|
||||
const values: number[] = [];
|
||||
let labels = "";
|
||||
while (list !== null) {
|
||||
values.push(list.value);
|
||||
labels += `[${list.map((v) => v * 2)}]`;
|
||||
list = list.next;
|
||||
}
|
||||
console.log(values.join(","), labels);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4000 }
|
||||
Reference in New Issue
Block a user