fix(hir): register the dynamic parent of a mixin-of-a-mixin (#9079) - #9567
fix(hir): register the dynamic parent of a mixin-of-a-mixin (#9079)#9567proggeramlug wants to merge 2 commits into
Conversation
`const Mixed2 = mixin(Mixed)` — a mixin applied to a previous mixin's
RESULT — SIGSEGVed as soon as anything derived from it was constructed.
Node prints `4` for the issue's reproducer; Perry exited 139 after
unbounded recursion.
The HIR mixin fast path in `lower/stmt.rs` synthesizes a real class for
`const M = mixinFn(Base)`. At the second level the base is `Mixed`, a
lexical VALUE binding, so `lower_class_from_ast` takes its
locally-shadowed arm and captures the parent as `extends_expr` — a
dynamic parent — rather than a static class link. That is correct. What
was missing is the other half: unlike the sibling `const X = class {…}`
path immediately above it, this arm bound the synthesized class without
emitting the declaration-time `RegisterClassParentDynamic`. The
generated `Mixed2_constructor` therefore called
`js_get_dynamic_parent_value` for its class id with no registration to
answer it; with an undefined parent `js_fetch_or_value_super` fell back
to the most-derived receiver, re-selected `Mixed2`, and recursed until
the stack overflowed.
Emit the registration here too, in source order after the parent's own
value binding and before this class's — exactly where the sibling path
puts it. A single-level `mixin(Root)` extends a real class, keeps
`extends_expr` at `None`, and is unchanged: that is why one level
already worked (#9073) and two did not.
Verified on Linux (perrymaster) with a fresh
`PERRY_NO_AUTO_OPTIMIZE=1 cargo build --profile perry-dev -p perry
-p perry-runtime-static -p perry-stdlib-static`:
- Baseline binary built from this tree before the patch: exit 139.
- After: the gap fixture is byte-identical to the pinned Node 26.5.1
oracle under both `PERRY_NO_AUTO_OPTIMIZE=1` and the default
auto-optimize pipeline.
- LLVM for the fixture: every `js_get_dynamic_parent_value(i32 N)` in
the module now has a matching `js_register_class_parent_dynamic(i32
N, …)` — zero orphans; `Mixed2_constructor`'s id is among them.
- `cargo test -p perry-hir`: all green. The new lowering unit test was
confirmed to FAIL against the unpatched lowering.
The gap fixture keeps the issue's reproducer verbatim and adds the
assertions it left open: inherited `Root` state and the mixin method
through both synthesized levels, `instanceof` across the whole chain, a
leaf with no own constructor, the still-working one-level case, and a
three-level chain built from three distinct mixins so a dropped level
shows up as a missing method rather than being masked by identical
bodies.
Closes #9079
Claude-Session: https://claude.ai/code/session_01SNcEDcviLvFMta5oL7Zxig
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (5)
Included review availability: Your plan provides up to 8 included reviews per hour; 3 remain after this review. 📝 WalkthroughWalkthroughThe HIR mixin lowering path now registers dynamic parents for synthesized classes in nested mixin chains. New lowering and runtime tests cover registration order, constructors, field initialization, method lookup, and ChangesDynamic Mixin Parent Chain
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to This change restores correct nested-mixin inheritance without changing public interfaces or deployment behavior; no actionable merge-blocking risk remains after normal checks and review. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Description checkExplanation The description is comprehensive and covers the summary, root cause, fix, related issue, validation, and regression tests. It does not use the template headings or checklist format, but it provides the required substantive information. Full details: Docstring CoverageExplanation Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 4 files. (1 skipped: 1 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 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 |
|
Landed via merge train #9572 (rebase-merge, authorship preserved). |
Gap suite: greenFull serial run of The 5 are exactly the committed snapshot entries ( Note on a first, discarded run — a local environment fault, not a findingAn earlier run of the same suite reported 25 The gap harness flips auto-optimize back on for ext-routed tests, which links against the cached |
Closes #9079
const Mixed2 = mixin(Mixed)— a mixin applied to a previous mixin's result — SIGSEGVed as soon as anything derived from it was constructed. Node prints4for the issue's reproducer; Perry exited 139 after unbounded recursion.Root cause
The HIR mixin fast path in
crates/perry-hir/src/lower/stmt.rssynthesizes a real class forconst M = mixinFn(Base), copying the mixin's class AST with theextendsclause rewritten to the concrete base.At the second level the base is
Mixed, which by then is a lexical value binding (the first level ends withemit_class_expression_value_binding).lower_class_from_asttherefore takes itslocally_shadowedarm and captures the parent asextends_expr— a dynamic parent — instead of a static class link. That part is correct.What was missing is the other half. Unlike the sibling
const X = class {…}path immediately above it in the same function, this arm bound the synthesized class without emitting the declaration-timeRegisterClassParentDynamic. So the generatedMixed2_constructorcalledjs_get_dynamic_parent_valuefor its class id with no registration to answer it. With an undefined parent,js_fetch_or_value_superfell back using the most-derived receiver (Deep), re-selectedMixed2, and recursed until the stack overflowed.This is why the failure looked arbitrary: a single-level
mixin(Root)extends a real class, soextends_exprstaysNoneand no registration is needed — which is exactly the case #9073 fixed.Fix
Capture
lowered_class.extends_exprbeforepush_class_dedupmoves the class out, and push aRegisterClassParentDynamicintomodule.initin source order — after the parent's own value binding (the registration reads that local) and before this class's binding. Same placement the sibling class-expression path uses.Emitted HIR for the reproducer:
Mixed(level 1, static parentRoot) still gets no registration.Validation
All on Linux (x86-64), fresh build of this branch:
exit 139(SIGSEGV); Node prints4.test-files/test_gap_9079_dynamic_parent_chain_own_ctor.tsis byte-identical to the pinned Node 26.5.1 oracle (diff -uclean) under bothPERRY_NO_AUTO_OPTIMIZE=1and the default auto-optimize pipeline, exit 0.--trace llvm): everyjs_get_dynamic_parent_value(i32 N)in the module now has a matchingjs_register_class_parent_dynamic(i32 N, …)— zero orphans.Mixed2_constructorfetches id 9, and id 9 is registered.cargo test -p perry-hir— all green (378 + 22 further test targets, 0 failures).lower::tests::mixin_parent_chain::mixin_of_a_mixin_registers_its_dynamic_parent_before_its_own_binding— confirmed to FAIL against the unpatched lowering (stashed thestmt.rshunk, re-ran, red) and pass with it. It also asserts the negative: level 1 must not gain a dynamic parent.scripts/run_gap_tests.sh(--filter test_gap_, 675 tests, Node 26.5.1,npm cioracle deps): see the run summary in the comments below.cargo fmt --all --checkandgit diff --checkclean. No version bump;Cargo.toml/Cargo.lock/CLAUDE.mduntouched.Regression fixture
test-files/test_gap_9079_dynamic_parent_chain_own_ctor.tskeeps the issue's reproducer verbatim and adds the assertions the issue explicitly left open:Rootstate and the mixin method reached through both synthesized levels;instanceofacross the whole chain (Deep/Mixed2/Mixed/Root);New unit-test module
crates/perry-hir/src/lower/tests/mixin_parent_chain.rs— split out rather than appended tolower/tests.rs, which sits at 1,980 of the 2,000-line file cap.https://claude.ai/code/session_01SNcEDcviLvFMta5oL7Zxig
Summary by CodeRabbit
Bug Fixes
instanceofbehavior across multi-level mixin chains.Tests