Problem
call_primitive_closure_value (crates/perry-runtime/src/object/native_call_method.rs ~L317) still wraps the receiver with js_object_coerce for every non-strict callee, including built-in functions. Per spec, built-ins receive the primitive this unwrapped. The closed draft #9800 fixed this in commit 32f150b22 (the native_call_method.rs change plus its test). Its gated A/B showed byte-identical output on 51 cases (thunks unwrap anyway), so this is spec alignment plus a cost bound: no per-call wrapper allocation if a built-in without a native arm is ever reached.
Fix
Re-apply only 32f150b22's change on current main (git show 32f150b22; the branch was perf/builtin-callee-no-toobject), using builtin_closure_length / builtin detection as it exists on main now. Don't carry #9800's older stacked commits.
Verify
- The ported test goes red without the fix (sabotage).
- The gap suite shows no regressions against a pristine
main build (A/B the named tests if anything moves).
Rules for the PR (repo conventions)
- Code + tests + a
changelog.d/<PR>-<slug>.md fragment. No version bump (don't touch [workspace.package].version, the CLAUDE.md version line, or Cargo.lock versions).
- Every new regression test must be sabotage-checked: revert the fix, confirm the test goes red, restore. Say so in the PR body.
perry-runtime tests must run with RUST_TEST_THREADS=1. Build -p perry -p perry-runtime-static -p perry-stdlib-static together (the .a archives come from the -static wrappers).
- Run
scripts/run_lint_gates.sh (or at least cargo fmt --all -- --check, scripts/check_file_size.sh, python3 scripts/raw_handle_debt.py, python3 scripts/addr_class_inventory.py, python3 scripts/gc_runtime_root_holders.py, python3 scripts/gc_rekeyed_key_tables.py).
- Self-contained: no private bundle or special host needed. A normal Linux or macOS dev box is enough.
Filed from the 2026-09 side-table / RSS audit; line numbers are against main e379a7a and may drift.
Problem
call_primitive_closure_value(crates/perry-runtime/src/object/native_call_method.rs~L317) still wraps the receiver withjs_object_coercefor every non-strict callee, including built-in functions. Per spec, built-ins receive the primitivethisunwrapped. The closed draft #9800 fixed this in commit32f150b22(thenative_call_method.rschange plus its test). Its gated A/B showed byte-identical output on 51 cases (thunks unwrap anyway), so this is spec alignment plus a cost bound: no per-call wrapper allocation if a built-in without a native arm is ever reached.Fix
Re-apply only
32f150b22's change on currentmain(git show 32f150b22; the branch wasperf/builtin-callee-no-toobject), usingbuiltin_closure_length/ builtin detection as it exists on main now. Don't carry #9800's older stacked commits.Verify
mainbuild (A/B the named tests if anything moves).Rules for the PR (repo conventions)
changelog.d/<PR>-<slug>.mdfragment. No version bump (don't touch[workspace.package].version, theCLAUDE.mdversion line, orCargo.lockversions).perry-runtimetests must run withRUST_TEST_THREADS=1. Build-p perry -p perry-runtime-static -p perry-stdlib-statictogether (the.aarchives come from the-staticwrappers).scripts/run_lint_gates.sh(or at leastcargo fmt --all -- --check,scripts/check_file_size.sh,python3 scripts/raw_handle_debt.py,python3 scripts/addr_class_inventory.py,python3 scripts/gc_runtime_root_holders.py,python3 scripts/gc_rekeyed_key_tables.py).Filed from the 2026-09 side-table / RSS audit; line numbers are against
maine379a7a and may drift.