You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
perry-codegen: cross-module class method dispatch on dynamic-typed receiver — obj.method() undefined when obj is class-instance via assigned property #493
After v0.5.586 closed #485 (cross-module subclass field initializers + arrow class fields installed in parent ctor), `new Hono()` instances correctly carry `get`, `post`, `fetch`, `use`, `on`, `routes`, `_basePath`. The next blocker for hono compile (#421) is that `app.get('/', cb)` doesn't actually register a route — `app.routes.length` stays 0 even though the arrow-field method gets called.
// hono.js (subclass)constructor(options={}){super(options);this.router=options.router??newSmartRouter({...});}// hono-base.js (parent — runs in cross-module ctor as of v0.5.586)this[method]=(args1, ...args)=>{if(typeofargs1==="string")this.#path =args1;elsethis.#addRoute(method,this.#path,args1);args.forEach((handler)=>this.#addRoute(method,this.#path,handler));};
#addRoute(method,path,handler){// ...this.router.add(method,path,[handler,r]);// ← undefined callthis.routes.push(r);}
`SmartRouter` is yet another class with a regular `add(method, path, handler) { ... }` declaration (not an arrow class field). Perry's class-method dispatch keys on the receiver's known class — but `app.router` was assigned dynamically inside hono.js's constructor (`this.router = options.router ?? new SmartRouter(...)`), and the static type of `app.router` is unknown at the call site in hono-base.js (where `#addRoute` lives). Without static type info, perry's `PropertyGet` falls through to dynamic property lookup, which doesn't find the prototype method and returns `undefined`.
Same root cause as #91 but in a different shape: instead of `map.get(key).method()`, this is `this.fieldAssignedToInstance.method()` where the assignment crosses module boundaries.
Likely fix
Two approaches:
Codegen-time class-instance tracking — when `this.x = new SmartRouter(...)` is lowered, record `(this_class, x) → SmartRouter` in the class's field type map. The cross-module `obj.x.method()` site then sees `this.router` typed as `SmartRouter` and dispatches to `SmartRouter::add`.
Runtime dispatch fallback for prototype methods — when `obj.method` returns undefined and `obj` has a class id stamped in its header, walk the class's method registry by name. Slower but works without compile-time type info.
`SmartRouter` itself probably has further dispatch issues (it has its own `add` and `match` methods, also a #routes private field, and the constructor takes `{routers: [...]}` with destructuring — verified lowering of those is needed too).
Why it matters
Blocks #421 (hono compile end-to-end). Probably also blocks #420 (drizzle's `db.query.users.findMany()` — same dynamic-receiver method dispatch shape).
Fixed in v0.5.589. Three coordinated changes: (1) new runtime CLOSURE_REST_REGISTRY side-table mapping func_ptr → fixed_arity, populated at module init via js_register_closure_rest, consulted in js_closure_callN to bundle trailing args into a rest array via dispatch_rest_bundled; (2) codegen emits the registration calls in emit_string_pool for every closure body whose HIR has is_rest; (3) the static-bundling path in lower_call.rs::LocalGet was removed to avoid double-bundling. Plus js_native_call_method's array dispatch tower learned forEach. Parity: 209 pass / 3 known fails (was 204 / 8). End-to-end: app.get('/', cb) correctly increments app.routes.length to 1 — only the path field reads as [object Object] instead of / is left, which is a separate followup. Note: this fix also unblocks part of #421 (hono compile end-to-end) by reducing the gap to one final mergePath/Object.assign issue. The original #493 root cause analysis (cross-module class method dispatch on dynamic receiver) was wrong — the actual culprit was the rest-param-bundling-on-dynamic-dispatch path.
Activity
proggeramlug commentedon May 6, 2026
Fixed in v0.5.589. Three coordinated changes: (1) new runtime
CLOSURE_REST_REGISTRYside-table mapping func_ptr → fixed_arity, populated at module init viajs_register_closure_rest, consulted injs_closure_callNto bundle trailing args into a rest array viadispatch_rest_bundled; (2) codegen emits the registration calls inemit_string_poolfor every closure body whose HIR hasis_rest; (3) the static-bundling path inlower_call.rs::LocalGetwas removed to avoid double-bundling. Plusjs_native_call_method's array dispatch tower learnedforEach. Parity: 209 pass / 3 known fails (was 204 / 8). End-to-end:app.get('/', cb)correctly incrementsapp.routes.lengthto 1 — only thepathfield reads as[object Object]instead of/is left, which is a separate followup. Note: this fix also unblocks part of #421 (hono compile end-to-end) by reducing the gap to one final mergePath/Object.assign issue. The original #493 root cause analysis (cross-module class method dispatch on dynamic receiver) was wrong — the actual culprit was the rest-param-bundling-on-dynamic-dispatch path.Merge origin/main, bump 0.5.589 → 0.5.590, add tray docs
fix(codegen+compile): closure-bound exported var calls fetch via gett…
fix(runtime+stdlib): closes #421 — hono compiles end-to-end (v0.5.613)