Skip to content

fix(runtime): exclude class objects from the store-plan cache (#6595)#6596

Merged
proggeramlug merged 2 commits into
PerryTS:mainfrom
proggeramlug:fix/6595-store-plan-capture-class
Jul 18, 2026
Merged

fix(runtime): exclude class objects from the store-plan cache (#6595)#6596
proggeramlug merged 2 commits into
PerryTS:mainfrom
proggeramlug:fix/6595-store-plan-capture-class

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Fixes #6595.

Root cause

#6541 consults and records the shared store-plan cache in ordinary_set_with_receiver before the store flows through target_setjs_object_set_field_by_name. That ordering is the regression: the very store being vetted arrives at js_object_set_field_by_name with its own (class_id, key) plan freshly recorded, so it takes the top fast lane — which performs a raw slot store and skips every in-body write completion, including the #6530/#6537 mirror_class_object_static_write hook that syncs per-evaluation class objects into CLASS_DYNAMIC_PROPS.

For a class object (what a capture-carrying class materializes as — bundled zod's entire ZodType family) the failure needs one more ingredient, which is why the single-class #6537 gap test stayed green: the lane's raw store also needs a shape-transition cache hit, and class objects repeat identical key-append sequences. So the first class of a shape falls through to the mirroring path (simple cases pass), and from the second same-shaped class on (zodlocal.cjs has 32 ZodX.create = ... writes) the static lands only on the class object's own fields. js_class_static_method_call through the INT32 ClassRef then misses and returns the receiver — .optional() evaluates to the class itself, this._def is undefined, and every downstream read blows up (errorMap / _getType / description), exactly the #6530 symptom set.

Confirmed with a temporary diagnostic on the bundled-zod repro — per class, in order:

ordinary_set class-object cid=N  plan check: miss   ← #6541 records after the walk
top-lane     class-object cid=N key=create plan_hit=true   ← same store, mirror skipped
[perry dispatch-miss] static-member-call: class-ref `ZodOptional` (id 133)."create"
  did not resolve → returning the receiver (class ref)

Fix

Class objects (object_type == OBJECT_TYPE_CLASS) are now ineligible for the store-plan cache at all four gates:

  • proxy.rs ordinary_set_with_receiver fast path (plan_eligible — blocks both check and record),
  • field_set_by_name.rs top fast lane gate,
  • field_set_by_name.rs in-body plan_eligible (check),
  • field_set_by_name.rs in-body record site.

Their writes always take the mirroring completions again (statics writes are cold — no measurable cost), and this also closes the latent cross-poisoning both ways: a class object shares its template cid with its instances, so a plan learned on one chain could falsely certify a store on the other (the interception verdict is per-chain, and the two chains differ). Instances (OBJECT_TYPE_REGULAR) keep the #6539/#6541 fast lanes unchanged.

Regression test

test_gap_6595_repeated_capture_class_statics.tstwo same-shaped capture-carrying classes with post-class arrow statics, dispatched from a sibling method through the ClassRef world. Two subtleties baked into the test (both learned the hard way):

  • every class must capture an outer local, or it stays an INT32 ClassRef and never touches the heap-class-object store path;
  • one class cannot catch the bug — the first class of a shape misses the transition cache and still mirrors.

Fails on current main (null typeof: function, _def set: false, then the errorMap TypeError); byte-identical to node --experimental-strip-types with the fix.

Validation

Summary by CodeRabbit

  • Bug Fixes

    • Fixed incorrect handling of static properties on dynamically created classes.
    • Ensured class-based writes consistently preserve static behavior across repeated, similarly shaped instances.
    • Improved chained optional and nullable operations involving class static properties, including correct parsing, type checks, and unwrapping results.
  • Tests

    • Added regression coverage for captured classes, inherited static behavior, and repeated property access.

Ralph Küpper added 2 commits July 18, 2026 16:21
…S#6595)

The PerryTS#6541 receiver-based [[Set]] records a store-plan verdict BEFORE the
store flows through target_set -> js_object_set_field_by_name, so the
very same write hits the mirror-free top fast lane there. For a
per-evaluation CLASS OBJECT (what a capture-carrying class materializes
as — bundled zod's entire ZodType family) that lane skips the PerryTS#6530
mirror_class_object_static_write hook: from the second same-shaped class
on, the shape-transition cache also hits, the post-class static
(ZodX.create = ...) lands only on the class object's own fields, and
ClassRef-world static dispatch misses — returning the receiver, so
.optional() yielded the class and every ._def read blew up
(errorMap/_getType/description).

Class objects are now ineligible for the store-plan cache at all four
gates (ordinary_set fast path, top fast lane, in-body check + record):
their writes always take the mirroring completions, and their template
cid — shared with their instances — can no longer conflate two different
prototype chains in one cache line. Statics writes are cold; instances
(OBJECT_TYPE_REGULAR) keep the fast lanes unchanged.
@coderabbitai

coderabbitai Bot commented Jul 18, 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: fd184e8f-2ce7-4d66-9efb-83c05fb5db32

📥 Commits

Reviewing files that changed from the base of the PR and between 03f219e and a9e2a82.

📒 Files selected for processing (3)
  • crates/perry-runtime/src/object/field_set_by_name.rs
  • crates/perry-runtime/src/proxy.rs
  • test-files/test_gap_6595_repeated_capture_class_statics.ts

📝 Walkthrough

Walkthrough

Runtime setter fast paths and store-plan caching now exclude class objects, while a regression test validates repeated capture-carrying class static writes across optional, nullable, and chained calls.

Changes

Class-object write safety

Layer / File(s) Summary
Runtime write eligibility gates
crates/perry-runtime/src/object/field_set_by_name.rs, crates/perry-runtime/src/proxy.rs
Fast-lane execution, store-plan eligibility, and store-plan recording now require regular-object receivers.
Capture-carrying class static regression
test-files/test_gap_6595_repeated_capture_class_statics.ts
Adds repeated same-shaped subclass scenarios and checks optional, nullable, chained dispatch, parsing, labels, and unwrapping.

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

Possibly related PRs

Suggested reviewers: thehypnoo, andrewtdiz

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: excluding class objects from the store-plan cache for #6595.
Description check ✅ Passed It covers the issue, root cause, fix, test file, and validation, though it doesn't use the repo's exact template headings.
Linked Issues check ✅ Passed The changes match #6595 by excluding class objects from all store-plan cache gates and adding the targeted regression test.
Out of Scope Changes check ✅ Passed All code changes support the #6595 fix or its regression test; no unrelated edits are shown.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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.

@proggeramlug
proggeramlug merged commit b389a58 into PerryTS:main Jul 18, 2026
23 of 25 checks passed
@proggeramlug
proggeramlug deleted the fix/6595-store-plan-capture-class branch July 18, 2026 14:43
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.

regression: #6541 store-plan cache sharing re-breaks bundled zod (#6530 repros fail again) — Next.js standalone down on main

1 participant