A prototype method replaced after first call is not picked up when the receiver arrives as a typed parameter, though it is picked up on a direct call.
class P { v = 1; m() { return 1; } }
function run(o: P, n: number) { let s = 0; for (let i = 0; i < n; i++) s += (o as any).m(); return s; }
const p = new P();
const before = run(p, 3);
(P.prototype as any).m = function () { return 100; };
const after = run(p, 3);
console.log(before, after);
node v26.5.1 3 300
perry 3 3
Patching mid-loop behaves the same way:
const p = new P(); let s = 0;
for (let i = 0; i < 6; i++) { s += p.m(); if (i === 2) (P.prototype as any).m = function () { return 50; }; }
// node: 153 perry: 6
The direct-call form is correct, which is what makes this narrow:
const p = new P(); const a = p.m();
(P.prototype as any).m = function () { return "patched"; };
console.log(a, p.m()); // node: orig patched perry: orig patched ✓
Not a regression
Found while auditing #9124 and re-checked on #9130. Both reproduce identically on main, so this predates the inline-probe series — but it lives in exactly the code that series is changing (method_override.rs's guard selection), so it is worth pinning before more optimization lands on top.
Related shapes that are also wrong on main
From the same 23-shape probe, all identical on main and on #9124/#9130:
|
shape |
node |
perry |
Object.setPrototypeOf(c, {inc: () => 777}) then c.inc() |
777 |
2 |
|
Object.setPrototypeOf(p, Q.prototype) then p.m() |
"b" |
"a" |
|
c.inc.call(otherInstance) |
[101,101,1] |
TypeError |
|
get m() { return () => this.v * 7 } then g.m() |
21 |
TypeError |
|
delete c.v mid-loop then c.inc() |
NaN |
21 |
|
The first two are the same root as the headline case (a stale prototype edge); the last three look independent and may deserve splitting out once someone digs in.
Why it matters beyond spec conformance
Monkey-patching a prototype is how a lot of instrumentation, test doubles and polyfills work. Silently continuing to call the old body — with no error — is the failure mode most likely to be misdiagnosed as "my mock didn't get applied".
A prototype method replaced after first call is not picked up when the receiver arrives as a typed parameter, though it is picked up on a direct call.
Patching mid-loop behaves the same way:
The direct-call form is correct, which is what makes this narrow:
Not a regression
Found while auditing #9124 and re-checked on #9130. Both reproduce identically on main, so this predates the inline-probe series — but it lives in exactly the code that series is changing (
method_override.rs's guard selection), so it is worth pinning before more optimization lands on top.Related shapes that are also wrong on main
From the same 23-shape probe, all identical on main and on #9124/#9130:
Object.setPrototypeOf(c, {inc: () => 777})thenc.inc()7772Object.setPrototypeOf(p, Q.prototype)thenp.m()"b""a"c.inc.call(otherInstance)[101,101,1]TypeErrorget m() { return () => this.v * 7 }theng.m()21TypeErrordelete c.vmid-loop thenc.inc()NaN21The first two are the same root as the headline case (a stale prototype edge); the last three look independent and may deserve splitting out once someone digs in.
Why it matters beyond spec conformance
Monkey-patching a prototype is how a lot of instrumentation, test doubles and polyfills work. Silently continuing to call the old body — with no error — is the failure mode most likely to be misdiagnosed as "my mock didn't get applied".