Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 51 additions & 3 deletions crates/perry-transform/src/inline/factory_specialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,15 @@ pub fn specialize_captured_class_factories(module: &mut Module) {
*next_class_counter += 1;
let mut cloned = class.clone();
cloned.name = cloned_name.clone();
// When the parent arg specializes to a runtime value rather than a
// static class (`Serializable(Mid)` where `Mid` is a local binding, not
// a `ClassRef`), we can't wire a static `extends_name`. The factory
// Call — including the `RegisterClassParentDynamic` its lowered Sequence
// carried — is about to be replaced by a bare `ClassRef` below, which
// would drop the parent edge entirely: `new Top()` would then inherit
// nothing from `Mid` (no fields, no methods, `instanceof Mid` false).
// Re-emit the dynamic registration at the call site instead (#6356).
let mut dynamic_parent_expr: Option<Expr> = None;
if let Some(extends_expr) = cloned.extends_expr.as_mut() {
substitute_locals(extends_expr, &param_subst, &mut next_id_seed);
if let Expr::ClassRef(parent_name) = extends_expr.as_ref() {
Expand All @@ -837,6 +846,8 @@ pub fn specialize_captured_class_factories(module: &mut Module) {
.find(|c| &c.name == parent_name)
.map(|c| c.id)
});
} else {
dynamic_parent_expr = Some((**extends_expr).clone());
}
}
// Filter out the capture ctor params + matching synthetic fields +
Expand Down Expand Up @@ -914,13 +925,42 @@ pub fn specialize_captured_class_factories(module: &mut Module) {
cloned.constructor = None;
}
}
// Assign a FRESH, unique class id. `class.clone()` above copied the
// template's id, and codegen keys the runtime class registry (methods,
// constructor, parent edges) by `c.id` (`codegen/mod.rs` builds
// `class_ids` from each class's `id`). Leaving the copy in place makes a
// clone collide 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 then share one registry
// slot, last-writer-wins on the constructor/parent edge, and a two-level
// dynamic mixin chain loses its intermediate parent (#6356). Ids past
// the current max are free: codegen derives its imported-class fallback
// range from the post-specialization max, and static parent edges are
// resolved by NAME, so only the numeric id has to be unique here. Later
// clones referencing this one as a static parent (see the `extends`
// lookup above) read the id assigned here, so assign before pushing.
let base_class_id = classes.iter().map(|c| c.id).max().unwrap_or(0) + 1;
cloned.id = base_class_id + new_classes.len() as u32;
new_classes.push(cloned);
// Replace the Call with `ClassRef(cloned_name)`. The Let's init is
// now a plain ClassRef — the regular inliner won't touch it and
// subsequent `new <X>()` sites will see it as an alias for the
// specialized class via the existing `local_class_aliases`
// mechanism in codegen.
*expr = Expr::ClassRef(cloned_name);
// mechanism in codegen. When the parent is a runtime value, sequence
// the dynamic parent registration ahead of the ClassRef (the Sequence
// still yields the ClassRef as its value) so the parent edge is wired
// when this init runs — mirroring the class-expression lowering in
// `perry-hir`'s `arm_class.rs`.
*expr = match dynamic_parent_expr {
Some(parent_expr) => Expr::Sequence(vec![
Expr::RegisterClassParentDynamic {
class_name: cloned_name.clone(),
parent_expr: Box::new(parent_expr),
},
Expr::ClassRef(cloned_name),
]),
None => Expr::ClassRef(cloned_name),
};
true
}

