Skip to content

perry-codegen: cross-module class method dispatch loses this binding (app.router.match() ends up with this = number) #519

Description

@proggeramlug

Summary

The current state of hono compile end-to-end (post v0.5.594):

  • Construction works.
  • Route registration works (`app.routes.length === 2`).
  • `app.fetch(req)` triggers SmartRouter.match → RegExpRouter.match → `this.buildAllMatchers()` — but inside `match`, `this` is a number, so `this.buildAllMatchers` is undefined and the call throws.

Repro

import { Hono } from 'hono';
const app = new Hono();
app.get('/', (c: any) => 'hi');
console.log('typeof router.match:', typeof app.router.match);   // undefined  ← bug A
const m = app.router.match('GET', '/');                          // ↓
//   TypeError: (number).buildAllMatchers is not a function     ← bug B

Why

Two related issues:

Bug A — app.router.match reads as undefined. perry's PropertyGet on a class instance only sees own-fields, not prototype methods. SmartRouter has `match()` as a regular class method (lives in the vtable, not on the instance), so `app.router.match` reads undefined. The CALL form `app.router.match('GET', '/')` should still dispatch via the vtable correctly — and it does — but the read form returns undefined.

Same shape as #446, where calling worked but `typeof obj.method` read undefined. That issue's fix landed v0.5.503 but only for direct-instance method access; the cross-module field-stored-instance case (`this.router` set in a constructor body, where the field's static type is unknown) doesn't take the same path.

Bug B — this.buildAllMatchers invokes on a number. When the runtime tower's vtable dispatch fires for SmartRouter.match, it calls `router.match(method, path)` inside the method body — but `this` in that nested call resolves to a number value (probably the integer 0 or 1, the class id of one of the routers in the array). Inside RegExpRouter.match, `this.buildAllMatchers()` is called on `this` which is now a number — TypeError.

The runtime's vtable dispatch (`call_vtable_method`) passes the receiver as `this_i64` (the instance pointer). For nested method calls inside the body, `this` is read from the function's `this_arg` — perry's codegen for cross-module methods may be passing the wrong value.

Likely fix

Both bugs share a root cause: cross-module class-method dispatch when the receiver is dynamically-typed (`Record<string, any>` field, `this.x = new Foo()` in another module's constructor).

Bug A's fix: have PropertyGet on a class-typed instance auto-bind a closure for prototype methods (similar to v0.5.503's `js_class_method_bind`, but for cross-module receivers stored in unknown-typed fields).

Bug B's fix: ensure cross-module method dispatch passes the correct `this` value at every nested call site. The vtable lookup gets the right func_ptr, but the call must have the receiver's pointer (not the array's first element interpreted as a number) as `this_arg`.

Why it matters

Last blocker between v0.5.594 and a 30-line hono acceptance test (#486). Once `app.fetch()` runs to completion, the rest of the dispatch chain (`compose()` / `Context.text()` / `new Response`) likely has its own gaps but each is incrementally tractable.

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