Skip to content

fix(runtime): pass the property key to JSON.stringify toJSON, omit undefined members (#5909) - #6686

Merged
proggeramlug merged 2 commits into
PerryTS:mainfrom
proggeramlug:fix/5909-json-tojson-key-omit
Jul 19, 2026
Merged

fix(runtime): pass the property key to JSON.stringify toJSON, omit undefined members (#5909)#6686
proggeramlug merged 2 commits into
PerryTS:mainfrom
proggeramlug:fix/5909-json-tojson-key-omit

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes built-ins/JSON/stringify/value-tojson-arguments.js from the test262 JSON worklist (#5909). SerializeJSONProperty (ECMA-262 §25.5.2.2 step 2) now:

  1. Passes the correct property key to toJSON — the empty String at the root, an object's own key for a member, the stringified index for an array element. Perry previously always passed "".
  2. Applies toJSON to an object member before its key is written, so a member whose toJSON returns undefined is omitted per spec instead of being emitted as "k":null.

Both are the same root: getting Call(toJSON, value, « key ») and the "undefined member is omitted" rule right.

What changed (perry-runtime json/)

  • TO_JSON_KEY thread-local (mod.rs) — an owned Rust String (so it never roots a movable GC pointer) holding the key to hand toJSON. Reset to "" at each top-level JSON.stringify entry; set before each child recursion by the object-property loop, the array-element loops, the shape-template fast path, and the replacer walk (apply_to_json_keyed, which already carried the key but discarded it). The three probes (object_get_to_json / array_get_to_json / bigint_apply_to_json) read it instead of allocating a hardcoded empty string.
  • member_to_json (stringify.rs) — applies a heap-valued member's toJSON up front in the object walk, returning Some(result) only when a toJSON actually ran. The caller omits the member when the result is undefined/function/Symbol, else serializes the result with SUPPRESS_NEXT_TO_JSON armed (so it isn't re-probed). Returns None — "serialize normally" — for plain objects without a toJSON and for non-object/array values, so a plain data-object member keeps the JSON.stringify is too slow then nodejs #6009 fast path (no added probe) and the one-shot guard is never leaked into a sibling member.

Verification

  • test262 built-ins/JSON slice (scripts/test262_subset.py --dir built-ins/JSON), baseline vs. patched built the same way (target/release):
    • value-tojson-arguments : fail → pass
    • zero regressions (every other pass/fail unchanged; 120 → 121 pass)
  • ~30 hand-written stringify cases (nested no-toJSON objects wrapping toJSON objects, toJSON→undefined omission with surrounding keys, replacer + toJSON keys, class instances, Date/Buffer/RegExp members, shape-template arrays) all match Node 26 byte-for-byte.
  • cargo fmt clean on the changed files; scripts/check_file_size.sh passes (stringify.rs 1959 < 2000).

Scope note

The toJSON-key threading also covers the compact replacer walk (via apply_to_json_keyed). The pretty-print serializer (stringify_value_pretty, used when a space argument is given) pre-builds its member list with comma placement keyed on that list's length, so applying the same "omit an undefined-toJSON member" rule there is a separate change; it keeps its prior (pre-existing) behavior in this PR. The target test and every case in the built-ins/JSON slice use the compact form, so this is a follow-up, not a regression.

Code-only per the issue: no version bump, no CHANGELOG/CLAUDE.md edits.

Refs #5909.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Fixed JSON serialization so toJSON callbacks receive the correct property name or array index.
    • Improved serialization of nested objects, arrays, BigInts, and replacer callbacks.
    • Ensured nested JSON.stringify calls use the correct root key and do not inherit stale key values.
    • Corrected handling of values omitted when toJSON returns undefined, functions, or Symbols.

…defined members (PerryTS#5909)

SerializeJSONProperty (ECMA-262 §25.5.2.2 step 2) now hands `toJSON` the
correct property key — the empty String at the root, an object's own key
for a member, the stringified index for an array element — instead of the
hardcoded empty string. The key travels from each serialization loop to the
`object_get_to_json` / `array_get_to_json` / `bigint_apply_to_json` probes
through a new `TO_JSON_KEY` thread-local (an owned Rust String, so it never
roots a movable GC pointer), reset at each top-level entry and threaded
through the object-property, array-element, shape-template, and replacer
(`apply_to_json_keyed`) paths.

A heap-valued object member's `toJSON` is now applied by `member_to_json`
BEFORE the member key is written, so a member whose `toJSON` returns
`undefined`/function/Symbol is omitted per spec instead of emitting
`"k":null`. `member_to_json` returns `Some` only when a `toJSON` actually
ran, so a plain data-object member keeps the PerryTS#6009 fast path and the
one-shot suppression guard is never leaked into a sibling member.

Flips test262 built-ins/JSON/stringify/value-tojson-arguments fail→pass with
zero regressions across the built-ins/JSON slice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@proggeramlug, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 6 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2cf7cea7-9640-491b-a70e-1ab1c670fe25

📥 Commits

Reviewing files that changed from the base of the PR and between 112d0a9 and b8329f4.

📒 Files selected for processing (1)
  • crates/perry-runtime/src/json/stringify.rs
📝 Walkthrough

Walkthrough

JSON serialization now tracks the current property key or array index in thread-local state, passes it to toJSON, applies member-level toJSON results during object serialization, and resets the state for each stringify call.

Changes

JSON toJSON key propagation

Layer / File(s) Summary
Track pending toJSON keys
crates/perry-runtime/src/json/mod.rs, crates/perry-runtime/src/json/stringify_tojson_probe.rs
Adds thread-local key storage, reset/set helpers, NaN-boxed string decoding, and JavaScript key-argument construction.
Apply toJSON during member serialization
crates/perry-runtime/src/json/stringify.rs
Probes object members before emission, handles omitted results and suppression state, and propagates property names and indexes through template and array paths.
Propagate keys across stringify entry points
crates/perry-runtime/src/json/stringify_api.rs, crates/perry-runtime/src/json/replacer.rs, crates/perry-runtime/src/json/stringify_scalars.rs
Resets key state for stringify calls and passes current replacer and BigInt keys into toJSON.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant stringify_object_inner
  participant member_to_json
  participant object_get_to_json
  participant toJSON_callback
  stringify_object_inner->>member_to_json: set property key and probe member
  member_to_json->>object_get_to_json: resolve toJSON with current key
  object_get_to_json->>toJSON_callback: invoke toJSON(key)
  toJSON_callback-->>member_to_json: return replacement or omitted value
  member_to_json-->>stringify_object_inner: serialize replacement or skip member
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the key runtime fix: passing the property key to JSON.stringify toJSON and omitting undefined members.
Description check ✅ Passed The description covers the summary, concrete changes, issue reference, and verification, with the required template sections effectively addressed.
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 unit tests (beta)
  • Create PR with unit tests

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/perry-runtime/src/json/stringify.rs (1)

1258-1296: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Record the property key before primitive BigInt fallback

write_number already routes BigInt through serialize_bigint; the gap is that these object-member and shape-template primitive paths don’t set the pending toJSON key first. A field like { a: 1n } can reach BigInt.prototype.toJSON with the old/root key instead of "a".

🤖 Prompt for 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.

In `@crates/perry-runtime/src/json/stringify.rs` around lines 1258 - 1296, The
inline primitive dispatch for object members must record the current property
key before BigInt reaches write_number and serialize_bigint. Update the
surrounding member/shape-template serialization paths, including the field_bits
dispatch near write_number, to set the pending toJSON key using the existing
key-tracking mechanism before serializing primitive values, while preserving
current handling for null, booleans, strings, pointers, and numbers.
🤖 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.

Outside diff comments:
In `@crates/perry-runtime/src/json/stringify.rs`:
- Around line 1258-1296: The inline primitive dispatch for object members must
record the current property key before BigInt reaches write_number and
serialize_bigint. Update the surrounding member/shape-template serialization
paths, including the field_bits dispatch near write_number, to set the pending
toJSON key using the existing key-tracking mechanism before serializing
primitive values, while preserving current handling for null, booleans, strings,
pointers, and numbers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 975be661-22c3-400a-ada2-40b67cdd0d5f

📥 Commits

Reviewing files that changed from the base of the PR and between 7531311 and 112d0a9.

📒 Files selected for processing (6)
  • crates/perry-runtime/src/json/mod.rs
  • crates/perry-runtime/src/json/replacer.rs
  • crates/perry-runtime/src/json/stringify.rs
  • crates/perry-runtime/src/json/stringify_api.rs
  • crates/perry-runtime/src/json/stringify_scalars.rs
  • crates/perry-runtime/src/json/stringify_tojson_probe.rs

…emplate fields (PerryTS#5909)

A BigInt field reaches `serialize_bigint` → `bigint_apply_to_json` (which
reads the pending `toJSON` key) through `write_number`'s numeric fallback,
which the object-member and shape-template primitive dispatch paths took
without first recording the member's key — so `{ a: 1n }` with a userland
`BigInt.prototype.toJSON` observed the previous/root key instead of "a". The
array-element path already set it. Set the key in the three primitive
fallback branches when the value is a BigInt (zero cost for ordinary
numbers). Addresses CodeRabbit review on PerryTS#6686.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai — good catch. Addressed in b8329f4: the object-member and shape-template primitive dispatch paths now record the member's toJSON key before a BigInt field funnels through write_numberserialize_bigintbigint_apply_to_json, matching the array-element path. So JSON.stringify({ a: 1n }) with a userland BigInt.prototype.toJSON now observes key "a" (verified byte-for-byte against Node 26; the built-ins/JSON slice is still zero-regression with value-tojson-arguments passing). The guard is gated on BIGINT_TAG, so ordinary number fields are unaffected.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@proggeramlug Thanks for the thorough fix and validation. The BIGINT_TAG-gated propagation covers the previously missing object-member and shape-template paths without affecting ordinary numeric serialization.

🐇 ✅


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@proggeramlug
proggeramlug merged commit 1de8687 into PerryTS:main Jul 19, 2026
2 checks passed
@proggeramlug
proggeramlug deleted the fix/5909-json-tojson-key-omit branch July 19, 2026 14:25
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