From 02ff0eb16ba16b742278d200940e91c8a223aac5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 19:02:23 -0500 Subject: [PATCH] Read the getter first for classes that a later lowering wave reaches A call rewritten for a getter-only name records the classes it tests. When a later wave reaches another class with the same getter, the fallback call is rewritten again for that class, instead of being skipped, so its instances also read the getter before the arguments. Before, `target.step(arg)` on such an instance threw "target.step is not a function". --- .../lowering/class-dynamic-dispatch.ts | 23 +++++++++---- tests/corpus/dynamic-call-callee-order.mjs | 32 +++++++++++++++++++ 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index 7d170d24a..45c51096a 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -1214,6 +1214,11 @@ export class ClassDynamicDispatch { * classes included: the value's capsule can be any of its subclasses. */ private readonly boxedStatic = new Set(); private readonly dispatches = new Map(); + /** Per receiver local of a call rewritten for a getter-only name, the + * classes whose instances it already reads the getter on. A later wave + * that reaches another class with the getter rewrites the fallback call + * for that class. */ + private readonly getterReceivers = new Map>(); private readonly properties = new Map(); private readonly computed = new Map< string, @@ -1919,15 +1924,19 @@ export class ClassDynamicDispatch { fn: IrFunction, invoke: IrExpr & { kind: "dynInvoke" }, capsules: readonly string[], + covered: ReadonlySet | undefined, ): IrExpr => { const loc = invoke.loc; const receiverLocal = { - id: `${GETTER_RECEIVER}${fn.locals.length}`, + id: `${GETTER_RECEIVER}${this.getterReceivers.size}`, name: "receiver", type: DYN, mutable: false, }; fn.locals.push(receiverLocal); + const keys = new Set(capsules); + if (covered) for (const key of covered) keys.add(key); + this.getterReceivers.set(receiverLocal.id, keys); const calleeLocal = { id: `%dispatch.callee.${fn.locals.length}`, name: "callee", @@ -3069,12 +3078,14 @@ export class ClassDynamicDispatch { invoke && expr.kind === "dynInvoke" && candidates(invoke.method).length === 0 && - !staticCandidates(invoke.method) && - // The other receivers' call this rewrite left in place. - !(expr.recv.kind === "varRef" && expr.recv.localId.startsWith(GETTER_RECEIVER)) + !staticCandidates(invoke.method) ) { - const capsules = getterCapsules(invoke.method); - if (capsules.length > 0) return getterCall(fn, expr, capsules); + // The other receivers' call a rewrite left in place tests only + // the classes that rewrite did not. + const covered = + expr.recv.kind === "varRef" ? this.getterReceivers.get(expr.recv.localId) : undefined; + const capsules = getterCapsules(invoke.method).filter((key) => !covered?.has(key)); + if (capsules.length > 0) return getterCall(fn, expr, capsules, covered); } if ( !invoke || diff --git a/tests/corpus/dynamic-call-callee-order.mjs b/tests/corpus/dynamic-call-callee-order.mjs index 39ea01c9e..e2bc4bf97 100644 --- a/tests/corpus/dynamic-call-callee-order.mjs +++ b/tests/corpus/dynamic-call-callee-order.mjs @@ -122,3 +122,35 @@ function filtered(value) { } console.log(new Material().map, doubled([1, 2, 3]).join(","), filtered([4, 5]).join(",")); console.log(fixed(3.14159, 2), fixed(new Material(), 3), filtered(new Material())); +// A second class with the same getter, constructed only in a method that +// an untyped call reaches: its instances also read the getter before the +// arguments. +const order = []; +class EarlyRunner { + get step() { + order.push("early-get"); + return (value) => `early-${value}`; + } +} +class LateRunner { + get step() { + order.push("late-get"); + return (value) => `late-${value}`; + } +} +class RunnerFactory { + make() { + return new LateRunner(); + } +} +function noted(value) { + order.push(`arg-${value}`); + return value; +} +function step(target, value) { + return target.step(noted(value)); +} +function made(factory) { + return factory.make(); +} +console.log(step(new EarlyRunner(), 1), step(made(new RunnerFactory()), 2), order.join(","));