Skip to content

Generic-class specializations are distinct constructor objects: a.constructor !== Gen, and two specializations compare unequal #7757

Description

@proggeramlug

Two specializations of one generic class are distinct constructor objects, and an instance's .constructor is not the class the user wrote.

Repro

class Gen<T> { v: T | undefined; }
const a = new Gen<number>();
const b = new Gen<string>();
const c = new Gen();                       // never specialized
console.log(a.constructor === Gen, b.constructor === Gen, c.constructor === Gen);
console.log(a.constructor === b.constructor);
console.log(Object.getPrototypeOf(a) === Gen.prototype,
            Object.getPrototypeOf(a) === Object.getPrototypeOf(b));
$ node --experimental-strip-types t.ts
true true true
true
true true

$ ./t
false false true
false
true false

Why

new Gen<number>() is monomorphized into a separate class Gen$num with its own class id (monomorph::mangle::generate_specialized_name), and the instance carries that id. TypeScript erases type arguments, so at runtime there is exactly one Gen — the specializations are an implementation detail that is leaking through every id-keyed identity surface.

This is the third face of the same leak:

Worth noting that #7756 makes this one harder to see, not easier: after it, a.constructor.name === b.constructor.name === "Gen" while a.constructor !== b.constructor. Two things that print identically and compare unequal is a worse failure mode for a user to debug than an obviously-wrong name, so this is the natural follow-up to schedule rather than leave implicit.

Already inconsistent today

Object.getPrototypeOf(a) === Gen.prototype is true while a.constructor === Gen is false — so the prototype edge and the constructor edge already disagree with each other about whether the specialization is Gen. Whatever the fix is, that pair should come out consistent.

Shape of a fix (unverified)

The specialization exists to monomorphize method bodies; nothing about that requires a distinct constructor object or prototype. Candidates, in rough order of blast radius:

  1. Point the specialization's constructor/prototype registry entries at the generic's, keeping only the specialized method table — i.e. treat specialized_from as an identity alias for the reflective surfaces, the way instanceof a Map/Set SUBCLASS is false (m instanceof MyMap); only the native base edge survives #7575 already treats it for instanceof.
  2. Stop specializing classes whose type arguments do not change any lowering decision (the erased-generic majority), so no second class exists to diverge. driver.rs already declines to specialize when a type argument is still an unresolved type var (the rxjs Observable<R> case) — this would extend that judgment.

Option 2 also removes the code-size cost of specializing classes that monomorphization does not benefit.

Not urgent

Every shape here is reflective/identity code, not a miscompile: instanceof, field access, method dispatch and .name are all correct. Filing so the last face of the leak is tracked rather than rediscovered.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions