Skip to content

class-method bound closure returns undefined when invoked via local var (blocks hono compose) #628

Closed

Description

@proggeramlug

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 undefined

Node:

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

  1. router.match (PropertyGet on a known class instance) lowers via crates/perry-codegen/src/expr.rs:3403+ to js_class_method_bind(router, "match"). Returns a NaN-boxed closure with captures (router_f64, name_ptr_i64, name_len_i64).
  2. m("GET", "/test") (Call with LocalGet callee, m is type Any) falls through to the closure-call path at crates/perry-codegen/src/lower_call.rs:2473+ which emits js_closure_call2(handle, arg0, arg1).
  3. js_closure_call2 checks func_ptr == BOUND_METHOD_FUNC_PTR → routes to dispatch_bound_method(closure, &[arg0, arg1]) (crates/perry-runtime/src/closure.rs:562+).
  4. dispatch_bound_method reads captures and calls js_native_call_method(namespace_obj, name_ptr, name_len, args, args_len).
  5. js_native_call_method walks CLASS_VTABLE_REGISTRY[Router2.class_id].methods["match"] → finds the entry → calls call_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

Environment

Refs #603, #446 (the original js_class_method_bind machinery).

Activity

  1. proggeramlug commented on May 9, 2026

    @proggeramlug
    ContributorAuthor

    Closed in v0.5.721. Single-line cause: crates/perry-runtime/src/closure.rs::get_valid_func_ptr's macOS code-address range check rejected BOUND_METHOD_FUNC_PTR (0xBADD_DEAD, ~3.1 GiB) because it sits below the 0x1_0000_0000 lower bound, so js_closure_callN saw a null func_ptr and returned TAG_UNDEFINED before its if func_ptr == BOUND_METHOD_FUNC_PTR arm 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.

  2. added 2 commits that reference this issue on May 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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