feat(runtime): Function.prototype.apply + .call dispatch#970
Merged
Conversation
Add runtime dispatch arms for `fn.call(thisArg, ...args)` and `fn.apply(thisArg, argsArr)` in `js_native_call_method`. Both rebind `IMPLICIT_THIS` and forward through `js_native_call_value`. Apply materialises an `ArrayHeader` into a positional `Vec<f64>` and tolerates null / undefined / non-array argsArr as zero-args. Also skip TypeScript `this:` type-only param annotation at both `lower_decl.rs` FnDecl sites — without this, `function greet(this:…, prefix)` was lowered as a 2-arg function and `.call(obj,'Hi')` bound `prefix=undefined` because the runtime call passed 'Hi' into the synthetic `this` slot. The `expr_function.rs` site already skipped these per #915; the FnDecl sites were missed. Verified against Node byte-for-byte via test-files/test_function_apply_call.ts (7 assertions: plain apply/call, multiple invocations, this-rebind on a method with TS this: annotation, methods on arrow functions). Unblocks ramda's `_curry1`/`_curry2`/`_curry3` helpers; ramda execution still throws during module init (deeper CommonJS / module-resolution issue) — that is the next blocker.
4 tasks
proggeramlug
added a commit
that referenced
this pull request
May 18, 2026
Five coordinated fixes that get every ramda curry/variadic helper past module init after #970 landed Function.prototype.apply/.call: 1. js_object_to_string (the codegen-inlined Object.prototype.toString.call helper) now discriminates primitive tags + GC_TYPE_ARRAY / GC_TYPE_ERROR so ramda's _isString/_isObject/_isRegExp/_isArguments IIFEs see the spec-exact "[object Tag]" strings instead of "[object Object]" across the board. 2. js_native_call_method gains hasOwnProperty / propertyIsEnumerable arms so keys.js / _has.js / _clone.js IIFEs don't throw `value is not a function` on the missing-method fall-through. 3. populate_global_this_builtins now installs `Array.prototype.slice` and `Object.prototype.toString` as real callable closures that read their receiver from IMPLICIT_THIS — covers `var ts = Object.prototype.toString; ts.call(x)` shapes that don't go through the inlined helper. 4. lower_call.rs no longer fast-paths well-known Object.prototype methods through the class dispatch tower for user-class instances, so the new arms in (2) actually fire on AnonShape receivers. 5. emit_string_pool now registers `__perry_wrap_<name>` wrappers' declared param counts in CLOSURE_ARITY_REGISTRY and the closure-property accessor intercepts `name == "length"` to return that arity. Unblocks the converge/juxt/useWith chain that builds curry arities via `pluck('length', fns)` → `reduce(max, 0, …)` → `_arity(N, …)`. Validated against `node --experimental-strip-types` byte-for-byte via new test-files/test_ramda_sum.ts (5 mini-reproducers). `R.add(2,3)` / `R.add(10)(20)` work end-to-end through the direct module path. Full `R.sum([1,2,3,4,5])` still blocks on the transducer prototype-on-callable pattern (XWrap.prototype['@@transducer/step'] = fn) — tracked as the next ramda blocker.
proggeramlug
added a commit
that referenced
this pull request
May 18, 2026
…th (#978) Five coordinated fixes that get every ramda curry/variadic helper past module init after #970 landed Function.prototype.apply/.call: 1. js_object_to_string (the codegen-inlined Object.prototype.toString.call helper) now discriminates primitive tags + GC_TYPE_ARRAY / GC_TYPE_ERROR so ramda's _isString/_isObject/_isRegExp/_isArguments IIFEs see the spec-exact "[object Tag]" strings instead of "[object Object]" across the board. 2. js_native_call_method gains hasOwnProperty / propertyIsEnumerable arms so keys.js / _has.js / _clone.js IIFEs don't throw `value is not a function` on the missing-method fall-through. 3. populate_global_this_builtins now installs `Array.prototype.slice` and `Object.prototype.toString` as real callable closures that read their receiver from IMPLICIT_THIS — covers `var ts = Object.prototype.toString; ts.call(x)` shapes that don't go through the inlined helper. 4. lower_call.rs no longer fast-paths well-known Object.prototype methods through the class dispatch tower for user-class instances, so the new arms in (2) actually fire on AnonShape receivers. 5. emit_string_pool now registers `__perry_wrap_<name>` wrappers' declared param counts in CLOSURE_ARITY_REGISTRY and the closure-property accessor intercepts `name == "length"` to return that arity. Unblocks the converge/juxt/useWith chain that builds curry arities via `pluck('length', fns)` → `reduce(max, 0, …)` → `_arity(N, …)`. Validated against `node --experimental-strip-types` byte-for-byte via new test-files/test_ramda_sum.ts (5 mini-reproducers). `R.add(2,3)` / `R.add(10)(20)` work end-to-end through the direct module path. Full `R.sum([1,2,3,4,5])` still blocks on the transducer prototype-on-callable pattern (XWrap.prototype['@@transducer/step'] = fn) — tracked as the next ramda blocker.
This was referenced May 18, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Function.prototype.call(thisArg, ...args)andFunction.prototype.apply(thisArg, argsArr)dispatch injs_native_call_method(crates/perry-runtime/src/object.rs). Both rebindIMPLICIT_THISand forward throughjs_native_call_value;applymaterialisesArrayHeaderelements into aVec<f64>and tolerates null/undefined/non-array as zero-args.this:type-only parameter annotation at bothFnDeclparameter-lowering sites incrates/perry-hir/src/lower_decl.rs. Without this,function greet(this:…, prefix)was lowered as a 2-arg function and.call(obj,'Hi')boundprefix=undefinedbecause the runtime call passed'Hi'into the syntheticthisslot. Theexpr_function.rssite already skipped these per Native server SIGSEGV atjwt.sign(...)(jsonwebtoken) after resumed async-step body — follow-up to #859 #915.Why
Pre-fix,
add.apply(null, [2,3])andadd.call(null, 2, 3)both returned[object Object](theNULL_OBJECT_BYTESstub) because the closure path injs_native_call_methodreturned the null-object stub when a method name wasn't found in the closure's dynamic-prop side table. This is the core reason ramda fails to bootstrap:_curry1/_curry2/_curry3all wrap their bodies infn.apply(this, arguments), so the very first invocation of any curried ramda export (R.sum,R.add, etc.) returned a stub instead of the dispatch result.Test plan
test-files/test_function_apply_call.ts(7 assertions) — matches Node byte-for-byte (add.apply,add.call, multiple invocations,greet.call({name:'Bob'},'Hi'),greet.apply({name:'Sue'},['Hello']), arrow-fnsq.apply(null,[4]),sq.call(null,5)).test_gap_closures.ts,test_gap_object_methods.ts,test_gap_array_methods.ts,test_gap_async_advanced.ts.Ramda outcome
import * as R from 'ramda'; console.log(R.sum([1,2,3,4,5]))now compiles underperry.compilePackages(2.6 MB binary).TypeError: Cannot read properties of undefined (reading 'call')during ramda's module init — a deeper, separate issue. Likely the CommonJSrequire()resolution returningundefinedfor an internal helper, then ramda's bootstrap calls.call()on the undefined value. This is the next blocker and is not covered by this PR.