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(","));