Skip to content

class instance property reassignment ignored: this.method = X dispatched to prototype, not own property #620

Description

@proggeramlug

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions