Files
scriptc/tests/corpus/recursive-generic-method-members.ts
T
Malte UblandClaude Opus 5.5 bd1bf39a76 Close recursive record knots through erased generic method members
A record or union that refers back to itself only closes its recursive
knot when the frame did not resolve anything through context-sensitive
hooks; otherwise the same checker type could answer differently per
instantiation, so the knot is refused. Erasing a generic method member
(`send<T>(message: Message<T>): string`) resolves the method's own type
parameters to `unknown`, and that resolution counted as context-sensitive.
Any recursive interface declaring a generic method was therefore refused:
`interface Sub extends Base { send<T>(m: Message<T>): string }` reached
from `Base { child?: Sub }`, or a list node with a generic method beside
its `next` field.

The member erasure now records its own type parameters, and resolving one
of them no longer counts. Consulting an enclosing binding still does, so a
generic method that mentions an outer type parameter keeps the fence. The
erased member's mapping is not memoized under the member's type, since the
same type maps to nothing outside the erasure.

pi's ReplacedSessionContext (sendMessage<T>) extends ExtensionCommandContext,
whose methods take callbacks over ReplacedSessionContext. The fence refused
ExtensionAPI.registerCommand and, through it, pi's main().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:18:18 -07:00

59 lines
1.6 KiB
TypeScript

// 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);