Skip to content

fix(runtime): bind this to undefined for sort/reduce callbacks (#11419) - #11514

Merged
proggeramlug merged 2 commits into
mainfrom
claude/eager-fermat-dqtrno
Sep 27, 2026
Merged

proggeramlug merged 2 commits into
mainfrom
claude/eager-fermat-dqtrno

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

sort comparators and reduce / reduceRight callbacks run from inside a method saw that method's receiver as this, not undefined. The runtime called them through plain js_closure_callN calls and never reset the per-thread implicit-this cell. The forEach / map / filter / find* / some / every / flatMap family already bound undefined around their callbacks. This PR does the same at the sort and reduce entry points.

Changes

  • object/this_binding.rs: new ImplicitThisScope::bind_undefined(scope). It is the existing rooted guard, so the caller's receiver is restored from a handle slot on return and on unwind. iter_methods.rs's bind_undefined_this now uses it.
  • Bind undefined at these entry points:
    • arrays: js_array_sort_with_comparator, which also serves toSorted(cmp) and the plain-object/typed-array reroutes; js_array_reduce; js_array_reduce_right
    • typed arrays: js_typed_array_sort_with_comparator, js_typed_array_to_sorted_with_comparator (both including BigInt lanes), js_typed_array_reduce, js_typed_array_reduce_right
    • array-likes (Array.prototype.*.call(obj, …)): object_sort, js_arraylike_reduce, js_arraylike_reduceRight
  • No side tables, no codegen change, no version bump.
  • Plain calls of a function value (const t: any = two; t(1, 2)) already reset this in codegen (runtime/object-model: implement strict and sloppy function this binding #3576). The issue's plain2 line already printed undefined on main. The new test covers arities 0–3 and direct calls so this stays true.
  • The issue's "Related" note (a callee that throws leaves this stale) is already covered by the implicit_this try-savepoint from fix(runtime): root the displaced implicit this in every guard that restores it (#10490) #10564.
  • The structural fix (receiver as a calling-convention parameter) is still the this-as-parameter plan. This PR closes the observable leak for these callbacks.

Related issue

Fixes #11419

Test plan

  • New test-files/test_gap_11419_callback_this_undefined.ts covers array / typed-array (Float64, Int32 toSorted, BigInt64) / array-like sort and reduce, plain calls of arities 0–3, and a class method whose own this must survive the callbacks.

    • On main, with the runtime changes stashed and rebuilt, every surface leaks (leaks sort:object … reduce:object …, dozens of observations).
    • With this change the output matches node --experimental-strip-types byte for byte.
  • The issue's repro prints only plain2 undefined, matching Node.

  • 34 existing gap tests matching *this* / *sort* / *reduce* match Node. test_gap_9445_implicit_this_restore_sweep can't run under local Node 22, but all 34 of its lines report bad=0 under Perry.

  • RUST_TEST_THREADS=1 cargo test --profile perry-dev -p perry-runtime --lib -- sort reduce implicit_this this_binding typed_array: 134 passed.

  • rustfmt --check on the changed files is clean. scripts/check_file_size.sh is OK.

  • Not run locally: the full workspace cargo test. Left to CI.

  • Added a test under test-files/

  • Did NOT bump the workspace version or edit CLAUDE.md / CHANGELOG.md


Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Sort and reduction callbacks for arrays, typed arrays, and array-like values now receive undefined as this, rather than inheriting the enclosing method’s receiver.
    • The enclosing method’s receiver remains available after callbacks complete, including when they throw.

)

Comparators and reduce callbacks were invoked through plain closure calls
without resetting the per-thread implicit-`this` cell, so a callback run
from inside `obj.m()` saw `obj` instead of `undefined`. The forEach/map/
filter family already bound `undefined`; the array, typed-array and
array-like sort/toSorted/reduce/reduceRight entry points now do too, via a
new `ImplicitThisScope::bind_undefined` (rooted, restored on unwind).
@coderabbitai

coderabbitai Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 4869c996-89af-48f6-a721-f6e1178b868d

📥 Commits

Reviewing files that changed from the base of the PR and between d57f513 and 876468f.

📒 Files selected for processing (10)
  • changelog.d/11514-callback-this-undefined.md
  • crates/perry-runtime/src/array/generic.rs
  • crates/perry-runtime/src/array/generic_object.rs
  • crates/perry-runtime/src/array/iter_methods.rs
  • crates/perry-runtime/src/array/reduce_right.rs
  • crates/perry-runtime/src/array/sort.rs
  • crates/perry-runtime/src/object/this_binding.rs
  • crates/perry-runtime/src/typedarray/iterate.rs
  • crates/perry-runtime/src/typedarray/transform.rs
  • test-files/test_gap_11419_callback_this_undefined.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

Array and typed-array sort comparator and reduce callback entry points now bind implicit this to undefined with a scoped runtime binding. The binding restores the prior receiver when the scope ends. A regression test covers these callbacks and related plain calls.

Changes

Callback this binding

Layer / File(s) Summary
Scoped binding and array callbacks
crates/perry-runtime/src/object/this_binding.rs, crates/perry-runtime/src/array/iter_methods.rs, crates/perry-runtime/src/array/reduce_right.rs, crates/perry-runtime/src/array/sort.rs, crates/perry-runtime/src/array/generic.rs, crates/perry-runtime/src/array/generic_object.rs
Added ImplicitThisScope::bind_undefined and applied it to array and array-like sort and reduce callback entry points.
Typed-array callbacks and regression test
crates/perry-runtime/src/typedarray/iterate.rs, crates/perry-runtime/src/typedarray/transform.rs, test-files/test_gap_11419_callback_this_undefined.ts, changelog.d/11514-callback-this-undefined.md
Applied the binding to typed-array reduce and comparator-sort entry points. Added tests for array, typed-array, array-like, and plain-call cases, and added a changelog entry.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Low

Merge Risk: ⚪ Minimal · up to 87646

No actionable issue remains from the reviewed changes; the callback regression is covered by output comparison. Mergeable after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 87646

The change corrects callback receiver behavior without adding a callback route or privilege transition. The receiver-restoration mechanism is established, although failure and accessor behavior are not tested through every changed operation.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The affected exposure is JavaScript callback execution through existing runtime collection operations, rather than a newly reachable service or privileged sink.

Trust Boundaries and Controls

  • observed — The array-like sort entry point validates a caller-provided comparator before routing it. The new guard changes its implicit receiver, while the existing comparator route remains in place.

Resilience and Maintainability Implications

  • observed — Exception handling restores saved implicit receiver state before transferring a throw, covering failure paths that cannot rely on the guard's normal destructor.

Hardening Proposals

  • proposed — Exercise throwing callbacks and reentrant array-like accessors through the changed entry points to verify receiver restoration and accessor identity end to end.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.90% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 21 functions across 9 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements the active coding objective in issue #11419. It adds ImplicitThisScope::bind_undefined and applies it to array, typed-array, and array-like sort, reduce, and reduceRight call…
Out of Scope Changes check ✅ Passed The changed runtime files directly implement issue #11419. The regression test verifies the required callback behavior. The changelog documents the same fix. No unrelated change is established by the …
Title check ✅ Passed The title clearly and concisely identifies the main runtime fix: binding callback this to undefined for sort and reduce operations.
Description check ✅ Passed The description includes the required Summary, Changes, Related issue, and Test plan sections. It explains the affected callback entry points, the regression test, targeted test results, and that the …
Full details: Docstring Coverage

Explanation

Docstring coverage is 61.90% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 21 functions across 9 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug
proggeramlug force-pushed the claude/eager-fermat-dqtrno branch from 876468f to 364b1c9 Compare September 27, 2026 11:53
@proggeramlug
proggeramlug merged commit 6e34e25 into main Sep 27, 2026
25 checks passed
@proggeramlug
proggeramlug deleted the claude/eager-fermat-dqtrno branch September 27, 2026 11:53
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.

this leaks into sort/reduce callbacks and plain calls made from inside a method (per-thread this cell never reset)

2 participants