diff --git a/CLAUDE.md b/CLAUDE.md index 3285a52f83..28a4b2c5fb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1440 +**Current Version:** 0.5.1441 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 01e4cae7e7..fb3b5c59be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1440" +version = "0.5.1441" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1440" +version = "0.5.1441" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1440" +version = "0.5.1441" [[package]] name = "perry-ui-tvos" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1440" +version = "0.5.1441" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 281c4a6797..c9cab46c53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1440" +version = "0.5.1441" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/7756-generic-class-display-name.md b/changelog.d/7756-generic-class-display-name.md new file mode 100644 index 0000000000..ac208eb4e4 --- /dev/null +++ b/changelog.d/7756-generic-class-display-name.md @@ -0,0 +1,11 @@ +### Fixed + +- **`constructor.name` of a generic-class instance reported the mangled specialization (#7632).** `(new Gen()).constructor.name` said `Gen$num`; node says `Gen`. TypeScript erases type arguments, so the mangling `monomorph::mangle::generate_specialized_name` produces is Perry's business and must not reach a user-visible name. + + This is the display-name half of the leak whose `instanceof` half was fixed in #7575 / PR #7631. Monomorphization emits a second class (`Gen$num`) with its own class id, and the instance is stamped with that id — so every id-keyed user-visible surface reports the specialization. The fix 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. + + Only instantiation WITH type arguments was affected — `new Gen()` is never specialized. Measured against node 26.5.1, the affected surface was exactly `instance.constructor.name`: `Gen.name` read off the constructor binding, `Object.prototype.toString.call(...)`, and error-subclass `name`/`toString()` were already correct, and `instanceof` keeps working. + + **Not fixed, and pre-existing:** two specializations of one generic are still distinct constructor objects. `new Pair().constructor === new Pair().constructor` is `false` where node says `true`, as is `a.constructor === Gen`, and `Object.getPrototypeOf(a) === Object.getPrototypeOf(b)`. That is the monomorphization model rather than a name-registry bug, so it is filed separately rather than bundled here — but it is worth knowing that after this change the two report the same NAME while remaining `!==`. + + Coverage: a `perry-hir` unit test asserting the registration (`--lib`, so it runs per-PR rather than only at tag time), verified to fail when the registration is removed; plus `test-files/test_gap_generic_class_constructor_name_7632.ts`, byte-identical to node, covering nested generics, two specializations, error subclasses and the #7575 `instanceof` half. diff --git a/crates/perry-hir/src/monomorph/driver.rs b/crates/perry-hir/src/monomorph/driver.rs index 941ebc66af..a871d78771 100644 --- a/crates/perry-hir/src/monomorph/driver.rs +++ b/crates/perry-hir/src/monomorph/driver.rs @@ -12,6 +12,8 @@ pub fn monomorphize_module(module: &mut Module) { // Process work queues until empty let mut new_functions = Vec::new(); let mut new_classes = Vec::new(); + // #7632: (specialization class id, the JS-visible name of its origin). + let mut new_display_names: Vec<(crate::ClassId, String)> = Vec::new(); while !ctx.func_work_queue.is_empty() || !ctx.class_work_queue.is_empty() { // Process function specializations @@ -68,6 +70,27 @@ pub fn monomorphize_module(module: &mut Module) { // Continue with specialization even on constraint errors (for now) } let new_id = ctx.fresh_class_id(); + // #7632: `new Gen()` constructs `Gen$num`, and the + // instance's `constructor.name` reported that mangled name. + // The mangling is Perry's business — TypeScript erases type + // arguments, so node says `Gen` for every specialization. + // + // Register the ORIGIN's display name against the + // specialization's class id. `class_display_names` is the + // existing mechanism for exactly this (#5592 uses it when a + // class-expression binding is uniquified), and + // `codegen/string_pool.rs` already prefers it over the + // registration key when emitting the named-class constants. + // + // Read through 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. + let display_name = module + .class_display_names + .get(&original.id) + .cloned() + .unwrap_or_else(|| original.name.clone()); + new_display_names.push((new_id, display_name)); let specialized = specialize_class(original, &request.type_args, new_id); new_classes.push(specialized); } @@ -77,6 +100,10 @@ pub fn monomorphize_module(module: &mut Module) { // Add specialized functions and classes to the module module.functions.extend(new_functions); module.classes.extend(new_classes); + // #7632: deferred out of the loop above, which holds `module` immutably. + for (class_id, display_name) in new_display_names { + module.class_display_names.insert(class_id, display_name); + } // Update call sites to use specialized versions update_call_sites(module, &ctx); diff --git a/crates/perry-hir/src/monomorph/tests.rs b/crates/perry-hir/src/monomorph/tests.rs index 7210362d83..71b357be6f 100644 --- a/crates/perry-hir/src/monomorph/tests.rs +++ b/crates/perry-hir/src/monomorph/tests.rs @@ -932,3 +932,93 @@ fn assert_specialized_call(expr: &Expr, module: &Module, expected_name: &str) { "Type args should be cleared after monomorphization" ); } + +/// #7632: a monomorphized class must report the GENERIC's name to user code. +/// +/// `new Gen()` constructs `Gen$num` (`generate_specialized_name`), and +/// the instance carries that class's id — so `instance.constructor.name` said +/// `Gen$num` where node says `Gen`. TypeScript erases type arguments; the +/// mangling is Perry's business and must not reach a user-visible name. +/// +/// The fix registers the origin's display name against the specialization's +/// class id, which `codegen/string_pool.rs` already prefers over the +/// registration key when emitting named-class constants (the #5592 mechanism). +/// +/// Unit, not just gap: the gap suite is tag-gated, so a regression there sits +/// red for days (#5960). This runs on every PR. +#[test] +fn a_specialized_class_reports_the_generics_display_name() { + fn generic_class(id: u32, name: &str) -> Class { + Class { + id, + name: name.to_string(), + type_params: vec![TypeParam { + name: "T".to_string(), + constraint: None, + default: None, + }], + extends: None, + extends_name: None, + native_extends: None, + extends_expr: None, + heritage_lexically_shadowed: false, + fields: vec![], + constructor: None, + methods: vec![], + getters: vec![], + setters: vec![], + static_accessor_names: vec![], + static_accessor_fn_ids: vec![], + static_fields: vec![], + static_methods: vec![], + computed_members: vec![], + decorators: vec![], + is_exported: false, + aliases: vec![], + is_nested: false, + alloc_width_hint: 0, + specialized_from: None, + } + } + + let mut module = Module::new("test"); + module.classes.push(generic_class(1, "Gen")); + module.init.push(Stmt::Expr(Expr::New { + class_name: "Gen".to_string(), + args: vec![], + type_args: vec![Type::Number], + byte_offset: 0, + cap_args_appended: 0, + })); + + monomorphize_module(&mut module); + + let specialized = module + .classes + .iter() + .find(|c| c.name == "Gen$num") + .expect("`new Gen()` must produce a `Gen$num` specialization"); + assert_eq!( + specialized.specialized_from.as_deref(), + Some("Gen"), + "#7575's origin edge is the precondition for the display name" + ); + assert_eq!( + module + .class_display_names + .get(&specialized.id) + .map(String::as_str), + Some("Gen"), + "the specialization must present the GENERIC's name to user code — \ + reporting `Gen$num` leaks the mangling into `constructor.name`, \ + `TypeError` text and stack frames" + ); + // The generic itself is untouched: it never needed a display-name entry, + // and adding one for it would change nothing but could mask a regression + // in which the specialization's entry is written against the wrong id. + assert_eq!( + module.class_display_names.get(&1), + None, + "only the specialization gets an entry" + ); +} diff --git a/test-files/test_gap_generic_class_constructor_name_7632.ts b/test-files/test_gap_generic_class_constructor_name_7632.ts new file mode 100644 index 0000000000..067c9b1c9e --- /dev/null +++ b/test-files/test_gap_generic_class_constructor_name_7632.ts @@ -0,0 +1,44 @@ +// #7632: `(new Gen()).constructor.name` reported the MANGLED +// specialization name `Gen$num`. Perry monomorphizes `new Gen()` into a +// second class (`monomorph/mangle.rs`), and the instance carries that class's +// id — but TypeScript erases type arguments, so node reports `Gen` for every +// specialization. The mangling is an implementation detail and must not reach +// user-visible names. +// +// The other half of the same leak was `instanceof`, fixed in #7575 / PR #7631 +// by recording `Class::specialized_from`; this is the display-name half. + +class Base {} +class Gen extends Base {} +class GenNoExtends {} + +const a = new Gen(); +const b = new Gen(); // no type arguments: never specialized +const c = new GenNoExtends(); +console.log("instances:", a.constructor.name, b.constructor.name, c.constructor.name); + +// The constructor binding itself was already correct — it names the generic. +console.log("ctor:", Gen.name, GenNoExtends.name); + +// Two different specializations of one generic both report the generic's name. +class Pair { + v: T | undefined; +} +const p1 = new Pair(); +const p2 = new Pair(); +console.log("specializations:", p1.constructor.name, p2.constructor.name); + +// Nested/chained generics. +class Wrap extends Gen {} +const w = new Wrap(); +console.log("nested:", w.constructor.name, w instanceof Gen, w instanceof Base); + +// Surfaces worth pinning alongside the name (#7632's own checklist). +console.log("toString:", Object.prototype.toString.call(a)); +class MyErr extends Error {} +const e = new MyErr(); +e.message = "boom"; +console.log("error:", e.name, String(e), e instanceof Error); + +// The #7575 half must keep working. +console.log("instanceof:", a instanceof Gen, a instanceof Base, c instanceof GenNoExtends);