Call the declared base method for super calls in JavaScript method values

Since 76735a8d9 (#1975), a `super.m()` call in a JavaScript class method
whose `this` is the call-site receiver reflected the base class's whole
prototype and read `m` from it. An instance's member lookup first tests
its property bag, which inherits from its class's prototype data, so once
the base's prototype was reflected, `child[k]` for a method the subclass
overrides found the base's method there: `value[name](arg)` on a subclass
instance called the base method.

While no code reads a prototype method by name, `super.m()` now calls the
declaring class's receiver-independent method value with the receiver, as
the method's [[HomeObject]] lookup answers, without reflecting the base
prototype. Programs that read prototype methods by name keep the
reflected lookup.
This commit is contained in:
Chris Tate
2026-10-25 00:26:55 -05:00
parent 3e54e0edb3
commit 478feb15cd
6 changed files with 94 additions and 11 deletions
@@ -194,7 +194,10 @@ import { classPrototypeData, reflectClassPrototype } from "./class-prototypes.js
import { constructionEscape } from "./construction-escapes.js";
import { checkedClassConstruction, checkedClassConstructionPacked } from "./class-construction.js";
import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js";
import { reflectedClassStaticMethodValue } from "./class-method-values.js";
import {
reflectedClassMethodValue,
reflectedClassStaticMethodValue,
} from "./class-method-values.js";
import { identifierReadSymbol } from "./surfaces.js";
export function returnsOnlyThis(member: ts.MethodDeclaration): boolean {
@@ -9560,19 +9563,31 @@ export function lowerSuperMethodCall(
// method up on the base class's complete prototype, as `super` reads
// [[HomeObject]].[[Prototype]], and calls it with that receiver.
const dynamicThis = thisRef.type.kind === "dyn";
// While no code reads a prototype method by name (which could replace it),
// that method is the declaring class's own: its receiver-independent value
// is called directly. Reflecting the base's whole prototype instead would
// put its members on the prototype chain of every subclass instance's
// property bag, ahead of the subclasses' own compiled overrides.
const declared =
dynamicThis && !lowerer.prototypeMethodAccesses.has(access.name.text)
? reflectedClassMethodValue(lowerer, found.declarer, access.name.text)
: null;
if (dynamicThis || lowerer.prototypeMethodAccesses.has(access.name.text)) {
const prototype =
dynamicThis && cls.base.decl
const prototype = declared
? null
: dynamicThis && cls.base.decl
? reflectClassPrototype(lowerer, cls.base, loc, classValueRef(lowerer, cls.base, access))
: classPrototypeData(lowerer, cls.base, loc);
if (prototype) {
const callee: IrExpr = {
kind: "dynKeyGet",
value: prototype,
key: { kind: "strLit", value: access.name.text, type: STRING, loc },
type: DYN,
loc,
};
if (declared || prototype) {
const callee: IrExpr = declared
? lowerer.coerceToExpected(declared, DYN)
: {
kind: "dynKeyGet",
value: prototype!,
key: { kind: "strLit", value: access.name.text, type: STRING, loc },
type: DYN,
loc,
};
// Spread arguments are spread into the dynamic call, as in a dynamic method call.
const spreads: { arg: number; what: string }[] = [];
const args = call.arguments.nodes.map((arg, index) => {
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/8302-class-super-method-values/lib.cjs",
"<repo>/tests/corpus/8302-class-super-method-values/main.ts"
],
"diags": []
}
@@ -0,0 +1,33 @@
"use strict";
// Subclasses whose methods call super, read and called through computed and
// literal keys, as values called with call(), and directly.
class Base {
constructor(n) { this.n = n; }
scale(k) { return this.n * k; }
label() { return "base:" + this.n; }
plain(k) { return k; }
}
class Child extends Base {
scale(k) { return super.scale(k) * 10; }
label() { return "child(" + super.label() + ")"; }
plain(k) { return k * 100; }
}
class Grandchild extends Child {
scale(k) { return super.scale(k) + 1; }
}
class Sibling extends Base {
scale(k) { return -k; }
plain(k) { return super.plain(k) + 0.5; }
}
exports.make = function (i) {
switch (i % 4) {
case 0: return new Base(i);
case 1: return new Child(i);
case 2: return new Grandchild(i);
default: return new Sibling(i);
}
};
exports.computed = function (value, name, arg) { return value[name](arg); };
exports.viaCall = function (value, name, arg) { const f = value[name]; return f.call(value, arg); };
exports.literal = function (value, arg) { return [value["scale"](arg), value["plain"](arg), value["label"]()].join(" "); };
exports.direct = function (value, arg) { return [value.scale(arg), value.plain(arg), value.label()].join(" "); };
@@ -0,0 +1,26 @@
// Methods of JavaScript subclasses that call super, read from instances
// through computed keys, literal keys and as values, and called directly.
import { createRequire } from "node:module";
interface Lib {
make(i: number): unknown;
computed(value: unknown, name: string, arg: number): unknown;
viaCall(value: unknown, name: string, arg: number): unknown;
literal(value: unknown, arg: number): string;
direct(value: unknown, arg: number): string;
}
const require = createRequire(import.meta.url);
const lib: Lib = require("./lib.cjs");
for (let i = 0; i < 8; i++) {
const value = lib.make(i);
console.log(
i,
lib.computed(value, "scale", 3),
lib.computed(value, "plain", 3),
lib.computed(value, "label", 0),
lib.viaCall(value, "scale", 2),
lib.viaCall(value, "label", 0),
);
console.log(i, lib.literal(value, 5), "|", lib.direct(value, 5));
}
@@ -0,0 +1 @@
{ "type": "module" }
@@ -0,0 +1 @@
{ "plain": 4797 }