Expand All @@ -942,13 +982,21 @@ pub fn specialize_captured_class_factories(module: &mut Module) {
// lets `lower_new` walk through the specialized class for inherited
// constructors and field initializers instead of the unspecialized
// anonymous original.
//
// `parent_expr` may be an `Expr::Sequence([RegisterClassParentDynamic, …,
// ClassRef(<inline>)])` when a class DECLARATION extends a factory call
// with a runtime-value arg (`class Child extends Factory(runtimeVar) {}`):
// `rewrite_call_init` rewrote the inner factory Call to that Sequence
// (see `rewrite_to_specialized_class`). Resolve through `classref_name`,
// which unwraps a Sequence to its trailing `ClassRef`, so the child's
// static parent still hoists rather than falling back to the runtime edge.
for stmt in &module.init {
if let Stmt::Expr(Expr::RegisterClassParentDynamic {
class_name,
parent_expr,
}) = stmt
{
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
Expand Down
114 changes: 114 additions & 0 deletions test-files/test_gap_6356_dynamic_parent_mixin_chain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Issue #6356 — a class whose parent is wired at runtime (via
// `RegisterClassParentDynamic`, i.e. any mixin/factory class that declines the
// HIR mixin fast path) must inherit the base's instance FIELD initializers and,
// through a TWO-LEVEL dynamic chain, the grandparent's methods + `instanceof`.
//
// These shapes route through `specialize_captured_class_factories`
// (`perry-transform`), which monomorphizes the returned class per call site.
// Two defects made the two-level chain fail:
//
// 1. The specialized clone reused the template class's `id` (`class.clone()`
// copies it). Codegen keys the runtime class registry by that id, so a
// clone collided with its template AND with sibling clones of the same
// template — the distinct classes shared one registry slot (last-writer-
// wins on the constructor / parent edge).
// 2. When the parent arg specialized to a runtime value (`Serializable(Mid)`
// where `Mid` is a local binding, not a static `ClassRef`), the factory
// Call — including the `RegisterClassParentDynamic` its Sequence carried —
// was replaced by a bare `ClassRef`, dropping the parent edge entirely.
//
// Net symptom before the fix: `new Serializable(Greetable(Base))()` / the
// `Top = Serializable(Mid)` chain lost `.value`, `greet()`, and
// `instanceof Mid`. See the matrix in #6355 (which deliberately left these
// cells out because they matched neither node nor a clean gap).

class Base {
value = "base value";
hello() {
return "base hello";
}
}

// A canonical single-statement mixin (takes the HIR fast path when bound
// directly, e.g. `const Mid = Greetable(Base)`).
function Greetable(B: any) {
return class extends B {
greet() {
return "hi";
}
};
}

function Serializable(B: any) {
return class extends B {
ser() {
return "ser";
}
};
}

// A factory whose body is NOT the fast-path shape (`const K = …; return K`),
// so it declines the fast path and is factory-specialized instead.
function ViaConst(B: any) {
const K = class extends B {
greet() {
return "const";
}
};
return K;
}

// --- (1) return-via-const: inherited field through a dynamic parent ---------
const C = ViaConst(Base);
console.log("viaconst method:", new C().hello());
console.log("viaconst field:", (new C() as any).value);
console.log("viaconst instanceof base:", new C() instanceof Base);

// --- (2) two mixins composed in one expression (arg is a Call) -------------
const Comp = Serializable(Greetable(Base) as any);
console.log("composed field:", (new Comp() as any).value);
console.log("composed outer:", new Comp().ser());
console.log("composed inner:", (new Comp() as any).greet());
console.log("composed instanceof base:", new Comp() instanceof Base);

// --- (3) mixin over a mixin RESULT (the core two-level dynamic chain) -------
const Mid = Greetable(Base);
const Top = Serializable(Mid as any);
console.log("mid field:", (new Mid() as any).value);
console.log("mid greet:", (new Mid() as any).greet());
console.log("mid instanceof base:", new Mid() instanceof Base);
console.log("chained field:", (new Top() as any).value);
console.log("chained greet:", (new Top() as any).greet());
console.log("chained ser:", new Top().ser());
console.log("chained inherited method:", (new Top() as any).hello());
console.log("chained instanceof base:", new Top() instanceof Base);
console.log("chained instanceof mid:", new Top() instanceof Mid);

// --- (4) sibling specializations of one template must not collide ----------
// `Comp` and `Top` are both clones of Serializable's returned class; with the
// id-collision bug they shared a class id and their parent edges clobbered each
// other. Re-read both after constructing the other to confirm independence.
const compAgain = new Comp() as any;
const topAgain = new Top() as any;
console.log("sibling comp instanceof base:", compAgain instanceof Base);
console.log("sibling top instanceof mid:", topAgain instanceof Mid);
console.log("sibling comp value:", compAgain.value);
console.log("sibling top value:", topAgain.value);

// --- (5) class DECLARATION extending a factory call with a runtime-value arg
// `class Child extends Serializable(V)` rewrites the factory Call into a
// `Sequence([RegisterClassParentDynamic, ClassRef(clone)])`. The decl-time
// `RegisterClassParentDynamic { class_name: "Child", parent_expr: <that
// Sequence> }` must still hoist the clone as `Child`'s static parent (the hoist
// resolves through the Sequence's trailing `ClassRef`), so `Child` inherits the
// base's field/method through the specialized clone.
const V: any = Base;
class Child extends Serializable(V) {
child() {
return "child";
}
}
console.log("decl field:", (new Child() as any).value);
console.log("decl inherited method:", (new Child() as any).hello());
console.log("decl own method:", new Child().child());
console.log("decl instanceof base:", new Child() instanceof Base);
Loading