Skip to content

Drop the old parent's members from a relinked class chain - #11764

Merged
proggeramlug merged 8 commits into
mainfrom
fix/class-relink-method-visibility
Oct 3, 2026
Merged

proggeramlug merged 8 commits into
mainfrom
fix/class-relink-method-visibility

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

After Object.setPrototypeOf(C.prototype, X), methods, getters and setters from C's old declared parent remained visible through several runtime and compiler dispatch paths. Reads, membership checks, calls and super.m() now follow the replacement instance chain, while C's own members and constructor-side static inheritance remain available. Wide dispatch towers also stop running a stale parent body after prototype redefinition.

Current head: 6a61c2ceff67f5eebc347ae70ca9a54afc13e0c0, refreshed against main 420721032422900c04797dd6ca59155f54eb1295.

The refresh additionally fixes a reproduced defect in the initial PR: a getter on the replacement super chain returning a callable Proxy was rejected as non-callable. The original 2687543 product fails the eight-case fixture with TypeError: m is not a function; the repaired product matches Node 26.5.1 for callable, default, nested and revoked function proxies, ordinary functions, non-callable values, object-target proxies with apply traps, and getter exceptions. Proxy calls use the existing rooted proxy dispatcher. No published ABI signatures change.

Runtime declared-member walks stop at a user-relinked class prototype. Per-name invalidation bytes retire compiler-resolved inherited bodies, including wide towers and direct super calls; existing cache generations retire dispatch caches. The super fallback roots its receiver and arguments across key allocation and getter calls, then refreshes them before calling the returned value. The current-main refresh preserves all GC helper tables and root-holder inventories and routes the landed immutable-global test suite in scoped CI.

Validation at the current head on Linux, Rust nightly 2026-08-20 and LLVM 22.1.8:

  • Locked release compiler plus both matching static wrapper libraries built successfully.
  • All three class-relink integration tests passed, including the callable-proxy regression and wide-tower mutation test.
  • Full serial runtime unit suite: 4,748 passed, zero failed, five ignored.
  • Full serial stdlib unit suite: 244 passed, zero failed.
  • Eight compiled proxy cases match pinned Node 26.5.1.
  • Two separate forced-moving/protected-from-space runs match Node: 72,827 and 72,979 copied objects, with 1,126 and 1,129 protected retired sets. These use the live FORCE/VERIFY/PROTECT and seeded schedule instruments and require positive copy/protection counts.

Full local script lint completed successfully: all 111 gates passed; the compile tier and two CI-only commands were explicitly skipped. The release product and targeted integration tests above ran separately. After lint, the compiler and both static wrappers were rebuilt together at the same current head and frozen with verified SHA256 hashes; the source tree was clean. Fresh hosted PR acceptance remains required; both whole shadow/native root-dominance jobs have now succeeded as recorded below; this body does not claim merge readiness. The earlier cfa candidate's 110-gate result remains historical; the 111-gate result above is from this current head.

The original class fixture checks primed/hit read sites but uses an obsolete poison variable and does not assert actual moving collection. Its passing result alone is not moving-GC evidence; the separate positive controls above supply that evidence. The author's 42-cell instruction-cost comparison is historical and has not been independently repeated at this head.

Separate remaining defects: instanceof still follows declared ancestry; super property reads remain statically resolved; prototype __proto__ assignment is not repaired here; assigned prototype methods in the unrelinked super fallback require separate work. This PR does not close #11760.

Current-head hosted evidence: the whole shadow root-dominance job 110888864845 succeeded; the whole native statepoints job 110888865324 also completed successfully. Both required whole jobs passed at this exact current head. The GC ratchet job 110888865025 failed only its pinned-baseline check: all 73 failed numeric rows exactly match the independently completed main job 110380943914, including values and allowances. This attributes that failure; it does not make the failed job green.

Other current-head failures remain recorded: the five backend IR assertions match the independently completed main job; macOS native-root linking lacks eight CF/Objective-C provider symbols; Windows retains the old try/catch refusal tripwire; moving-witness CI reports the same five zero-movement cells as main. The provider/tripwire repairs are included in #11756 and require current integrated four-target acceptance. The witness harness repair is undergoing a separate native comparison. These dependencies and remaining hosted checks prevent merge approval.

Summary by CodeRabbit

  • Bug Fixes
    • Prototype changes to class instances now affect inherited method, accessor, and super lookups consistently, including when the replacement chain contains callable proxies.
    • Method calls after prototype changes now use the updated chain and report a TypeError when the resolved value is not callable.

