Skip to content

feat(runtime): Function.prototype.apply + .call dispatch#970

Merged
proggeramlug merged 1 commit into
mainfrom
fix/function-apply-call-dispatch
May 17, 2026
Merged

feat(runtime): Function.prototype.apply + .call dispatch#970
proggeramlug merged 1 commit into
mainfrom
fix/function-apply-call-dispatch

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Summary

  • Add Function.prototype.call(thisArg, ...args) and Function.prototype.apply(thisArg, argsArr) dispatch in js_native_call_method (crates/perry-runtime/src/object.rs). Both rebind IMPLICIT_THIS and forward through js_native_call_value; apply materialises ArrayHeader elements into a Vec<f64> and tolerates null/undefined/non-array as zero-args.
  • Skip TypeScript this: type-only parameter annotation at both FnDecl parameter-lowering sites in crates/perry-hir/src/lower_decl.rs. 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 Native server SIGSEGV at jwt.sign(...) (jsonwebtoken) after resumed async-step body — follow-up to #859 #915.

Why

Pre-fix, add.apply(null, [2,3]) and add.call(null, 2, 3) both returned [object Object] (the NULL_OBJECT_BYTES stub) because the closure path in js_native_call_method returned 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 / _curry3 all wrap their bodies in fn.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-fn sq.apply(null,[4]), sq.call(null,5)).
  • No regression on 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 under perry.compilePackages (2.6 MB binary).
  • Execution still throws TypeError: Cannot read properties of undefined (reading 'call') during ramda's module init — a deeper, separate issue. Likely the CommonJS require() resolution returning undefined for 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.

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.
@proggeramlug proggeramlug merged commit 43cd9ff into main May 17, 2026
5 of 9 checks passed
@proggeramlug proggeramlug deleted the fix/function-apply-call-dispatch branch May 17, 2026 23:50
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant