Skip to content

fix(runtime): improve test262 built-ins/Object parity (#5588)#5763

Merged
proggeramlug merged 2 commits into
mainfrom
fix/test262-object-parity-5588
Jun 28, 2026
Merged

fix(runtime): improve test262 built-ins/Object parity (#5588)#5763
proggeramlug merged 2 commits into
mainfrom
fix/test262-object-parity-5588

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Closes a cluster of test262 built-ins/Object failures tracked in #5588.

Root causes and fixes

1. Object.keys/values/entries on functions — length/name missing when made enumerable (enumeration.rs)

closure_dynamic_enumerable_props iterated only closure_dynamic_props_snapshot, which holds user-added dynamic properties. The built-in function slots length and name live outside that snapshot (they're computed on the fly from the closure registry), so a Object.defineProperty(fn, "length", { enumerable: true }) call correctly recorded the descriptor in the side table but the key never appeared in Object.keys(fn).

Fix: Before iterating user-added props, check the descriptor side table for length and name. If either is explicitly enumerable, compute its value through the same cascade as the direct field-read path (dynamic-prop override → bound arity → builtin length → closure arity) and prepend it. length/name are then skipped in the user-props pass to avoid double-counting.

2. Object.prototype.toLocaleString with a patched primitive toString (object_proto.rs)

The spec (20.1.3.6) says step 1 is Let O = this value (not ToObject) and step 2 is Return ? Invoke(O, "toString"). For toLocaleString.call(true) with a user-patched Boolean.prototype.toString = function() { return typeof this; } (strict), this must be the raw primitive true; typeof this must return "boolean". The old code called js_native_call_method which hard-codes the native result without checking prototype patches.

Fix: Before falling back to the native path, call builtin_proto_user_method to detect a user patch on the primitive's prototype (Boolean/Number/BigInt/String/Symbol). If found, dispatch via call_primitive_closure_value, which already implements OrdinaryCallBindThis (strict callee gets raw primitive; sloppy callee gets the boxed wrapper).

3. Prototype cycle detection in Object.setPrototypeOf (define_properties.rs)

OrdinarySetPrototypeOf step 7 requires walking the prototype chain of the proposed new prototype and throwing TypeError: Cyclic __proto__ value if any ancestor equals the target object. This check was absent, so Object.setPrototypeOf(a, b); Object.setPrototypeOf(b, a) silently created a cycle instead of throwing on the second call.

Fix: Walk the chain of the proposed prototype (bounded by null or a stable fixed-point); throw on the first ancestor that equals the target.

Before / after (counts estimated from issue #5588 analysis)

Category Before After
keys/entries function property ordering fail pass
toLocaleString with patched primitive toString fail pass
setPrototypeOf cycle detection fail pass

Validation

  • cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static — clean
  • cargo fmt --all -- --check — clean
  • bash scripts/check_file_size.sh — clean (all files under 2000 lines)

No version bump, no CHANGELOG entry — maintainer folds those at merge per contributor guidelines.


Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Fixed dynamic property enumeration for closure instances so built-in length/name appear in spec order, respect deletions/enumerability rules, and aren’t double-counted.
    • Improved toLocaleString for primitive values to align with Object.prototype.toLocaleString semantics and to honor user-customized toString on relevant built-in prototypes.
    • Prevented cyclic prototype assignments by detecting and rejecting __proto__ changes that would create inheritance loops.

Three targeted fixes that close a cluster of test262 built-ins/Object
failures found in issue #5588:

1. `closure_dynamic_enumerable_props` (enumeration.rs): `Object.keys/
   values/entries` on a function now includes `length` and `name` when
   they have been explicitly made enumerable via `Object.defineProperty`.
   Previously those built-in function properties were only surfaced via
   the direct field-read path, so a descriptor-only redefinition (no
   `value` written to the dynamic-props side table) was silently dropped.
   Fix: check the descriptor side table for `length`/`name` first; if
   enumerable, compute the value from the same cascade as the field-read
   path (dynamic-prop override → bound arity → builtin length → closure
   arity) and prepend them before user-added dynamic props.

2. `js_object_default_to_locale_string` (object_proto.rs): spec step 2
   of `Object.prototype.toLocaleString` is `Invoke(O, "toString")` where
   O is the raw `this` (primitive, not ToObject'd). A strict callee on
   the patched prototype must receive the original primitive as `this`
   (`typeof this === "boolean"` for `toLocaleString.call(true)` after
   patching `Boolean.prototype.toString`). The old code called the
   generic `js_native_call_method` which hard-codes the native toString
   result. Fix: check `builtin_proto_user_method` for a user patch first;
   if present, dispatch via `call_primitive_closure_value` which correctly
   applies strict/non-strict `this` binding.

3. `js_object_set_prototype_of` (define_properties.rs): add the
   OrdinarySetPrototypeOf step-7 prototype-cycle check. Walking the chain
   of the proposed new prototype and comparing each ancestor against the
   target object throws `TypeError: Cyclic __proto__ value` when a cycle
   would be created.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWENcSJa3zenoQyBXXCF5j
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bbb88136-ef9f-4f2e-a13e-4a00858dba64

📥 Commits

Reviewing files that changed from the base of the PR and between 0c35ad0 and 1724d9a.

📒 Files selected for processing (3)
  • crates/perry-runtime/src/object/field_get_set/enumeration.rs
  • crates/perry-runtime/src/object/native_call_method/object_proto.rs
  • crates/perry-runtime/src/object/object_ops/define_properties.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/perry-runtime/src/object/native_call_method/object_proto.rs
  • crates/perry-runtime/src/object/object_ops/define_properties.rs
  • crates/perry-runtime/src/object/field_get_set/enumeration.rs

📝 Walkthrough

Walkthrough

Three spec-compliance fixes update JS object enumeration, primitive toLocaleString dispatch, and Object.setPrototypeOf cycle handling.

Changes

JS Object Model Spec Compliance

Layer / File(s) Summary
Closure built-in prop enumeration ordering and deduplication
crates/perry-runtime/src/object/field_get_set/enumeration.rs
closure_dynamic_enumerable_props now emits length and name first (with deletion and enumerability checks and side-table/fallback value resolution), then user-added dynamic props with those keys excluded, then accessor-descriptor keys also filtered to skip length/name.
toLocaleString patched-prototype dispatch for primitives
crates/perry-runtime/src/object/native_call_method/object_proto.rs
Non-pointer receivers in js_object_default_to_locale_string now resolve the primitive's builtin name (Boolean/BigInt/String/Symbol), attempt dispatch through a user-patched prototype toString via call_primitive_closure_value, and fall back to js_native_call_method only when no patch is found.
Cyclic __proto__ detection in setPrototypeOf
crates/perry-runtime/src/object/object_ops/define_properties.rs
js_object_set_prototype_of walks the proposed prototype chain and throws TypeError: Cyclic __proto__ value if any ancestor equals the target object, using an internal TAG_NULL_U64 sentinel to detect chain termination.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PerryTS/perry#5396: Also updates js_object_set_prototype_of to enforce Object.setPrototypeOf invariants, with different prototype-chain validation logic.

Poem

🐰 Hop, hop, the spec is neat,
length and name stay in their seat.
toString paths now take the right lane,
And proto loops won’t spin again.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main runtime/Object parity fixes and the issue reference.
Description check ✅ Passed The description covers the main fixes, links #5588, and includes validation, though it doesn't strictly follow every template heading.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/test262-object-parity-5588

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-runtime/src/object/field_get_set/enumeration.rs`:
- Around line 269-272: The override lookup in enumeration.rs is using
closure_get_dynamic_prop with TAG_UNDEFINED as a sentinel, which conflates a
missing override with an explicit undefined value. Update the enumeration logic
to separate “property exists” detection from reading the override value in the
Object.values/Object.entries path, and use the computed length/name fallback
only when the override is truly absent. Also adjust the Object.keys path so it
does not call [[Get]] via closure_get_dynamic_prop, preventing enumerable
length/name accessors from being invoked while only collecting keys.

In `@crates/perry-runtime/src/object/native_call_method/object_proto.rs`:
- Around line 97-115: The Symbol primitive branch in object_proto.rs is
currently unreachable because the check uses js_is_symbol(receiver) inside the
!jsval.is_pointer() path, so Symbol.prototype.toString still won’t dispatch
through the primitive path. Update the primitive-type detection in this
native_call_method::object_proto logic so Symbols are identified from the same
non-pointer receiver value as the other primitives, and ensure
call_primitive_closure_value can be reached for Symbol just like
Boolean/BigInt/String.

In `@crates/perry-runtime/src/object/object_ops/define_properties.rs`:
- Around line 205-215: The prototype chain walk in define_properties.rs only
checks for null and self-edges, so it can hang on pre-existing multi-node
cycles. Update the loop in the Object.setPrototypeOf path to detect any repeated
prototype node, either by tracking visited bits or using tortoise/hare cycle
detection in the prototype traversal. When a cycle is found, throw the same
TypeError used for cyclic __proto__ values via throw_object_type_error, and keep
the existing behavior for null termination and direct self-cycles.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b0fc7e91-9ed9-4d5a-b677-cd99c548484d

📥 Commits

Reviewing files that changed from the base of the PR and between 69eaa23 and 0c35ad0.

📒 Files selected for processing (3)
  • crates/perry-runtime/src/object/field_get_set/enumeration.rs
  • crates/perry-runtime/src/object/native_call_method/object_proto.rs
  • crates/perry-runtime/src/object/object_ops/define_properties.rs

Comment thread crates/perry-runtime/src/object/field_get_set/enumeration.rs Outdated
Comment thread crates/perry-runtime/src/object/native_call_method/object_proto.rs Outdated
Comment thread crates/perry-runtime/src/object/object_ops/define_properties.rs Outdated
Three correctness fixes raised in the code review:

1. enumeration.rs: use `closure_has_own_dynamic_prop` to separate presence
   from value in `closure_dynamic_enumerable_props`. The previous
   `closure_get_dynamic_prop` sentinel (TAG_UNDEFINED) conflated "no dynamic
   override" with "dynamic override whose value is undefined", and also
   invoked getters in a keys-only path.

2. object_proto.rs: symbols are POINTER-tagged, so the `!jsval.is_pointer()`
   guard made the `js_is_symbol` branch inside it dead code. Compute
   `is_symbol` before the guard and extend the condition to
   `!jsval.is_pointer() || is_symbol` so Symbol.prototype.toString dispatch
   is reachable.

3. define_properties.rs: replace the single-step self-edge cycle check in
   `js_object_set_prototype_of` with Floyd's tortoise-and-hare algorithm so
   a pre-existing multi-node cycle (A→B→A) terminates the walk instead of
   looping forever.
@proggeramlug proggeramlug merged commit 1f4a2c5 into main Jun 28, 2026
15 checks passed
@proggeramlug proggeramlug deleted the fix/test262-object-parity-5588 branch June 28, 2026 18:10
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.

2 participants