Summary
obj.method = newFn writes an own property on the instance, but Perry's class-method dispatch always invokes the prototype/class-vtable method — the own-property override is ignored. Bites any code that uses the "first call sets up the fast path" pattern (SmartRouter in hono, memoized dispatch tables, polymorphic dispatch via assignment).
Minimal repro
class Router {
routes: any[] | undefined = [];
match(path: string): string {
console.log("[Router.match] original called");
const inner = (p: string) => `inner:${p}`;
this.match = inner;
return inner(path);
}
}
const r = new Router();
console.log(r.match("/foo"));
console.log(r.match("/bar"));
Expected (Node):
[Router.match] original called
inner:/foo
inner:/bar ← second call goes to `inner`
Actual (Perry):
[Router.match] original called
inner:/foo
[Router.match] original called ← second call ALSO hits the original method
inner:/bar
Why it matters
Common JS pattern. Hono's SmartRouter.match (node_modules/hono/dist/router/smart-router/router.js:23) does exactly this:
match(method, path) {
if (!this.#routes) throw new Error("Fatal error");
// ... pick a sub-router, build it ...
this.match = router.match.bind(router); // ← rebind for fast path
this.#routes = void 0;
return res;
}
On the second app.fetch(...) call, Perry calls the original SmartRouter.match again instead of the bound router.match. The body checks if (!this.#routes) throw new Error("Fatal error") and throws — surfacing in #603 as silent failure of every call after the first.
Same pattern in jQuery, Underscore, lodash's once/memoize, redux's lazy action creators, and any framework that uses dispatch-table promotion.
Fix shape
Class method dispatch should consult the instance's own-property dictionary BEFORE falling back to the class vtable. Currently the dispatch path in crates/perry-codegen/src/lower_call.rs and the js_object_call_method runtime helper short-circuit to the class vtable when the receiver is a known class instance.
This may be invasive — the fast path optimisation that skips the property dictionary is load-bearing for class-method-call performance. Possible mitigations:
- Detect "instance has been mutated to override
method" via a per-instance flag and disable the fast path for that instance only.
- Add a write-barrier on
this.<methodName> = X for known class methods that flips a "shadowed methods" bitset on the receiver, consulted by the dispatch path.
- Less invasively: special-case the assignment shape
this.<bound-method-name> = X inside a class method body and emit a runtime call that records the override in the instance's overflow property dictionary, plus a runtime arm in js_native_call_method / class method dispatch that checks the dict first.
Environment
Summary
obj.method = newFnwrites an own property on the instance, but Perry's class-method dispatch always invokes the prototype/class-vtable method — the own-property override is ignored. Bites any code that uses the "first call sets up the fast path" pattern (SmartRouter in hono, memoized dispatch tables, polymorphic dispatch via assignment).Minimal repro
Expected (Node):
Actual (Perry):
Why it matters
Common JS pattern. Hono's
SmartRouter.match(node_modules/hono/dist/router/smart-router/router.js:23) does exactly this:On the second
app.fetch(...)call, Perry calls the originalSmartRouter.matchagain instead of the boundrouter.match. The body checksif (!this.#routes) throw new Error("Fatal error")and throws — surfacing in #603 as silent failure of every call after the first.Same pattern in jQuery, Underscore, lodash's once/memoize, redux's lazy action creators, and any framework that uses dispatch-table promotion.
Fix shape
Class method dispatch should consult the instance's own-property dictionary BEFORE falling back to the class vtable. Currently the dispatch path in
crates/perry-codegen/src/lower_call.rsand thejs_object_call_methodruntime helper short-circuit to the class vtable when the receiver is a known class instance.This may be invasive — the fast path optimisation that skips the property dictionary is load-bearing for class-method-call performance. Possible mitigations:
method" via a per-instance flag and disable the fast path for that instance only.this.<methodName> = Xfor known class methods that flips a "shadowed methods" bitset on the receiver, consulted by the dispatch path.this.<bound-method-name> = Xinside a class method body and emit a runtime call that records the override in the instance's overflow property dictionary, plus a runtime arm injs_native_call_method/ class method dispatch that checks the dict first.Environment