fix(transform): specialized mixin clones keep parent edge + a unique id (#6356)#6548
fix(transform): specialized mixin clones keep parent edge + a unique id (#6356)#6548proggeramlug wants to merge 2 commits into
Conversation
…id (PerryTS#6356) A class whose parent is wired at runtime (a mixin/factory class that declines the HIR mixin fast path) lost the base's inheritance through a two-level dynamic chain: `const Mid = Greetable(Base); const Top = Serializable(Mid)` produced a `Top` with no fields, no inherited methods, and `new Top() instanceof Mid === false`. These shapes route through `specialize_captured_class_factories`, which monomorphizes the returned class per call site. Two defects there broke the chain: 1. **Colliding class id.** `class.clone()` copied the template's `id`, and the pass renamed the clone but never reassigned it. Codegen keys the runtime class registry (methods, constructor, parent edges) by `c.id`, so a clone collided with its template AND with sibling clones of the same template (`Serializable(Greetable(Base))` and `Serializable(Mid)` both clone `__anon_class_N`) — the distinct classes shared one registry slot, last-writer-wins on the constructor/parent edge. 2. **Dropped parent edge for a runtime-value parent.** When the parent arg specialized to a runtime value (`Serializable(Mid)`, `Mid` a local binding) rather than a static `ClassRef`, no static `extends_name` could be set, and the factory Call — including the `RegisterClassParentDynamic` its lowered Sequence carried — was replaced by a bare `ClassRef`, dropping the parent edge entirely. Fix: assign each specialized clone a fresh, unique class id (past the current max; codegen resolves static parent edges by name and derives its imported-class id range from the post-specialization max, so only uniqueness matters), and when the specialized parent is a runtime value, sequence a `RegisterClassParentDynamic` ahead of the `ClassRef` so the parent edge is wired when the init runs — mirroring the class-expression lowering in perry-hir's `arm_class.rs`. Adds `test_gap_6356_dynamic_parent_mixin_chain.ts` covering return-via-const, composed mixins, the two-level `Serializable(Mid)` chain, and sibling-clone independence. Byte-identical to `node --experimental-strip-types`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughFactory-specialized classes now preserve runtime parent expressions, register dynamic parent edges at factory call sites, receive unique class IDs, and support hoisting through rewritten sequences. A regression test covers dynamic mixin chains, inherited fields and methods, ChangesDynamic parent specialization
Estimated code review effort: 4 (Complex) | ~35 minutes Sequence Diagram(s)sequenceDiagram
participant rewrite_to_specialized_class
participant FactoryCallSite
participant RegisterClassParentDynamic
participant module_init
rewrite_to_specialized_class->>FactoryCallSite: replace factory Call
FactoryCallSite->>RegisterClassParentDynamic: register cloned class with runtime parent
RegisterClassParentDynamic-->>FactoryCallSite: yield specialized ClassRef
module_init->>RegisterClassParentDynamic: hoist parent edge from rewritten sequence
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/perry-transform/src/inline/factory_specialize.rs (1)
991-996: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winUse
classref_nameto correctly hoist parents from sequence expressions.With the newly introduced dynamic parent behavior, factory calls can be rewritten into
Expr::Sequenceblocks containingRegisterClassParentDynamicandClassRef. If a user writes a class declaration likeclass Child extends DynamicFactory(Var) {}, theparent_exprinsidemodule.init's pre-existing registration will be anExpr::Sequence.The strict
if let Expr::ClassRef(...) = parent_expr.as_ref()match will fail on sequences, preventing the static parent from being hoisted and causing the child class to lose inherited fields.Using the existing
classref_namehelper ensures the name is extracted correctly even if it's wrapped in a sequence.🐛 Proposed fix
- if let Expr::ClassRef(parent_name) = parent_expr.as_ref() { + if let Some(ref parent_name) = classref_name(parent_expr.as_ref()) { if let Some(child) = module.classes.iter_mut().find(|c| &c.name == class_name) { child.extends_name = Some(parent_name.clone()); if let Some(parent_cls) = new_classes .iter() .find(|c| &c.name == parent_name)🤖 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-transform/src/inline/factory_specialize.rs` around lines 991 - 996, Update the parent-name extraction in the class specialization logic around the existing Expr::ClassRef match to use the classref_name helper instead. Ensure sequence-wrapped ClassRef expressions produced by dynamic parent registration resolve their parent name, so the existing extends_name assignment and parent hoisting continue to apply.
🧹 Nitpick comments (1)
crates/perry-transform/src/inline/factory_specialize.rs (1)
928-944: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueExtract the base class ID calculation to avoid O(N) recomputation.
base_class_idis recalculated by iterating over all original module classes every time a factory call is rewritten. While functionally correct and guaranteed to produce unique IDs, this results inO(N)work per rewrite. You could calculate this once before thevisit_stmtstraversal and either pass it down or keep a mutable max ID counter to eliminate the redundant loops.🤖 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-transform/src/inline/factory_specialize.rs` around lines 928 - 944, Move the initial class-ID calculation out of the per-rewrite logic and compute it once before the visit_stmts traversal. Pass that base value into the factory specialization flow or maintain a mutable next-ID counter, then update the cloned.id assignment to allocate IDs without iterating over classes for each rewrite while preserving uniqueness across all new clones.
🤖 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-transform/src/inline/factory_specialize.rs`:
- Around line 991-996: Update the parent-name extraction in the class
specialization logic around the existing Expr::ClassRef match to use the
classref_name helper instead. Ensure sequence-wrapped ClassRef expressions
produced by dynamic parent registration resolve their parent name, so the
existing extends_name assignment and parent hoisting continue to apply.
---
Nitpick comments:
In `@crates/perry-transform/src/inline/factory_specialize.rs`:
- Around line 928-944: Move the initial class-ID calculation out of the
per-rewrite logic and compute it once before the visit_stmts traversal. Pass
that base value into the factory specialization flow or maintain a mutable
next-ID counter, then update the cloned.id assignment to allocate IDs without
iterating over classes for each rewrite while preserving uniqueness across all
new clones.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 633a3215-ccee-4eda-bf7e-52d722647de9
📒 Files selected for processing (2)
crates/perry-transform/src/inline/factory_specialize.rstest-files/test_gap_6356_dynamic_parent_mixin_chain.ts
…erryTS#6356) Addresses CodeRabbit review on PerryTS#6548. A class DECLARATION extending a factory call with a runtime-value arg (`class Child extends Serializable(V) {}`) has its inner factory Call rewritten to `Sequence([RegisterClassParentDynamic, ClassRef(clone)])`. The decl-time `RegisterClassParentDynamic { class_name: "Child", parent_expr }` then carries that Sequence, so the strict `if let Expr::ClassRef(..) = parent_expr` hoist failed to set `Child`'s static `extends_name` — the child fell back to the runtime dynamic edge instead of the static chain `lower_new` walks for inherited constructors/field initializers. Resolve the parent through `classref_name`, which unwraps a Sequence to its trailing `ClassRef`. Confirmed via `--trace llvm`: the static `js_register_class_parent(Child, clone)` edge is now emitted. Extends the PerryTS#6356 fixture with scenario (5) covering this declaration shape; byte-identical to `node --experimental-strip-types`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @coderabbitai — good catch on the Fixed in 116ae80 by resolving through the existing |
|
Tip For best results, initiate chat on the files or code changes.
🐇✨ ✏️ Learnings added
If you found this review helpful, would you consider giving us a shout-out on X? Thank you for using CodeRabbit! |
Fixes #6356.
Symptom
A class whose parent is wired at runtime (a mixin/factory class that declines the HIR mixin fast path) loses the base's inheritance through a two-level dynamic chain:
On current
mainmost of the #6356 matrix already passes (field-init through a single dynamic parent was fixed by later work); the residual failure is the two-levelSerializable(Mid)chain, whereTopinherits nothing fromMid.Root cause
These shapes route through
specialize_captured_class_factories(crates/perry-transform/src/inline/factory_specialize.rs), which monomorphizes the returned class per call site. Two defects there broke the chain:Colliding class id.
class.clone()copied the template'sid, and the pass renamed the clone but never reassigned it. Codegen keys the runtime class registry (methods, constructor, parent edges) byc.id, so a clone collided with its template and with sibling clones of the same template (Serializable(Greetable(Base))andSerializable(Mid)both clone__anon_class_N) — the distinct classes shared one registry slot, last-writer-wins on the constructor/parent edge.Dropped parent edge for a runtime-value parent. When the parent arg specialized to a runtime value (
Serializable(Mid), whereMidis a local binding) rather than a staticClassRef, no staticextends_namecould be set, and the factoryCall— including theRegisterClassParentDynamicits loweredSequencecarried — was replaced by a bareClassRef, dropping the parent edge entirely.Confirmed via
--trace llvm: the clone registered under the template's id (js_register_class_method/js_register_class_constructoremitted twice for the same cid), and no parent edge (js_register_class_parent[_dynamic]) was emitted for it.Fix
RegisterClassParentDynamicahead of theClassRefso the parent edge is wired when the init runs — mirroring the class-expression lowering inperry-hir'sarm_class.rs. TheSequencestill yields theClassRefas its value.Validation
test-files/test_gap_6356_dynamic_parent_mixin_chain.ts(return-via-const, composed mixins, the two-levelSerializable(Mid)chain, and sibling-clone independence) is byte-identical tonode --experimental-strip-types.(Top → Mid)dynamic edge, soTop → Mid → Baseresolves:instanceof Mid, inheritedgreet, and thevaluefield all match node.test_gap_5952_mixin_factory_binding,test_gap_class_expr_*,test_gap_class_ref_new_dynamic,test_gap_class_extends_function_static. The two pre-existing triaged failures in that neighbourhood (test_gap_2159_defineproperty_class_prototype,test_gap_class_expr_extends_static_call_this) produce identical output with and without this change (A/B'd against a stashed baseline).cargo test -p perry-transformgreen (48 tests);cargo fmt --all --checkand the 2000-line file-size gate pass.🤖 Generated with Claude Code
Summary by CodeRabbit
extendsrelationships and inherited fields/methods remain correct through multi-level chains.instanceofcorrectness, and runtime-valueextendsscenarios.