Found while reviewing #11383 (#10877). Reproduces on f6ad6defe (PR #11383's merge base) and is not changed by #11383.
Repro
function F(this: any) {}
(F as any).prototype = { a: 1 };
const o: any = new (F as any)();
(F as any).prototype = { a: 2, b: 3 }; // o's [[Prototype]] is still { a: 1 }
console.log(o.a, o.b, Object.getPrototypeOf(o) === (F as any).prototype);
o.own = 1;
console.log(o.a, o.b);
|
line 1 |
line 2 |
| node |
1 undefined false |
1 undefined |
perry f6ad6defe |
1 3 false |
1 3 |
o.b answers 3, a property of an object that is not on o's prototype chain. Object.getPrototypeOf(o) is right, so the recorded per-instance link is right. The read path is wrong.
Mechanism
A new F() instance carries F's synthetic class id and a recorded per-instance prototype link. On an own-key miss, get_field_by_name_object_tail asks the class-id walk first (resolve_proto_chain_field_with_receiver). That walk reads CLASS_PROTOTYPE_OBJECTS[F], which is F's current .prototype. Only after that does it ask resolve_inherited_field, which reads the instance's own [[Prototype]]. When F.prototype has been reassigned since construction, the class-id walk answers from the wrong object. This is the same pair of walks behind #10877's 2^depth cost on F.prototype = new G() chains. #11383 skips the second walk when the two name the same object. It does not change which walk answers.
Correct design
The instance's own [[Prototype]] is the authority: after #11342 it is part of the instance's shape and meta. For an instance whose prototype was recorded at construction, the class-id walk must not answer. The read walks the recorded chain once. That also makes #11383's resolve_proto_chain_field_noting_miss reconciliation unnecessary. Related: #10507, where F.prototype identity is re-derived from side tables on every operation.
Found while reviewing #11383 (#10877). Reproduces on
f6ad6defe(PR #11383's merge base) and is not changed by #11383.Repro
1 undefined false1 undefinedf6ad6defe1 3 false1 3o.banswers3, a property of an object that is not ono's prototype chain.Object.getPrototypeOf(o)is right, so the recorded per-instance link is right. The read path is wrong.Mechanism
A
new F()instance carries F's synthetic class id and a recorded per-instance prototype link. On an own-key miss,get_field_by_name_object_tailasks the class-id walk first (resolve_proto_chain_field_with_receiver). That walk readsCLASS_PROTOTYPE_OBJECTS[F], which is F's current.prototype. Only after that does it askresolve_inherited_field, which reads the instance's own[[Prototype]]. WhenF.prototypehas been reassigned since construction, the class-id walk answers from the wrong object. This is the same pair of walks behind #10877's2^depthcost onF.prototype = new G()chains. #11383 skips the second walk when the two name the same object. It does not change which walk answers.Correct design
The instance's own
[[Prototype]]is the authority: after #11342 it is part of the instance's shape and meta. For an instance whose prototype was recorded at construction, the class-id walk must not answer. The read walks the recorded chain once. That also makes #11383'sresolve_proto_chain_field_noting_missreconciliation unnecessary. Related: #10507, where F.prototype identity is re-derived from side tables on every operation.