Discard the result of void generic methods called through class-backed interfaces (#2275)

This commit is contained in:
Chris Tate
2026-10-25 18:56:01 -05:00
committed by GitHub
5 changed files with 111 additions and 19 deletions
@@ -6794,24 +6794,41 @@ export class ClassDynamicDispatch {
type: result,
loc,
};
// A void method dispatches its overrides with statements: a ternary
// has no void form.
let voidDispatch: IrStmt[] = [{ kind: "exprStmt", expr: call, loc }];
for (const override of overrides.toReversed()) {
const subType: IrType = { kind: "object", className: override.cls.def.name };
const isOverride: IrExpr = {
kind: "instanceOf",
value: receiver,
className: override.cls.def.name,
type: BOOL,
loc,
};
const overrideCall: IrExpr = {
kind: "call",
callee: override.callee,
args: [{ kind: "downcast", value: receiver, type: subType, loc }, ...args],
type: result,
loc,
};
if (result.kind === "void") {
voidDispatch = [
{
kind: "if",
cond: isOverride,
then: [{ kind: "exprStmt", expr: overrideCall, loc }],
else_: voidDispatch,
loc,
},
];
continue;
}
call = {
kind: "ternary",
cond: {
kind: "instanceOf",
value: receiver,
className: override.cls.def.name,
type: BOOL,
loc,
},
then: {
kind: "call",
callee: override.callee,
args: [{ kind: "downcast", value: receiver, type: subType, loc }, ...args],
type: result,
loc,
},
cond: isOverride,
then: overrideCall,
else_: call,
type: result,
loc,
@@ -6819,10 +6836,7 @@ export class ClassDynamicDispatch {
}
let body: IrStmt[];
if (result.kind === "void")
body = [
{ kind: "exprStmt", expr: call, loc },
{ kind: "return", value: dynUndefinedExpr(loc), loc },
];
body = [...voidDispatch, { kind: "return", value: dynUndefinedExpr(loc), loc }];
else {
const boxed = lowerer.coerceToExpected(call, DYN);
if (boxed.type.kind !== "dyn") return fence();
@@ -10107,7 +10107,13 @@ function lowerObjLitGenericMethodCall(
type: DYN,
loc,
};
return lowerer.coerceInto(call, invoked, lowerer.irTypeOf(call));
const resultType = lowerer.irTypeOf(call);
// A void method's result stays the runtime's untyped value, as a
// call through a non-generic void member's dispatch keeps it: a class
// may implement the member with a method that returns a value, which
// JavaScript passes on (`console.log(c.publish(m))`).
if (resultType.kind === "void") return invoked;
return lowerer.coerceInto(call, invoked, resultType);
}
}
if (onInterface) {
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/generic-interface-void-calls.ts"
],
"diags": []
}
@@ -0,0 +1,65 @@
// Calling a void generic method through an interface value that classes
// implement (the receiver is a checked-dynamic object, and the call
// dispatches by name to the class's erased method) discards the result, as
// JavaScript does, instead of converting it to `void`.
interface Message<T = unknown> {
content: string;
details?: T;
}
interface Channel {
name: string;
publish<T = unknown>(message: Message<T>): void;
count<T>(items: T[]): number;
}
const delivered: string[] = [];
class LogChannel implements Channel {
name: string;
constructor(name: string) {
this.name = name;
}
publish<T = unknown>(message: Message<T>): void {
delivered.push(`${this.name}:${message.content}:${message.details === undefined ? "-" : JSON.stringify(message.details)}`);
}
count<T>(items: T[]): number {
return items.length;
}
}
class LoudChannel extends LogChannel {
publish<T = unknown>(message: Message<T>): void {
super.publish({ content: message.content.toUpperCase(), details: message.details });
}
}
const channels: Channel[] = [new LogChannel("quiet"), new LoudChannel("loud")];
for (const channel of channels) {
channel.publish({ content: "hello", details: [1, 2] });
channel.publish({ content: "bare" });
const result = channel.publish<string>({ content: "typed", details: "x" });
console.log(channel.name, result === undefined, channel.count(["a", "b", "c"]));
}
console.log(delivered.join(" | "));
// A class may implement the void member with a method that returns a value;
// JavaScript passes that value on.
class CountingChannel implements Channel {
name = "counting";
sent = 0;
publish<T = unknown>(message: Message<T>): number {
this.sent++;
return message.content.length;
}
count<T>(items: T[]): number {
return items.length * 2;
}
}
const counted: Channel[] = [new CountingChannel(), new LogChannel("plain")];
for (const channel of counted) {
console.log(channel.name, channel.publish({ content: "four" }));
console.log(`sent ${channel.publish<number>({ content: "again", details: 2 })}`);
}
@@ -0,0 +1 @@
{ "plain": 4000 }