From dea433dacd1828f17c1bc76b3e15219b3b1b4df9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 28 Oct 2026 10:21:05 -0500 Subject: [PATCH] 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. --- .../src/frontend/lowering/receiver-returns.ts | 20 +++++++- ...ynamic-method-receiver-statements.mjs.json | 6 +++ .../dynamic-method-receiver-statements.mjs | 46 +++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dynamic-method-receiver-statements.mjs.json create mode 100644 tests/corpus/dynamic-method-receiver-statements.mjs diff --git a/packages/compiler/src/frontend/lowering/receiver-returns.ts b/packages/compiler/src/frontend/lowering/receiver-returns.ts index 5e83f236a..d1c160d7f 100644 --- a/packages/compiler/src/frontend/lowering/receiver-returns.ts +++ b/packages/compiler/src/frontend/lowering/receiver-returns.ts @@ -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 diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dynamic-method-receiver-statements.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dynamic-method-receiver-statements.mjs.json new file mode 100644 index 000000000..97d131a90 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/dynamic-method-receiver-statements.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/dynamic-method-receiver-statements.mjs" + ], + "diags": [] +} diff --git a/tests/corpus/dynamic-method-receiver-statements.mjs b/tests/corpus/dynamic-method-receiver-statements.mjs new file mode 100644 index 000000000..80c125475 --- /dev/null +++ b/tests/corpus/dynamic-method-receiver-statements.mjs @@ -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("|"));