Skip to content

Commit d2db140

Browse files
author
Ralph Küpper
committed
fix(instanceof): #7575 — a monomorphized generic class is an instance of the generic
`m instanceof MyMap` was false for a `class MyMap<K, V> extends Map<K, V>` instance while `m instanceof Map` was true. The issue read this as a Map/Set subclass / prototype-chain defect. It is neither: the mechanism is MONOMORPHIZATION, and Map/Set had nothing to do with it. Perry specializes generic classes. `class Gen<T> {}` plus `new Gen<number>()` emits a SECOND class named `Gen$num` (monomorph::mangle::generate_specialized_name) with its own class id, and the instance is stamped with that id — while `x instanceof Gen` resolves the RHS to the GENERIC's id, which appears nowhere in the specialization's parent chain. The bisect that pins it: class Gen<T> extends Base {} new Gen<number>() -> instanceof Gen false class Gen<T> extends Base {} new Gen() -> instanceof Gen true class Conc extends Base {} new Conc() -> instanceof Conc true class GenNoExtends<T> {} new G<number>() -> instanceof G false The last row has no base class at all, so this was never about `super()`-to-a- native-base wiring. `class MyMap<K, V> extends Map<K, V>` is simply the idiomatic spelling, which is why it surfaced there. HIR now records `Class::specialized_from`; codegen emits one `js_register_class_generic_origin(spec, generic)` per specialization next to the parent edges; and `instanceof`'s chain walk (now one shared, depth-bounded `class_chain_reaches`, used by both the static and the dynamic-RHS path) follows that edge as well as `extends`. It is deliberately a SEPARATE edge, not a CLASS_REGISTRY parent edge: that chain also resolves `super()` construction, static-method lookup and vtable dispatch, so splicing the generic in between a specialization and its real base would re-run the wrong constructor. The Array-side sibling #7603 left unfixed is covered by the same mechanism — `new GenArr<number>() instanceof GenArr` now holds. `constructor.name` still reports the mangled `Gen$num`; that is the same root cause on a different surface and is filed separately rather than folded in here. Validated locally: new gap test byte-identical to node 26.5.1 and byte-identical again under PERRY_GC_ZEAL=1 + PERRY_GC_PROTECT_FROMSPACE=1; 4 new runtime unit tests over the walk (including that the edge stays directional and does not make sibling specializations match); test_gap_6325 and test_gap_7570 tightened to assert the subclass edge the issue asked for.
1 parent bce1045 commit d2db140

19 files changed

Lines changed: 406 additions & 38 deletions

File tree

crates/perry-codegen/src/codegen/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,10 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
479479
// #6812: width hints don't cross module metadata; imported stubs
480480
// fall back to runtime learned sizing.
481481
alloc_width_hint: 0,
482+
// #7575: monomorphization is per-module, so an imported stub never
483+
// stands in for a specialization — its defining module registers
484+
// the origin edge itself.
485+
specialized_from: None,
482486
type_params: Vec::new(),
483487
extends: None,
484488
extends_name: ic.parent_name.clone(),

crates/perry-codegen/src/codegen/string_pool.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,38 @@ pub(super) fn emit_string_pool(
515515
);
516516
}
517517

518+
// #7575: register the GENERIC class a monomorphized specialization came
519+
// from. `class Gen<T> {}` + `new Gen<number>()` emits a second class
520+
// `Gen$num` carrying its own class id, and the instance is stamped with
521+
// that id — but `x instanceof Gen` resolves the RHS to the GENERIC's id,
522+
// which is in no parent chain, so the walk answered `false` for the class
523+
// the user wrote. This is a distinct edge from the parent one on purpose:
524+
// `CLASS_REGISTRY`'s chain also resolves `super()`, static-method lookup
525+
// and vtable dispatch, so it must keep pointing at the real base.
526+
let mut origin_pairs: Vec<(u32, u32)> = Vec::new();
527+
for (name, &cid) in class_ids.iter() {
528+
let Some(class) = classes.get(name) else {
529+
continue;
530+
};
531+
let Some(generic_name) = &class.specialized_from else {
532+
continue;
533+
};
534+
if let Some(&generic_cid) = class_ids.get(generic_name) {
535+
if generic_cid != 0 && generic_cid != cid {
536+
origin_pairs.push((cid, generic_cid));
537+
}
538+
}
539+
}
540+
origin_pairs.sort_unstable();
541+
for (cid, generic_cid) in origin_pairs {
542+
chunker.roll_if_full();
543+
let blk = chunker.current_block();
544+
blk.call_void(
545+
"js_register_class_generic_origin",
546+
&[(I32, &cid.to_string()), (I32, &generic_cid.to_string())],
547+
);
548+
}
549+
518550
// Issue #392: register every user class method in the runtime
519551
// VTABLE_REGISTRY so cross-module callers can dispatch via
520552
// `js_native_call_method` even when the codegen of the calling

crates/perry-codegen/src/runtime_decls/strings.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1275,6 +1275,11 @@ pub fn declare_phase_b_strings(module: &mut LlModule) {
12751275
// allocators register on every alloc; the inline allocator skips
12761276
// the alloc-site call and relies on this one-time registration.
12771277
module.declare_function("js_register_class_parent", VOID, &[I32, I32]);
1278+
// #7575: specialization -> generic edge for monomorphized generic classes.
1279+
// A SEPARATE edge from the parent one: the parent chain also resolves
1280+
// `super()`, static-method lookup and vtable dispatch, so only `instanceof`
1281+
// may follow this one.
1282+
module.declare_function("js_register_class_generic_origin", VOID, &[I32, I32]);
12781283
// Issue #711: dynamic parent registration for `class X extends fn(...)`
12791284
// shapes. Codegen emits at the class-declaration source position in
12801285
// module.init (lower.rs); the runtime helper extracts the parent

crates/perry-codegen/src/stmt/element_shape_loop_tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ fn node_class(extends_name: Option<&str>) -> Class {
118118
aliases: Vec::new(),
119119
is_nested: false,
120120
alloc_width_hint: 0,
121+
specialized_from: None,
121122
}
122123
}
123124

