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
21 changes: 21 additions & 0 deletions changelog.d/7826-generic-specialization-constructor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
**A generic class specialization now answers with its generic's constructor, and sees its generic's prototype patches.**
`new Gen<number>()` is monomorphized into a separate class (`Gen$num`) with its
own class id; TypeScript erases type arguments, so at runtime there is exactly
one `Gen`. After `instanceof` (#7575), `constructor.name` (#7632) and the two
prototype-object registries (#7762), two id-keyed holes remained. The instance
`.constructor` arm synthesized the class ref straight from the instance's class
id, so `a.constructor !== Gen` and `a.constructor !== b.constructor` — and since
#7632 gave both the display name `Gen`, they printed identically while comparing
unequal. Separately, and untouched by #7762's aliasing of the prototype
*objects*, the prototype-method chain walk started at the specialization's id
and followed only parent edges, so `Gen.prototype.tag = "G"` was invisible from a
specialized instance even though `Object.getPrototypeOf(a) === Gen.prototype`
reported true — the two edges disagreed about the same object. Both now take the
same generic-origin edge, with the generic tried *before* the parent because it
is an alias rather than an ancestor. Method dispatch is deliberately not aliased:
it runs off the per-class-id vtable, so each specialization keeps its own
monomorphized bodies. Covered by
`test-files/test_gap_generic_specialization_constructor_identity_7757.ts`, which
also pins the negative direction — distinct generics stay distinct, a specialized
subclass reports the subclass rather than its base, and `instanceof` still
discriminates. (#7757)
Original file line number Diff line number Diff line change
Expand Up @@ -1988,7 +1988,7 @@ pub(crate) fn lookup_prototype_method(class_id: u32, name: &str) -> Option<f64>
return Some(f64::from_bits(bits));
}
}
match get_parent_class_id(cid) {
match crate::object::class_generic_origin(cid).or_else(|| get_parent_class_id(cid)) {
Some(p) if p != 0 && p != cid => {
cid = p;
depth += 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,22 @@ pub(super) unsafe fn instance_constructor_value(
if let Some(v) = own_data_field_by_name(obj, key) {
return Some(v);
}
let class_id = (*obj).class_id;
// #7757: a monomorphized specialization must present the GENERIC's
// reflective surface. `new Gen<number>()` is stamped with `Gen$num`'s id
// (`monomorph::mangle::generate_specialized_name`), so every arm below
// answered with the specialization -- making `a.constructor !== Gen` and,
// worse after #7632 gave both the same display name, two constructors that
// PRINT identically and compare unequal. TypeScript erases type arguments:
// at runtime there is exactly one `Gen`.
//
// This is the third and last identity surface to take the same origin edge
// `instanceof` uses (#7575), the display name uses (#7632) and the two
// prototype registries use (#7762). METHOD DISPATCH is deliberately not
// aliased -- it runs off the per-class-id vtable, so each specialization
// keeps its own monomorphized bodies.
let class_id = crate::object::class_generic_origin((*obj).class_id)
.filter(|generic| is_class_id_registered(*generic))
.unwrap_or((*obj).class_id);
// #6530: a capture-carrying class has no ClassRef value — the
// class VALUE is the per-evaluation class OBJECT registered at
// `js_object_mark_class` time. Return that same object so
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Perry monomorphizes a generic class: `new Gen<number>()` is stamped with a
// SEPARATE class id (`monomorph::mangle::generate_specialized_name` →
// `Gen$num`). TypeScript erases type arguments, so at runtime there is exactly
// one `Gen`, one `Gen.prototype` and one `Gen.prototype.constructor` — the
// specializations are an implementation detail that must not reach any
// id-keyed identity or lookup surface.
//
// Three faces of the same leak were fixed one at a time: `instanceof` (#7575),
// `constructor.name` (#7632), and the two prototype-object registries (#7762).
// This file pins the two that remained — the CONSTRUCTOR VALUE, and the
// PROPERTY LOOKUP chain, which is the one that made a prototype patch
// invisible on a specialized instance.
//
// #7632 made the constructor case worse before it got better: two constructors
// that print identically and compare unequal is a harder failure to debug than
// an obviously-wrong name.

class Gen<T> {
v: T | undefined;
tell(): string {
return "gen";
}
}

const a = new Gen<number>();
const b = new Gen<string>();
const c = new Gen(); // never specialized

// --- constructor identity ---------------------------------------------------
console.log("a===Gen:", a.constructor === Gen);
console.log("b===Gen:", b.constructor === Gen);
console.log("c===Gen:", c.constructor === Gen);
console.log("a===b:", a.constructor === b.constructor);
console.log("name:", (a.constructor as any).name);

// The prototype edge and the constructor edge must AGREE. They disagreed for a
// release: `getPrototypeOf(a) === Gen.prototype` was already true while
// `a.constructor === Gen` was false.
console.log("proto===Gen.prototype:", Object.getPrototypeOf(a) === Gen.prototype);
console.log("protoA===protoB:", Object.getPrototypeOf(a) === Object.getPrototypeOf(b));
console.log("a.ctor===proto.ctor:", a.constructor === (Object.getPrototypeOf(a) as any).constructor);

// --- prototype LOOKUP, not just prototype identity --------------------------
// A patch on `Gen.prototype` must be visible from a specialized instance. It
// was not: the instance's chain walk keyed on the specialization's id and never
// reached the generic's entries, so this read was `undefined` while
// `getPrototypeOf(a) === Gen.prototype` reported true.
(Gen.prototype as any).patched = "P";
(Gen.prototype as any).patchedMethod = function (this: any) {
return "pm";
};
console.log("a.patched:", (a as any).patched, (b as any).patched, (c as any).patched);
console.log("a.patchedMethod():", (a as any).patchedMethod(), (b as any).patchedMethod());

// Declared methods still work — they dispatch off the per-class-id vtable,
// which is deliberately NOT aliased so each specialization keeps its own
// monomorphized bodies.
console.log("tell:", a.tell(), b.tell(), c.tell());

// --- the edge must not collapse everything into everything ------------------
class Other<T> {
w: T | undefined;
}
const o = new Other<number>();
console.log("o===Other:", o.constructor === Other);
console.log("o!==Gen:", o.constructor !== (Gen as any));
console.log("a instanceof Gen:", a instanceof Gen, "| o instanceof Gen:", o instanceof Gen);

// A specialization of a SUBCLASS reports the subclass, not the base: the origin
// edge is an alias for one class, not a walk up the parent chain.
class Base {
tag = "base";
}
class Sub<T> extends Base {
u: T | undefined;
}
const s = new Sub<number>();
console.log("s===Sub:", s.constructor === Sub, "| s!==Base:", s.constructor !== (Base as any));
console.log("s instanceof Sub:", s instanceof Sub, "| s instanceof Base:", s instanceof Base);

// Constructing through the reported constructor round-trips.
console.log("new a.ctor instanceof Gen:", new (a.constructor as any)() instanceof Gen);