Repository navigation
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
Description
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
.applyargs are a literal array (statically expandable).
It rewritesRecv.method.call(thisArg, a, b)→(thisArg).method(a, b).
- the receiver is a statically recognizable
- 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/.bindread as plain values (not immediately
invoked) resolve toundefined(typeofis"undefined"), so feature
detection and theFunction.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.applysemantics).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)
- Runtime dynamic dispatch: a helper that, given any callable value +
thisArg+ an args array, performsFunction.prototype.apply/.call
semantics (reusejs_closure_call_array). Handle user closures, bound
methods, and builtin function-method values (Function.prototype.call/
apply/bind) uniformly. - Value reads: reading
.call/.apply/.bindon a function value
yields a real callable (sotypeof fn.apply === "function"and the value
can be stored / passed / re-bound). - 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). .bindon function values returning a bound closure that itself
supports.call/.apply.
Acceptance criteria
- All three minimal repros above match Node.
- The hasown standalone repro returns
true/falsecorrectly. function-bind,call-bind-apply-helpers,call-bound,hasown,
get-intrinsicinitialize without throwing when compiled (verifiable via the
Express tree in Compile a standard Express app: full blocker inventory (codegen emits undefined @perry_closure_* globals → linked-but-crashing binary) #3527, which currently dies atget-intrinsic's
hasOwn(INTRINSICS, name)).- Byte-identical gap test(s) under
test-files/.
Context / links
- Part of the Compile a standard Express app: full blocker inventory (codegen emits undefined @perry_closure_* globals → linked-but-crashing binary) #3527 "compile a standard Express app" chain. Preceding
fixes already merged/open: fix(compile): honor "*"/"@scope/*" wildcard in perry.compilePackages routing (#3527) #3551 (*wildcard routing), fix(runtime): guard hasOwnProperty against misaligned non-object receivers (#3527) #3733 (init SIGBUS),
fix(runtime,codegen): make Object.hasOwn a real first-class value (#3527) #3751 (Object.hasOwnvalue), fix(cjs): make module/exports reassignable sovar module = Xworks (#3527) #3764 (CJSvar module), fix(hir): inferbool && valueas the value's type, not Boolean (#3527) #3771 (&&type
inference), fix(hir): resolve bareevalas a value instead of ReferenceError (#3527) #3776 (bareevalvalue), fix(regex): escape literal[inside character classes (#3527) #3777 (regex[in char class). - Related prior
.call/.applywork: .apply()/.call() on stdlib namespace methods returns undefined (path.join.apply, etc.) #1722 (stdlib namespace.apply/.call),
Function.prototype.call/.apply on builtin prototype methods returns undefined ([].slice.call(arguments)) — generalize #1722 #1777 (builtin-prototype.call/.applyborrow). This issue is the
generalization to function values with dynamic args, which those did not
cover. - This is the first Compile a standard Express app: full blocker inventory (codegen emits undefined @perry_closure_* globals → linked-but-crashing binary) #3527 blocker that is a genuine multi-part codegen+runtime
effort rather than a single targeted fix.
Activity
fix(runtime): Function.prototype.call/apply/bind on function values +…
proggeramlug commentedon May 31, 2026
Fixed — though note this was already resolved by #3846 (#3716,
Function.prototype.call.binduncurry-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 returnstrue/false. The string-thisArg residual in repros (1)/(2) is a separate boxed-primitiveJSON.stringifybug, filed as #3857.revert(runtime): drop #3856 changes redundant with #3846; keep #3828 …