Inherit the base constructor after a prototype's constructor is deleted

A constructor read on a class instance through an untyped value answered
the class itself whenever the class's prototype object did not hold an own
`constructor`. After `delete Object.getPrototypeOf(new Middle()).constructor`,
Node reads the property the base class's prototype holds, and past the last
class's prototype Object.prototype's.

The read now checks the instance's own property, then walks the class's
prototypes: each answers its own `constructor`; a prototype without one
answers its class unless reflection had populated it, in which case the
program deleted the property and the read continues to the base class's
prototype, and from the last one to Object.prototype. Reflection sets a
per-class flag when the prototype initializes, so the answer does not
depend on whether the reflection was compiled before the read.

Corpus 8260 gains the deletion case.
This commit is contained in:
Chris Tate
2026-10-24 03:14:24 -05:00
parent 3b951ef4e2
commit 03e677a9c4
4 changed files with 111 additions and 65 deletions
@@ -52,6 +52,7 @@ import {
import {
classPrototypeData,
hasClassPrototypeData,
prototypeReflectedFlag,
observedPrototypeMethod,
reflectClassPrototype,
} from "./class-prototypes.js";
@@ -4105,34 +4106,13 @@ export class ClassDynamicDispatch {
return { kind: "call", callee: name, args: [bag, key], type: BOOL, loc };
}
/** `%dyn.class.bagHasOwn[OrPrototype]`: whether an instance's property bag
* holds `key` as its own property, or, with `prototype`, whether the bag's
* prototype object does. False before the instance has a bag. */
private readBagHasOwn(
lowerer: Lowerer,
bag: IrExpr,
key: IrExpr,
prototype: boolean,
loc: SrcLoc,
): IrExpr {
const name = prototype ? "%dyn.class.bagHasOwnOrPrototype" : "%dyn.class.bagHasOwn";
/** `%dyn.class.bagHasOwn`: whether an instance's property bag holds `key`
* as its own property. False before the instance has a bag. */
private readBagHasOwn(lowerer: Lowerer, bag: IrExpr, key: IrExpr, loc: SrcLoc): IrExpr {
const name = "%dyn.class.bagHasOwn";
let helper = lowerer.liftedFnNamed(name);
if (!helper) {
const value = varRef("p.0", DYN, loc);
const keyRef = varRef("p.1", STRING, loc);
const proto = varRef("proto", DYN, loc);
const hasOwn = (target: IrExpr): IrExpr => ({
kind: "libCall",
fn: "dyn.hasOwn",
args: [target, keyRef],
type: BOOL,
loc,
});
const returnFalse: IrStmt = {
kind: "return",
value: { kind: "boolLit", value: false, type: BOOL, loc },
loc,
};
helper = {
name,
params: [
@@ -4143,47 +4123,28 @@ export class ClassDynamicDispatch {
locals: [
{ id: "p.0", name: "bag", type: DYN, mutable: false },
{ id: "p.1", name: "key", type: STRING, mutable: false },
...(prototype ? [{ id: "proto", name: "proto", type: DYN, mutable: false }] : []),
],
body: [
{
kind: "if",
cond: { kind: "dynTest", test: "undefined", value, type: BOOL, loc },
then: [returnFalse],
then: [
{ kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc },
],
else_: null,
loc,
},
...(prototype
? ([
{
kind: "if",
cond: hasOwn(value),
then: [
{
kind: "return",
value: { kind: "boolLit", value: true, type: BOOL, loc },
loc,
},
],
else_: null,
loc,
},
{
kind: "varDecl",
localId: "proto",
init: { kind: "libCall", fn: "dyn.getPrototype", args: [value], type: DYN, loc },
loc,
},
{
kind: "if",
cond: { kind: "dynTest", test: "nullish", value: proto, type: BOOL, loc },
then: [returnFalse],
else_: null,
loc,
},
{ kind: "return", value: hasOwn(proto), loc },
] satisfies IrStmt[])
: [{ kind: "return", value: hasOwn(value), loc } satisfies IrStmt]),
{
kind: "return",
value: {
kind: "libCall",
fn: "dyn.hasOwn",
args: [value, varRef("p.1", STRING, loc)],
type: BOOL,
loc,
},
loc,
},
],
loc,
};
@@ -4193,6 +4154,45 @@ export class ClassDynamicDispatch {
return { kind: "call", callee: name, args: [bag, key], type: BOOL, loc };
}
/** The `constructor` an instance of `info` inherits when it has no own
* one. The class's prototype view answers its own `constructor` (a
* replacement, or the class once reflection installed it). A view
* without one answers the class itself unless reflection had populated it
* (prototypeReflectedFlag, set when the view initializes): then the
* program deleted the property, and the read continues to the base
* class's prototype and past the last one to Object.prototype, as in
* Node. */
private inheritedConstructor(
lowerer: Lowerer,
info: ClassInfo,
key: IrExpr,
loc: SrcLoc,
): IrExpr {
const cls = lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl!), DYN);
const prototype = hasClassPrototypeData(info) ? classPrototypeData(lowerer, info, loc) : null;
if (!prototype) return cls;
const base = info.base;
const next: IrExpr =
base && !base.builtinError && base.decl && !base.generic && !base.localClass
? this.inheritedConstructor(lowerer, base, key, loc)
: { kind: "dynKeyGet", value: prototype, key, type: DYN, loc };
return {
kind: "ternary",
cond: { kind: "libCall", fn: "dyn.hasOwn", args: [prototype, key], type: BOOL, loc },
then: { kind: "dynKeyGet", value: prototype, key, type: DYN, loc },
else_: {
kind: "ternary",
cond: varRef(prototypeReflectedFlag(lowerer, info), BOOL, loc),
then: next,
else_: cls,
type: DYN,
loc,
},
type: DYN,
loc,
};
}
/** The hints for which some boxed class declares a conversion method. A
* string conversion calls toString first, so without a declared toString
* or Symbol.toPrimitive it answers Object.prototype.toString's
@@ -5951,15 +5951,13 @@ export class ClassDynamicDispatch {
!info.classDecorators
) {
this.constructors.set(info.def.name, info);
const cls = lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl), DYN);
const bag = this.branchPropertyBag(lowerer, info, receiver, false, loc);
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
// `constructor` is an own property of every class prototype, so an
// ancestor's prototype never answers for an instance of this class:
// the instance's own property answers, then one this class's
// prototype object defines (a reflected prototype, or a write to
// `C.prototype.constructor`), and otherwise the class itself.
const answered = this.readBagHasOwn(lowerer, bag, key, hasClassPrototypeData(info), loc);
// ancestor's prototype answers for an instance of this class only
// after the program deletes the class's own: the instance's own
// property answers, then the class's prototype (inheritedConstructor).
const answered = this.readBagHasOwn(lowerer, bag, key, loc);
return [
{
kind: "return",
@@ -5973,7 +5971,7 @@ export class ClassDynamicDispatch {
type: DYN,
loc,
},
else_: cls,
else_: this.inheritedConstructor(lowerer, info, key, loc),
type: DYN,
loc,
},
@@ -299,6 +299,23 @@ function declaresPrototypeMembers(decl: ts.ClassLikeDeclaration): boolean {
);
}
/** The global that is true once the class's prototype view has been
* populated by reflection (reflectClassPrototype): the view then holds an
* own `constructor` unless the program deleted it. A view that was never
* reflected leaves `constructor` implicit. */
export function prototypeReflectedFlag(lowerer: Lowerer, info: ClassInfo): string {
if (info.prototypeReflectedFlag) return info.prototypeReflectedFlag;
const id = `%g.%prototype.data.${info.def.name}.reflected`;
lowerer.globalsList.push({
id,
name: `${info.def.jsName ?? info.def.name}.prototype.reflected`,
type: BOOL,
mutable: true,
});
info.prototypeReflectedFlag = id;
return id;
}
/** Populate the shared prototype view before exposing it to reflection.
* Methods and accessors are non-enumerable and retain declaration identity. */
export function reflectClassPrototype(
@@ -399,6 +416,12 @@ export function reflectClassPrototype(
{ key: "value", value: classValueRef(lowerer, info, info.decl) },
writable,
]),
{
kind: "assign",
localId: prototypeReflectedFlag(lowerer, info),
value: { kind: "boolLit", value: true, type: BOOL, loc },
loc,
},
);
const context = newFnCtx(false, null, null, DYN);
lowerer.fnStack.push(context);
@@ -481,6 +481,10 @@ export interface ClassInfo {
localPrototypeData?: true;
prototypeMethodValues?: Map<string, IrExpr>;
prototypeReflectionReady?: true;
/** The global that records at run time that the prototype view was
* populated by reflection, so it holds its own `constructor` until the
* program deletes it (prototypeReflectedFlag). */
prototypeReflectedFlag?: string;
prototypeObservedMethodsReady?: true;
/** The globals holding each observed method's original prototype value
* (set when the prototype data is built), by method name. A typed call
@@ -132,3 +132,24 @@ const areaValue = Shape.prototype.area;
const disc = new Disc("d");
const rebuilt = disc.rebuild();
console.log(disc.constructor === Disc, rebuilt instanceof Disc, rebuilt.area(), rebuilt.name, typeof areaValue);
// Deleting a reflected prototype's constructor: instances read the
// constructor their base class's prototype holds, then Object's; a
// replacement answers again.
class Root {
hi() {
return "hi";
}
}
class Middle extends Root {}
class Tip extends Middle {}
const members = [new Middle(), new Tip(), new Root(), {}];
const constructorNames = () => members.map((member) => member.constructor.name).join(",");
console.log(constructorNames(), Object.getOwnPropertyNames(Object.getPrototypeOf(new Middle())).join("|"));
delete Object.getPrototypeOf(new Middle()).constructor;
console.log(constructorNames(), Object.getOwnPropertyNames(Object.getPrototypeOf(new Middle())).join("|"));
console.log(new Middle().constructor === Root, new Tip().constructor === Tip);
delete Object.getPrototypeOf(new Root()).constructor;
console.log(constructorNames(), new Middle().constructor === Object);
Middle.prototype.constructor = function Named() {};
console.log(constructorNames());