Skip to content

fix(monomorph): a specialized generic class reports the generic's name, not Gen$num (#7632) - #7756

Merged
proggeramlug merged 2 commits into
mainfrom
fix/7632-generic-class-name
Aug 10, 2026
Merged

fix(monomorph): a specialized generic class reports the generic's name, not Gen$num (#7632)#7756
proggeramlug merged 2 commits into
mainfrom
fix/7632-generic-class-name

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #7632.

The fix

Monomorphization emits a second class for new Gen<number>() (Gen$num, monomorph::mangle::generate_specialized_name) with its own class id, and the instance is stamped with that id — so every id-keyed user-visible surface reported the mangled name. TypeScript erases type arguments; the mangling is Perry's business.

monomorph/driver.rs now registers the ORIGIN's name against the specialization's class id in Module::class_display_names — the mechanism codegen/string_pool.rs already prefers over the registration key when emitting named-class constants (added for #5592's uniquified class-expression bindings). It reads the origin's own display name rather than its name, so a specialization of an already-uniquified class reports the JS name and not the internal key. Two lines of registration plus a deferred insert, because the collection loop holds module immutably.

node 26.5.1 before after
(new Gen<number>()).constructor.name Gen Gen$num Gen
(new GenNoExtends<number>()).constructor.name GenNoExtends GenNoExtends$num GenNoExtends
(new Gen()).constructor.name (no type args) Gen Gen Gen

The checklist in the issue, measured

The issue asked what else to check in the same pass. All of these were already correct and stay correct:

  • Gen.name read off the constructor binding — correct before and after. (Worth noting the asymmetry that made this bug easy to miss: Gen.name was Gen while a.constructor.name was Gen$num.)
  • Object.prototype.toString.call(new Gen<number>())[object Object].
  • Error subclasses: class MyErr<T> extends Error {}e.name === "Error", String(e) === "Error: boom", e instanceof Error.
  • The instanceof a Map/Set SUBCLASS is false (m instanceof MyMap); only the native base edge survives #7575 instanceof half keeps working, including through a nested class Wrap<T> extends Gen<T> {}.

On the issue's last checklist item — whether a diagnostic keyed on the mangled name becomes ambiguous: the HIR/codegen keys are unchanged. Only class_display_names is written, and it is consulted solely when emitting the JS-visible name constant. --print-hir / --trace still show Gen$num.

What this does NOT fix, and why I'm flagging it

Two specializations of one generic remain distinct constructor objects:

class Gen<T> { v: T | undefined; }
const a = new Gen<number>(), b = new Gen<string>();
a.constructor === Gen              // node: true   perry: false
a.constructor === b.constructor    // node: true   perry: false
Object.getPrototypeOf(a) === Object.getPrototypeOf(b)  // node: true  perry: false

That is the monomorphization model, not the name registry, so it is out of scope here — but it is worth stating plainly that after this change the two report the same NAME while remaining !==, which arguably makes the identity divergence harder to notice than it was. Filing it separately.

(Curiously Object.getPrototypeOf(a) === Gen.prototype is already true, so the prototype and constructor edges disagree with each other today. That is part of the same separate question.)

Validation

  • test-files/test_gap_generic_class_constructor_name_7632.ts — byte-identical to node 26.5.1. Covers nested generics, two specializations of one generic, the no-type-args case, error subclasses, Object.prototype.toString, and the instanceof a Map/Set SUBCLASS is false (m instanceof MyMap); only the native base edge survives #7575 instanceof half.
  • New perry-hir unit test a_specialized_class_reports_the_generics_display_name. Unit as well as gap on purpose: the gap suite is tag-gated, so a regression there sits red for days (CI: PR cargo-test never executes crates/perry integration suites; main-push full run starves under merge trains (documented near-miss) #5960). Verified to fail when the registration is removed.
  • cargo test -p perry-hir --lib: 290 passed. cargo test -p perry-codegen --lib: 822 passed.
  • Targeted parity over 36 class/name/prototype-related gap tests: 35 pass, 1 fails — test_gap_2159_defineproperty_class_prototype, which is a listed parity_fail in gap_snapshot.json (standing gap, issue 2159). A full sweep was not run: this host was at 11 GB free at one point today and a disk-full mid-sweep is its own failure mode, so I scoped the run to the tests that could plausibly be affected and am saying so rather than implying broader coverage.

No version bump (maintainer bumps at merge).

Summary by CodeRabbit

  • Bug Fixes

    • Generic class instances now report their original class name instead of an internal, mangled specialization name.
    • Corrected display names across nested and multiple specializations, inherited classes, error subclasses, and object string representations.
    • Preserved expected constructor bindings and instanceof behavior.
  • Tests

    • Added regression coverage for generic class display names, specialization behavior, inheritance, and type checks.

@proggeramlug
proggeramlug force-pushed the fix/7632-generic-class-name branch from a1900f4 to bff9987 Compare August 10, 2026 08:38
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Generic class monomorphization now registers the original class display name for specialized classes. Unit and integration tests cover constructor names, nested and inherited generics, error behavior, Object.prototype.toString, and instanceof.

Changes

Generic class display names

Layer / File(s) Summary
Register origin display names
crates/perry-hir/src/monomorph/driver.rs, crates/perry-hir/src/monomorph/tests.rs
Monomorphization derives each specialization’s display name from its original class and registers the mapping after specialization. Tests verify the mapping and origin metadata.
Validate constructor names
test-files/test_gap_generic_class_constructor_name_7632.ts, changelog.d/7756-generic-class-display-name.md, CLAUDE.md, Cargo.toml
Runtime tests cover generic, nested, inherited, and error classes, plus Object.prototype.toString and instanceof. The changelog documents the behavior. Project version references change to 0.5.1441.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

  • PerryTS/perry#7631 — Adds generic-origin metadata for instanceof; this change extends specialization tracking to display names.
  • PerryTS/perry#6684 — Uses HIR class display-name handling for incorrect constructor.name values in named class expressions.

Suggested labels: bug, parity

Suggested reviewers: jdalton

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes unrelated workspace version and CLAUDE.md updates that the repository template says maintainers must add at merge time. Remove the Cargo.toml version bump and CLAUDE.md version update; defer release metadata changes to the maintainer merge process.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the monomorphization fix and the incorrect specialized generic class name.
Description check ✅ Passed The description explains the fix, affected behavior, linked issue, validation, scope limits, and test results in sufficient detail.
Linked Issues check ✅ Passed The changes satisfy issue #7632 by preserving generic display names and testing constructor names, related surfaces, and unchanged diagnostic keys.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/7632-generic-class-name

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/perry-hir/src/monomorph/tests.rs (1)

984-1023: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for an origin display-name override.

This test only exercises the original.name fallback because module.class_display_names has no entry for class ID 1.

Add a second case that registers an origin override before monomorphize_module. Assert that the specialization receives that override. This validates the required behavior for already-uniquified classes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-hir/src/monomorph/tests.rs` around lines 984 - 1023, Add a
second test case alongside the existing monomorphization test that inserts a
display-name override for the generic class ID before calling
monomorphize_module. Assert that the generated “Gen$num” specialization’s
module.class_display_names entry uses the registered override instead of falling
back to the original class name, while preserving the existing origin and
specialization checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/perry-hir/src/monomorph/tests.rs`:
- Around line 984-1023: Add a second test case alongside the existing
monomorphization test that inserts a display-name override for the generic class
ID before calling monomorphize_module. Assert that the generated “Gen$num”
specialization’s module.class_display_names entry uses the registered override
instead of falling back to the original class name, while preserving the
existing origin and specialization checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1fa6efc5-3fb0-48ed-9f7b-49930b388239

📥 Commits

Reviewing files that changed from the base of the PR and between fb093cd and bff9987.

📒 Files selected for processing (4)
  • changelog.d/7756-generic-class-display-name.md
  • crates/perry-hir/src/monomorph/driver.rs
  • crates/perry-hir/src/monomorph/tests.rs
  • test-files/test_gap_generic_class_constructor_name_7632.ts

@proggeramlug
proggeramlug force-pushed the fix/7632-generic-class-name branch from bff9987 to 4dfe576 Compare August 10, 2026 09:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Cargo.toml`:
- Line 318: Revert the release-version metadata changes: restore the previous
[workspace.package].version in Cargo.toml at lines 318-318 and restore the
previous Current Version value in CLAUDE.md at lines 11-11; retain only the
changelog fragment.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 732aaae2-91d9-4b2c-9115-58688eeae084

📥 Commits

Reviewing files that changed from the base of the PR and between bff9987 and 4dfe576.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • CLAUDE.md
  • Cargo.toml

Comment thread Cargo.toml

[workspace.package]
version = "0.5.1440"
version = "0.5.1441"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Keep release-version metadata out of feature PRs.

This PR should add the changelog fragment only. The maintainer should apply the coordinated version update during release.

  • Cargo.toml#L318-L318: Revert the [workspace.package].version bump.
  • CLAUDE.md#L11-L11: Revert the Current Version update.
📍 Affects 2 files
  • Cargo.toml#L318-L318 (this comment)
  • CLAUDE.md#L11-L11
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Cargo.toml` at line 318, Revert the release-version metadata changes: restore
the previous [workspace.package].version in Cargo.toml at lines 318-318 and
restore the previous Current Version value in CLAUDE.md at lines 11-11; retain
only the changelog fragment.

Sources: Coding guidelines, Learnings

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Merging as v0.5.1441

A/B'd against node on my own host:

[old] ctor.name: Gen$num   noext: GenNoExtends$num   ...  identity a.ctor===Gen: false
[new] ctor.name: Gen       noext: GenNoExtends       ...  identity a.ctor===Gen: false
[node] ctor.name: Gen      noext: GenNoExtends       ...  identity a.ctor===Gen: true

The name is fixed; everything else — Gen.name, notypeargs, [object Object], the error subclass triple — is unchanged and already correct, and the identity divergence is unchanged in both arms exactly as you disclosed.

Reusing class_display_names is the right mechanism: it already exists for #5592's uniquified class-expression bindings, string_pool.rs already prefers it over the registration key, and reading the origin's display name rather than its name is the detail that makes a specialization of an already-uniquified class report the JS name instead of the internal key. Two lines, no new concept.

Verified the unit test has a subject: commenting out module.class_display_names.insert(class_id, display_name) fails it with left: None, right: Some("Gen") and the message "reporting Gen$num leaks the mangling into constructor.name, TypeError text and stack frames". Unit as well as gap is right for the stated reason — the gap suite is tag-gated, so a regression there sits red for days (#5960).

The disclosure is the part I want to keep

after this change the two report the same NAME while remaining !==, which arguably makes the identity divergence harder to notice than it was

That is worth having said out loud, and it is the right call anyway: TypeScript erases type arguments, so Gen is the correct name, and a wrong name is not worth keeping as an accidental symptom-indicator for a different bug. But the trade is real and the follow-up issue is the right place for it.

The observation that Object.getPrototypeOf(a) === Gen.prototype is already true while a.constructor === Gen is false — so the prototype and constructor edges disagree with each other today — is the more useful half of that follow-up. It suggests the fix is narrower than "collapse the specializations".

The checklist from the issue was measured rather than assumed, including the asymmetry that made this easy to miss: Gen.name was already Gen while a.constructor.name was Gen$num. And the confirmation that HIR/codegen keys are untouched — --print-hir/--trace still show Gen$num — is what keeps diagnostics unambiguous.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

constructor.name of a generic-class instance reports the mangled specialization (Gen$num), not Gen

1 participant