Skip to content

Function.prototype.call/apply/bind don't work on function values (dynamic dispatch + value reads) — blocks function-bind/call-bind/hasown/get-intrinsic (#3527) #3828

Closed

Description

@proggeramlug

Summary

Function.prototype.call / .apply / .bind do not work as first-class inherited methods on function values. Perry handles the static, directly-written shapes (Array.prototype.slice.call(arguments), fn.apply(thisArg, [literal])) via compile-time AST rewrites, but a function value flowing through a variable — especially the builtin function-method values Function.prototype.call/apply/bind themselves — has no working path, as either a value read or a dynamic dispatch.

This is the central blocker for the entire intrinsics/call-binding utility layer (function-bind, call-bind-apply-helpers, call-bound, hasown, get-intrinsic), which sits under a huge fraction of npm — including Express (#3527).

Minimal reproductions

function f(this: any, a: any, b: any) { return [this, a, b]; }

// (1) `.call` / `.apply` read as a VALUE on a function
typeof (f as any).apply   // Perry: "undefined"   Node: "function"
typeof (f as any).call    // Perry: "undefined"   Node: "function"

// (2) direct .apply on a user-function value with a literal array — WORKS today
(f as any).apply("T", [1, 2])           // Perry: ["T",1,2]  ✅

// (3) builtin function-method value held in a var, then .apply on it
const c: any = Function.prototype.call;
typeof c.apply                          // Perry: "undefined"   Node: "function"
c.apply(f, ["X", 9, 8])                 // Perry: undefined     Node: ["X",9,8]

Real-world failure chain (how Express hits it)

hasown/index.js:

var call = Function.prototype.call;
var $hasOwn = Object.prototype.hasOwnProperty;
var bind = require('function-bind');     // plain-closure bind
module.exports = bind.call(call, $hasOwn);   // → should be hasOwn(o, p)

bind.call(call, $hasOwn) returns a closure whose body does
target.apply(that, args.concat(...)), where target === Function.prototype.call.
That inner target.apply(...) throws TypeError: apply is not a function
(captured function-method value, dynamic args). So hasown's export ends up
non-callable, and the first consumer — get-intrinsic's
hasOwn(INTRINSICS, name) — throws TypeError: value is not a function at
module init, aborting the whole program.

Standalone repro of the hasown pattern:

function bind(this: any, that: any) {
  var target = this;
  var args = Array.prototype.slice.call(arguments, 1);
  return function (this: any) {
    return target.apply(that, args.concat(Array.prototype.slice.call(arguments)));
  };
}
var call = Function.prototype.call;
var $hasOwn = Object.prototype.hasOwnProperty;
var hasOwn: any = (bind as any).call(call, $hasOwn);
var o: any = { a: 1 };
hasOwn(o, "a");  // Node: true ; Perry: throws "apply is not a function"

Root cause

  • The existing handling, try_builtin_prototype_method_apply_call
    (crates/perry-hir/src/lower/expr_call/intrinsics.rs), is a compile-time
    AST rewrite
    . It only fires when:
    • the receiver is a statically recognizable X.prototype.method /
      array/string literal (is_builtin_prototype_receiver), and
    • the .apply args are a literal array (statically expandable).
      It rewrites Recv.method.call(thisArg, a, b) → (thisArg).method(a, b).
  • It cannot handle a function value in a variable, a captured function,
    or non-literal args (args.concat(...)) — exactly the function-bind /
    call-bind / hasown shapes.
  • .call / .apply / .bind read as plain values (not immediately
    invoked) resolve to undefined (typeof is "undefined"), so feature
    detection and the Function.prototype.call.bind(...) idiom break.

What already exists (so this is wiring, not a new engine)

  • js_closure_call_array(closure, args_array) — invoke a closure with args
    pulled from an array (this is .apply semantics).
  • js_function_bind(...) — bind support.

The runtime primitives are present; what's missing is the method-dispatch
wiring
that routes .call / .apply / .bind on an arbitrary function value
(user closure OR builtin function-method value) to them, both as a callable
value and as a dynamic call with runtime args.

Proposed scope (suggested phasing)

  1. Runtime dynamic dispatch: a helper that, given any callable value +
    thisArg + an args array, performs Function.prototype.apply / .call
    semantics (reuse js_closure_call_array). Handle user closures, bound
    methods, and builtin function-method values (Function.prototype.call/
    apply/bind) uniformly.
  2. Value reads: reading .call / .apply / .bind on a function value
    yields a real callable (so typeof fn.apply === "function" and the value
    can be stored / passed / re-bound).
  3. Codegen: route <fnValueExpr>.apply(thisArg, argsExpr) /
    .call(thisArg, ...args) to the runtime dispatch when the receiver isn't a
    statically rewritable shape (keep the existing fast AST rewrite for the
    static case).
  4. .bind on function values returning a bound closure that itself
    supports .call/.apply.

Acceptance criteria

Context / links

Activity

  1. proggeramlug commented on May 31, 2026

    @proggeramlug
    ContributorAuthor

    Fixed — though note this was already resolved by #3846 (#3716, Function.prototype.call.bind uncurry-this) which landed shortly before #3856. #3856 added an equivalent runtime implementation that ended up duplicating #3846's; #3865 reverts that redundant runtime delta (keeping the function-bind regression test) so #3846 is the single canonical implementation. Verified: the hasown standalone repro returns true/false. The string-thisArg residual in repros (1)/(2) is a separate boxed-primitive JSON.stringify bug, filed as #3857.

  2. added a commit that references this issue on May 31, 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