Call the receiver copy for direct calls whose result is discarded

A direct call to an untyped method that returns `this` on every path,
used as a statement (`builder.add(part);`), boxed its receiver into a
capsule that nothing read. Such a call now runs the method's copy that
returns nothing, as the class dispatch helpers already do.
This commit is contained in:
Chris Tate
2026-10-28 10:21:05 -05:00
parent 3a476d5f32
commit dea433dacd
3 changed files with 70 additions and 2 deletions
@@ -7,8 +7,10 @@
* (`%dyn.class.call.N`) that calls such a method already holds a capsule
* of the same instance at its exact class: its receiver after
* normalization. The helper now calls a copy of the method that returns
* nothing and answers its own receiver, so the call boxes nothing. Other
* callers (vtables, typed code, method values) keep the method itself. */
* nothing and answers its own receiver, so the call boxes nothing. A
* direct call whose result nothing reads calls the copy too. Other callers
* (vtables, method values, calls that use the result) keep the method
* itself. */
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
import { VOID } from "../../ir/ir.js";
import { varRef } from "../../ir/build.js";
@@ -57,6 +59,20 @@ export function returnDispatchReceivers(lowerer: Lowerer, functions: IrFunction[
];
}
}
// A direct call whose result nothing reads (`builder.set(1);` in typed
// code) needs no capsule at all.
for (const fn of [...functions]) {
everyStmtList(fn.body, {
stmt: (s) => {
if (s.kind === "exprStmt" && s.expr.kind === "call") {
const copy = copyOf(s.expr.callee);
if (copy) s.expr = { ...s.expr, callee: copy.name, type: VOID };
}
return true;
},
expr: () => true,
});
}
}
/** The helper's receiver is assigned only by its normalization, so it is
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/dynamic-method-receiver-statements.mjs"
],
"diags": []
}
@@ -0,0 +1,46 @@
// Untyped methods that return their receiver, called as statements on
// receivers whose class is known: side effects, throws, overrides, and
// results that are used.
class Builder {
constructor() {
this.parts = [];
this.count = 0;
}
add(part) {
if (part === "") throw new TypeError("empty part");
this.parts.push(part);
this.count++;
return this;
}
reset() {
this.parts.length = 0;
return this;
}
}
class LoudBuilder extends Builder {
add(part) {
super.add(part.toUpperCase());
return this;
}
}
const b = new Builder();
for (let i = 0; i < 1000; i++) {
b.add("p" + (i % 3));
if (i % 100 === 99) b.reset();
}
console.log(b.count, b.parts.join(","));
b.add("x").add("y");
const same = b.add("z");
console.log(same === b, b.parts.join(","));
try {
b.add("");
} catch (e) {
console.log(e instanceof TypeError, e.message, b.count);
}
const loud = new LoudBuilder();
loud.add("a");
loud.add("b").add("c");
console.log(loud.parts.join(","), loud.count, loud instanceof Builder);
const builders = [new Builder(), new LoudBuilder()];
for (const each of builders) each.add("q");
console.log(builders.map((each) => each.parts.join("")).join("|"));