Repository navigation
class-method bound closure returns undefined when invoked via local var (blocks hono compose) #628
Description
Summary
const m = router.match; m(...) returns undefined when match is a class method on a class registered in CLASS_VTABLE_REGISTRY. The PropertyGet path emits a bound-method closure via js_class_method_bind (BOUND_METHOD_FUNC_PTR sentinel + (instance, name_ptr, name_len) captures); typeof correctly reports "function"; but invoking it returns undefined.
This is the next blocker for #603 after #619 / #620 / #621 land in v0.5.715. The hono fixture's app.fetch(req2) flow goes through this.match = router.match.bind(router); this.match(method, path) — the override mechanism (#620) now correctly routes to the stored closure, but the closure itself doesn't actually dispatch to Router.match's body.
Minimal repro
class Router2 {
match(method: string, path: string): string {
return `Router2:${method}:${path}`;
}
}
const router = new Router2();
const m = router.match;
console.log("typeof m =", typeof m); // function (correct)
console.log("m direct:", m("GET", "/test")); // expected "Router2:GET:/test", got undefined
const bound = router.match.bind(router);
console.log("typeof bound =", typeof bound); // function
console.log("bound direct:", bound("GET", "/test")); // expected "Router2:GET:/test", got undefinedNode:
typeof m = function
m direct: Router2:GET:/test
typeof bound = function
bound direct: Router2:GET:/test
Perry (post v0.5.715):
typeof m = function
m direct: undefined
typeof bound = function
bound direct: undefined
Trace
router.match(PropertyGet on a known class instance) lowers viacrates/perry-codegen/src/expr.rs:3403+tojs_class_method_bind(router, "match"). Returns a NaN-boxed closure with captures(router_f64, name_ptr_i64, name_len_i64).m("GET", "/test")(Call with LocalGet callee, m is type Any) falls through to the closure-call path atcrates/perry-codegen/src/lower_call.rs:2473+which emitsjs_closure_call2(handle, arg0, arg1).js_closure_call2checksfunc_ptr == BOUND_METHOD_FUNC_PTR→ routes todispatch_bound_method(closure, &[arg0, arg1])(crates/perry-runtime/src/closure.rs:562+).dispatch_bound_methodreads captures and callsjs_native_call_method(namespace_obj, name_ptr, name_len, args, args_len).js_native_call_methodwalksCLASS_VTABLE_REGISTRY[Router2.class_id].methods["match"]→ finds the entry → callscall_vtable_method(entry.func_ptr, this_i64, args_ptr, args_len, entry.param_count).
Somewhere between (4) and the actual perry_method_<modprefix>__Router2__match body, the this / args / return value chain breaks and produces undefined. Likely candidate: call_vtable_method's arg packing diverges from how Router2.match's codegen-emitted body expects to be called (this as f64 NaN-boxed POINTER vs raw i64; param_count mismatch).
Why it matters
- Closes hono routes-with-params end-to-end (hono: app.get("/users/:id") + app.notFound() handlers produce no output on perry-compiled binary #603 acceptance criterion).
- Pattern is universal in JS:
arr.map(obj.method),setTimeout(obj.method, 0),register(obj.method), etc. Currently any of these would silently no-op. - Surfaced while bisecting hono: app.get("/users/:id") + app.notFound() handlers produce no output on perry-compiled binary #603 + my v0.5.715 fixes — once class instance property reassignment ignored: this.method = X dispatched to prototype, not own property #620's own-property override correctly fires for the SmartRouter pattern, it routes to a closure that itself can't dispatch.
Environment
- Perry 0.5.715
- macOS 26.4 / arm64
- Surfaced via
tests/release/packages/hono-basic/after v0.5.715's async-step driver: sync throw from iter.next() bypasses user catch and resolves to undefined #619/class instance property reassignment ignored: this.method = X dispatched to prototype, not own property #620/generator transform: throw closure terminates with done=true after catch instead of continuing past try/catch #621 fixes.
Refs #603, #446 (the original js_class_method_bind machinery).
Activity
proggeramlug commentedon May 9, 2026
Closed in v0.5.721. Single-line cause:
crates/perry-runtime/src/closure.rs::get_valid_func_ptr's macOS code-address range check rejectedBOUND_METHOD_FUNC_PTR(0xBADD_DEAD, ~3.1 GiB) because it sits below the 0x1_0000_0000 lower bound, sojs_closure_callNsaw a null func_ptr and returned TAG_UNDEFINED before itsif func_ptr == BOUND_METHOD_FUNC_PTRarm could fire. Fix: pass the sentinel through unchanged; the CLOSURE_MAGIC + closure-pointer-range checks earlier still gate against garbage. Repro byte-identical to Node post-fix. Test:test-files/test_issue_628.ts.fix: closes #619 #620 #621 — async sync-throw routing + this.method o…
fix: closes #628 — BOUND_METHOD_FUNC_PTR sentinel rejected by macOS c…