Skip to content

fix(runtime): inherit static data fields from a parent class - #6443

Merged
proggeramlug merged 2 commits into
PerryTS:mainfrom
proggeramlug:fix/static-field-inheritance
Jul 16, 2026
Merged

fix(runtime): inherit static data fields from a parent class#6443
proggeramlug merged 2 commits into
PerryTS:mainfrom
proggeramlug:fix/static-field-inheritance

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

A subclass inherits its parent's static data properties through the class-object prototype chain (Sub.__proto__ === Base) — both in-body static x = … fields and runtime Base.x = … assignments. Perry inherited static methods (they walk the class_id chain via lookup_static_method_in_chain) but the static-data-field read only consulted the receiver class's own CLASS_DYNAMIC_PROPS, so Sub.x returned undefined even though Base defined x.

get_field_by_name now walks the get_parent_class_id chain after an own-field miss, reading each ancestor's CLASS_DYNAMIC_PROPS (and honoring a deleted key on an ancestor). An own field still shadows an inherited one.

Why it matters

Surfaced by Auth.js v5: SignInError.kind = "signIn" is read off a CredentialsSignin subclass (this.kind = this.constructor.kind) to choose the sign-in vs error redirect page. The missing inheritance sent every credentials error to /auth/error instead of /auth/login.

Testing

test_gap_static_field_inheritance.ts (byte-compared to Node): inherited in-body field, inherited external C.x = … assignment, 2-level inheritance, own-field-shadows-inherited, and that shadowing a subclass doesn't mutate the parent. Existing perry-runtime suite passes (the rotating URL/stream failures are pre-existing parallel-execution flakes).

https://claude.ai/code/session_01NjymZygYh31wM9h3wLnUqv

Summary by CodeRabbit

  • Bug Fixes

    • Fixed static DATA inheritance so values no longer return undefined when accessed via subclasses.
    • Static fields now resolve correctly across multiple levels of class inheritance.
    • Ensured subclass-defined static values correctly override inherited ones, including cases involving deletion in intermediate classes.
  • Tests

    • Added/updated regression tests covering inherited static fields, overriding, and deletion/shadowing behavior.

A subclass inherits its parent's static data properties via the class-object
prototype chain (`Sub.__proto__ === Base`) — both in-body `static x = …` fields
and runtime `Base.x = …` assignments. Perry inherited static METHODS (they walk
the class_id chain) but the static-DATA-field read only consulted the receiver
class's own `CLASS_DYNAMIC_PROPS`, so `Sub.x` returned `undefined` even though
`Base` defined `x`.

`get_field_by_name` now walks the `get_parent_class_id` chain after an own-field
miss, reading each ancestor's `CLASS_DYNAMIC_PROPS` (and honoring a deleted key
on an ancestor). This mirrors the existing static-method chain walk.

Surfaced by Auth.js v5: `SignInError.kind = "signIn"` is read off a
`CredentialsSignin` subclass (`this.constructor.kind`) to choose the sign-in vs
error redirect page; the missing inheritance sent every credentials error to
`/auth/error` instead of `/auth/login`.
@coderabbitai

coderabbitai Bot commented Jul 15, 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: 905971f9-a8c3-49a8-b2c8-414dcaf92547

📥 Commits

Reviewing files that changed from the base of the PR and between 083e8a1 and 675e8eb.

📒 Files selected for processing (2)
  • crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs
  • test-files/test_gap_static_field_inheritance.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • test-files/test_gap_static_field_inheritance.ts
  • crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs

📝 Walkthrough

Walkthrough

Static class-reference property lookup now traverses ancestor class IDs for inherited dynamic static data fields. Regression tests cover direct and deep inheritance, subclass shadowing, and lookup past a deleted intermediate field.

Changes

Static field inheritance

Layer / File(s) Summary
Inherited lookup and regression coverage
crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs, test-files/test_gap_static_field_inheritance.ts
Static data lookup walks up to 32 parent class IDs, skips deleted ancestor keys, and returns the first inherited value. Tests cover inheritance, shadowing, and deletion traversal.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main runtime fix for inherited static data fields.
Description check ✅ Passed The description covers the summary, motivation, and testing, but it omits template sections like Changes and Related issue.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs (1)

855-889: 📐 Maintainability & Code Quality | 🔵 Trivial

Rebuild static wrapper crates and use local compiler feedback.

As per coding guidelines, when changing runtime or stdlib code, rebuild the corresponding static wrapper crates (perry-runtime-static and perry-stdlib-static) because the runtime and stdlib crates themselves emit only rlibs and otherwise may leave stale archives linked. Additionally, use cargo check -p perry for fast local compiler feedback, then cargo build --profile perry-dev -p perry; reserve release/dist profiles for shipping or optimization-sensitive work.