After Object.setPrototypeOf(C.prototype, X), a method, getter or setter
declared in C's old parent class stayed visible on C's instances: typeof
inst.m, "m" in inst, inst.m() and super.m() all still answered from the
declared parent.

#11738 made the generic class-chain read follow the recorded link. The
walks over declared members did not: lookup_class_method_in_chain,
method_owner_class_id, the `in` vtable fallback and the call dispatcher's
parent walks followed get_parent_class_id, fixed at declaration. They now
step through instance_chain_parent_class_id, which stops at a class whose
prototype a user relinked; the generic read continues on the link, so a
relink onto another class's prototype finds that class's methods.

The compiler resolves inherited names ahead of time along the declared
extends chain: the dispatch tower for untyped receivers and super.m()
call the ancestor's body directly. Object.setPrototypeOf already retires
every direct-method guard byte and bumps VTABLE_GEN, but only the narrow
tower's shape probe read those bytes. The wide tower (past eight
implementors) and super.m() now read them too, so a redefined parent
method no longer keeps running its old body there either. With the bytes
set, super.m() goes to js_super_method_call_dynamic, which reads the home
object's recorded link and throws when it carries no callable.

The A2 read sites need nothing: the relink restamps C.prototype's shape.
The one_shape_class_relink_methods fixture (node 26.5.1's output) covers
reads, in, Reflect, calls at primed and fresh sites, own members, null and
class-prototype targets, accessors, super.m(), statics, an intermediate
relink, relinking back, and class-typed receivers. The integration test
runs it under forced evacuation and requires the class read sites to have
primed and hit, plus a wide dispatch tower seeing a redefined parent method.
@proggeramlug proggeramlug changed the title ## Drop the old parent's members from a relinked class chain Drop the old parent's members from a relinked class chain Oct 2, 2026
@coderabbitai

coderabbitai Bot commented Oct 2, 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: 0a48e751-9f2d-4a66-99cc-1303cdba02cd

📥 Commits

Reviewing files that changed from the base of the PR and between 2687543 and 6a61c2c.

📒 Files selected for processing (6)
  • changelog.d/11764-class-relink-method-visibility.md
  • crates/perry-runtime/src/object/class_constructors.rs
  • crates/perry/tests/class_relink_methods.rs
  • scripts/ci_e2e_scope.py
  • tests/fixtures/one_shape_class_relink_methods/proxy_super.expected.txt
  • tests/fixtures/one_shape_class_relink_methods/proxy_super.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • changelog.d/11764-class-relink-method-visibility.md

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


📝 Walkthrough

Walkthrough

The runtime now tracks declared class prototype relinks, limits instance-method lookup to the live instance prototype chain, and invalidates cached dispatch guards. Compiled method and super calls use dynamic lookup when required. Fixture and integration tests cover relinking, patched methods, and related lookup behavior.

Changes

Prototype relinking and dispatch

Layer / File(s) Summary
Relink tracking and prototype reads
crates/perry-runtime/src/object/class_registry/prototype_methods.rs, crates/perry-runtime/src/object/class_registry/prototype_objects.rs, crates/perry-runtime/src/object/prototype_chain.rs, crates/perry-runtime/src/object/class_registry.rs
Prototype relinks now notify the class registry. The registry reads relinked chains, invalidates ancestor method guards, and retires prototype-dependent caches.
Relink-aware instance lookup
crates/perry-runtime/src/object/class_registry/parent_static.rs, crates/perry-runtime/src/object/native_module/class_ref_values.rs, crates/perry-runtime/src/object/native_call_method/*
Instance-chain lookup and related parent traversals stop at a class whose prototype was relinked. Declared class-chain lookups continue to use declared parents.
Guarded method and super dispatch
crates/perry-codegen/src/lower_call/method_override.rs, crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs, crates/perry-codegen/src/expr/super_method.rs, crates/perry-runtime/src/object/class_constructors.rs
Codegen checks method guards before direct dispatch. Dynamic super resolution reads the relinked chain and calls a callable property with the original receiver and arguments.
Relinking fixture and integration tests
crates/perry/tests/class_relink_methods.rs, tests/fixtures/one_shape_class_relink_methods/*, changelog.d/11764-class-relink-method-visibility.md, scripts/ci_e2e_scope.py
Tests compare relinking scenarios with expected output, check method-site statistics, and exercise callable proxies in super calls. The changelog describes method visibility after prototype changes, and scoped test selection includes the immutable thread-global IR suite.

Priority: ⬇️ Low

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

Change: Bug fix · Severity of issue fixed: Low

Sequence Diagram(s)

sequenceDiagram
  participant CompiledSuperCall
  participant PrototypeMethodGuard
  participant DynamicSuperDispatcher
  participant RelinkedPrototypeChain
  CompiledSuperCall->>PrototypeMethodGuard: Check invalidation bytes
  alt Guard passes
    CompiledSuperCall->>CompiledSuperCall: Call statically resolved method
  else Guard fails
    CompiledSuperCall->>DynamicSuperDispatcher: Pass class ID, method name, receiver, and arguments
    DynamicSuperDispatcher->>RelinkedPrototypeChain: Resolve the named property
    RelinkedPrototypeChain-->>DynamicSuperDispatcher: Return the property value
    DynamicSuperDispatcher->>DynamicSuperDispatcher: Call callable value with receiver and arguments
  end
Loading

Merge Risk: ⚪ Minimal · up to 6a61c

No merge-blocking issue was identified in the relinked prototype behavior. The separately acknowledged patched-method super case remains outside this change.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 6a61c

The inspected paths preserve callable checks and receiver identity while correcting stale inherited-method dispatch. No introduced security-control bypass was established. Confidence remains limited by incomplete coverage of concurrent access and adjacent prototype-sensitive operations.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — A class-prototype relink changes lookup for instances using that chain. Guard retirement can also force unrelated compiled calls sharing an inherited name or guard slot onto runtime resolution within the process. The inspected path does not establish cross-process or cross-tenant exposure.

Trust Boundaries and Controls

  • inferred — Code able to relink the home class prototype can now supply the getter or callable reached by affected super calls instead of the stale declared-parent body. This is intentional prototype authority, not an evidenced privilege grant: invocation preserves the receiver and rejects non-callable values.
  • observed — The inspected setter validates prototype type, rejects changing a non-extensible target and checks cycles before ordinary-object publication. These controls predate the PR and remain in the mutation path.

Resilience and Maintainability Implications

  • observed — The override marker remains sticky across further prototype writes. Base/head comparison shows this is existing behavior, while recorded links continue to be updated. Sticky invalidation alone therefore does not establish an introduced restoration or cleanup defect.
🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Issue [#11760] requires super.m() to observe patched parent methods and relinked prototypes. This PR adds guarded dynamic dispatch and tests for relinked prototypes, including callable proxies. The … Update super.m() dispatch to resolve later parent-prototype method patches through the live prototype chain. Add an automated regression test for the issue example and verify the patched result.
Out of Scope Changes check ⚠️ Warning The runtime, fixture, and test changes support [#11760] and related relinked-prototype behavior. However, scripts/ci_e2e_scope.py adds thread_immutable_globals to the perry-codegen suite mapping… Remove the unrelated thread_immutable_globals CI mapping change, or provide a concrete issue-related reason and test dependency for retaining it.
Docstring Coverage ⚠️ Warning Docstring coverage is 46.15% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 39 functions across 17 files. (2 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: removing the declared parent’s members from a relinked class’s instance chain.
Description check ✅ Passed The description explains the change, related issue context, limitations, and detailed validation results. It does not use the template headings or complete the checklist, but the required information …
Full details: Linked Issues check

Explanation

Issue [#11760] requires super.m() to observe patched parent methods and relinked prototypes. This PR adds guarded dynamic dispatch and tests for relinked prototypes, including callable proxies. The summary states that B.prototype.m = f still resolves through the declared vtable when the home object is not relinked. The patched-method requirement remains unmet.

Full details: Out of Scope Changes check

Explanation

The runtime, fixture, and test changes support [#11760] and related relinked-prototype behavior. However, scripts/ci_e2e_scope.py adds thread_immutable_globals to the perry-codegen suite mapping. The summary provides no connection between this CI selection change and super.m() patched or relinked parent lookup. This change is outside the linked issue scope.

Full details: Docstring Coverage

Explanation

Docstring coverage is 46.15% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 39 functions across 17 files. (2 skipped: 2 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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 added the run-extended-tests Opt PR into compile-smoke/parity/doc-tests/drizzle-mysql-smoke label Oct 2, 2026
@proggeramlug
proggeramlug merged commit e59101e into main Oct 3, 2026
16 of 17 checks passed
@proggeramlug
proggeramlug deleted the fix/class-relink-method-visibility branch October 3, 2026 04:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-extended-tests Opt PR into compile-smoke/parity/doc-tests/drizzle-mysql-smoke

Projects

None yet

Development

Successfully merging this pull request may close these issues.

super.m() ignores a patched or relinked parent prototype

1 participant