crates/perry-codegen/src/type_analysis_tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ fn hir_inferred_types_reuse_codegen_contextual_class_facts() {
215215
is_exported: false,
216216
is_nested: false,
217217
alloc_width_hint: 0,
218+
specialized_from: None,
218219
aliases: Vec::new(),
219220
};
220221
let widget = perry_hir::Class {
@@ -286,6 +287,7 @@ fn hir_inferred_types_reuse_codegen_contextual_class_facts() {
286287
is_exported: false,
287288
is_nested: false,
288289
alloc_width_hint: 0,
290+
specialized_from: None,
289291
aliases: Vec::new(),
290292
};
291293
let classes = HashMap::from([("Base".to_string(), &base), ("Widget".to_string(), &widget)]);

crates/perry-hir/src/analysis/value_types_tests.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,7 @@ fn seeds_contextual_class_and_enum_facts_from_module() {
623623
is_exported: false,
624624
is_nested: false,
625625
alloc_width_hint: 0,
626+
specialized_from: None,
626627
aliases: Vec::new(),
627628
});
628629

@@ -696,6 +697,7 @@ fn infers_named_class_and_interface_property_facts() {
696697
is_exported: false,
697698
is_nested: false,
698699
alloc_width_hint: 0,
700+
specialized_from: None,
699701
aliases: Vec::new(),
700702
});
701703
module.classes.push(Class {
@@ -721,6 +723,7 @@ fn infers_named_class_and_interface_property_facts() {
721723
is_exported: false,
722724
is_nested: false,
723725
alloc_width_hint: 0,
726+
specialized_from: None,
724727
aliases: Vec::new(),
725728
});
726729
module.interfaces.push(Interface {
@@ -1665,6 +1668,7 @@ fn resolves_this_and_super_in_class_context() {
16651668
is_exported: false,
16661669
is_nested: false,
16671670
alloc_width_hint: 0,
1671+
specialized_from: None,
16681672
aliases: Vec::new(),
16691673
});
16701674
module.classes.push(Class {
@@ -1690,6 +1694,7 @@ fn resolves_this_and_super_in_class_context() {
16901694
is_exported: false,
16911695
is_nested: false,
16921696
alloc_width_hint: 0,
1697+
specialized_from: None,
16931698
aliases: Vec::new(),
16941699
});
16951700

crates/perry-hir/src/ir/decl.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,19 @@ pub struct Class {
268268
/// site. Pure capacity: does not add fields, keys, or enumeration
269269
/// entries. 0 = no hint.
270270
pub alloc_width_hint: u32,
271+
/// #7575: the GENERIC class this one was monomorphized from, when it is a
272+
/// specialization. `class Gen<T> {}` + `new Gen<number>()` produces a second
273+
/// class named `Gen$num` (`monomorph::mangle::generate_specialized_name`)
274+
/// carrying its own class id, and the instance is stamped with THAT id —
275+
/// while `x instanceof Gen` resolves the RHS to the generic's id, which no
276+
/// longer appears anywhere in the instance's chain.
277+
///
278+
/// The runtime learns the edge from here (`js_register_class_generic_origin`)
279+
/// and consults it during the `instanceof` chain walk ONLY. It deliberately
280+
/// is not folded into `extends`/`extends_name`: the runtime parent chain also
281+
/// resolves `super()`, static-method lookup and vtable dispatch, so splicing
282+
/// the generic in as a parent would re-run the wrong constructor.
283+
pub specialized_from: Option<String>,
271284
}
272285

273286
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

crates/perry-hir/src/lower/context.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,7 @@ impl LoweringContext {
11011101
// timing is irrelevant.
11021102
is_nested: false,
11031103
alloc_width_hint,
1104+
specialized_from: None,
11041105
});
11051106

11061107
self.anon_shape_classes

crates/perry-hir/src/lower/module_decl/namespace.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ pub(crate) fn lower_namespace_as_class(
110110
aliases: Vec::new(),
111111
is_nested: false,
112112
alloc_width_hint: 0,
113+
specialized_from: None,
113114
});
114115
}
115116
};
@@ -409,5 +410,6 @@ pub(crate) fn lower_namespace_as_class(
409410
aliases: Vec::new(),
410411
is_nested: false,
411412
alloc_width_hint: 0,
413+
specialized_from: None,
412414
})
413415
}

crates/perry-hir/src/lower_decl/class_decl.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,6 +1304,7 @@ pub fn lower_class_decl(
13041304
// initializers must run on class evaluation, not at module init.
13051305
is_nested: ctx.scope_depth > 0 || ctx.inside_block_scope > 0,
13061306
alloc_width_hint: 0,
1307+
specialized_from: None,
13071308
})
13081309
}
13091310

@@ -1911,5 +1912,6 @@ pub fn lower_class_from_ast(
19111912
// initializers must run on class evaluation, not at module init.
19121913
is_nested: ctx.scope_depth > 0 || ctx.inside_block_scope > 0,
19131914
alloc_width_hint: 0,
1915+
specialized_from: None,
19141916
})
19151917
}

0 commit comments

Comments
 (0)