🤖 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/object/field_get_set/get_field_by_name.rs` around
lines 855 - 889, After updating the static-field lookup logic around
CLASS_DYNAMIC_PROPS, rebuild both perry-runtime-static and perry-stdlib-static
so their archives are refreshed. Run cargo check -p perry for fast compiler
feedback, then cargo build --profile perry-dev -p perry; do not use release or
dist profiles for this change.

Source: Coding guidelines

🤖 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/get_field_by_name.rs`:
- Around line 877-879: In the prototype-chain traversal in get_field_by_name,
replace the break used when class_is_key_deleted(cid, name) returns true with
continue. This should skip the deleted property on the current ancestor while
allowing lookup to proceed to higher ancestors.

---

Nitpick comments:
In `@crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs`:
- Around line 855-889: After updating the static-field lookup logic around
CLASS_DYNAMIC_PROPS, rebuild both perry-runtime-static and perry-stdlib-static
so their archives are refreshed. Run cargo check -p perry for fast compiler
feedback, then cargo build --profile perry-dev -p perry; do not use release or
dist profiles for this change.
🪄 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: ac87bdec-2bcd-4582-a8e6-ea07d0649c1e

📥 Commits

Reviewing files that changed from the base of the PR and between cb03eb9 and 083e8a1.

📒 Files selected for processing (2)
  • crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs
  • test-files/test_gap_static_field_inheritance.ts

Comment thread crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs
…nt-chain walk

The static-data-field inheritance walk broke out of the loop when a key was
marked deleted on an intermediate ancestor. But `delete Mid.foo` only removes
Mid's own static — a higher ancestor may still define it, so `Sub.foo` must
inherit `Base.foo`, not resolve to undefined. `break` aborted the whole
traversal; `continue` skips the deleted level and keeps walking up. Safe: `cid`
and `depth` both advance at the top of every iteration, so it can't loop.

Verified against node: `delete Mid.tag; Sub.tag` -> "base" with the fix,
"undefined" without it. Extended test_gap_static_field_inheritance.
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Applied CodeRabbit's breakcontinue finding (675e8eb00) — it's a valid Major bug.

The parent-chain walk aborted on a key deleted at an intermediate ancestor, but delete Mid.foo only removes Mid's own static — Sub.foo must still inherit Base.foo. Verified against node: delete Mid.tag; Sub.tag"base" with the fix, "undefined" without it. The continue is safe: cid and depth both advance at the top of every iteration, so it can't loop. Extended test_gap_static_field_inheritance with the delete-intermediate case (it fails on the pre-fix build, passes after).

@proggeramlug
proggeramlug merged commit fb04be0 into PerryTS:main Jul 16, 2026
26 checks passed
proggeramlug added a commit that referenced this pull request Jul 18, 2026
…istry props (#6552) (#6556)

* fix(runtime): per-evaluation inherited static wins over last-wins registry props (#6552)

A subclass of a class-EXPRESSION value evaluated more than once
(`function make(a){return class{static ast=a}}`, then
`class Number$ extends make(x) {}` / `class Widget$ extends make(y) {}`)
records THIS evaluation's parent object as its static prototype
(`class_prototype_object`, #1788). But #6443's static-DATA-field inheritance
walk reads the parent's `CLASS_DYNAMIC_PROPS`, which are keyed by the
class-expression TEMPLATE id — shared, last-wins across every evaluation — and
it ran BEFORE the per-evaluation prototype-object walk. So every renamed
effect-Schema class read via `import * as M` (`M.Number.ast`/`M.Widget.ast`)
collapsed to the LAST `make(...)`'s static `ast` (the direct export's
`DirectKeyword`), re-breaking effect Schema decode
(test_gap_renamed_class_export_namespace, the #1758/#1812 guard).

Interleave the two walks: at each level of the class-object proto chain,
consult the class's pinned per-evaluation parent object BEFORE the parent's
registry props. The pinned object is authoritative (it carries this
evaluation's own edge); the registry read stays the fallback for a plain
declaration parent (#6443: Auth.js `SignInError.kind`), which has no pinned
object. Depth order is preserved, so a closer registry static still shadows a
deeper per-evaluation one.

test_gap_renamed_class_export_namespace is byte-identical to node again across
renamed (global-colliding + not) and direct exports; #6443 static-field
inheritance + deleted-key fallthrough, #6438 per-eval statics, effect factory
static dispatch, and the rest of the class-expr/static suite (18/18) stay green.

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

* fix(runtime): return a present `null` inherited static, don't fall through (#6552 review)

CodeRabbit review follow-up. The interleaved per-evaluation proto-object read
guarded on `!is_undefined() && !is_null()`, so a static explicitly set to `null`
on one evaluation (`class NullAst$ extends make(null) {}`) was treated as "absent"
and fell through to the parent's last-wins `CLASS_DYNAMIC_PROPS`, returning a
SIBLING evaluation's `ast` instead of `null` (verified: perry read `{_tag:'OBJ'}`
where node reads `null`).

`null` is a present, authoritative value — only `undefined` means "absent here".
Guard on `!is_undefined()` alone so a present `null` is returned and only a true
miss continues to the registry / a higher ancestor. Extends the gap test + helper
with a renamed null-static export (`M.NullAst.ast === null`), placed before the
last non-null `make(...)` so `null` is a genuine discriminator; byte-identical to
node.

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

---------

Co-authored-by: Ralph <ralph@skelpo.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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