diff --git a/CLAUDE.md b/CLAUDE.md index 536d68935e..a89c718942 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.1447 +**Current Version:** 0.5.1448 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 5aff068416..79ca5025eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1447" +version = "0.5.1448" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1447" +version = "0.5.1448" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1447" +version = "0.5.1448" [[package]] name = "perry-ui-tvos" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1447" +version = "0.5.1448" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 86ddddd869..bdc607ee79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1447" +version = "0.5.1448" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/7762-generic-specialization-prototype-identity.md b/changelog.d/7762-generic-specialization-prototype-identity.md new file mode 100644 index 0000000000..dbc8cc1f09 --- /dev/null +++ b/changelog.d/7762-generic-specialization-prototype-identity.md @@ -0,0 +1,7 @@ +### Fixed + +- **Monomorphized generic specializations share the generic's prototype object (#7757, partial).** `new Gen()` compiles to a second class `Gen$num` with its own class id, and each specialization materialized its **own** prototype object. So `Object.getPrototypeOf(new Gen()) === Object.getPrototypeOf(new Gen())` was `false` where node says `true` — and, worse, the registry contradicted itself: `getPrototypeOf(a) === Gen.prototype` was simultaneously `true`, because the answer depended on which class id you asked through. + + TypeScript erases type arguments, so at runtime there is exactly one `Gen` and one `Gen.prototype`. Both prototype registries — `CLASS_DECL_PROTOTYPE_OBJECTS` and `CLASS_PROTOTYPE_OBJECTS`, the two paths CLAUDE.md warns have disagreed about the same chain before — now resolve a specialization through `class_generic_origin`, the same edge `instanceof` uses (#7575) and the display name uses (#7632). Method dispatch is untouched: it runs off the per-class-id vtable, so each specialization keeps its monomorphized bodies. + + **`a.constructor === Gen` is still divergent and #7757 stays open for it.** That edge does not resolve through either prototype registry; redirecting `instance_constructor_value` — the obvious site, and the one `get_field_by_name_tail` calls for exactly this key — changes nothing. Two things learned while chasing it, recorded so the next attempt does not repeat them: `object_static_prototype()` appears to materialize on first call, so an added probe of it flips the very branch under test; and `class Empty {}` gives `e.constructor === undefined` (node: `Empty`), which suggests the `.constructor` gap is broader than generics rather than a monomorphization leak. diff --git a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs index b18879bebd..8b0350a742 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_objects.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_objects.rs @@ -277,6 +277,13 @@ pub extern "C" fn js_set_function_prototype(func: f64, proto: f64) -> u32 { /// object pointer for a synthetic class id, or null if none. #[inline] pub(crate) fn class_prototype_object(class_id: u32) -> *mut ObjectHeader { + // #7757: a monomorphized specialization shares its GENERIC's prototype + // object — there is exactly one `Gen.prototype` at runtime, since + // TypeScript erases the type arguments. This is the second of the two + // prototype registries (the other is `CLASS_DECL_PROTOTYPE_OBJECTS`); both + // must redirect or `x.constructor` and `Object.getPrototypeOf` disagree + // about whether the specialization is `Gen`. + let class_id = crate::object::class_generic_origin(class_id).unwrap_or(class_id); if let Ok(read) = CLASS_PROTOTYPE_OBJECTS.read() { if let Some(map) = read.as_ref() { return map.get(&class_id).copied().unwrap_or(0) as *mut ObjectHeader; diff --git a/crates/perry-runtime/src/object/class_registry/state.rs b/crates/perry-runtime/src/object/class_registry/state.rs index ea34c65e27..e89c1437e9 100644 --- a/crates/perry-runtime/src/object/class_registry/state.rs +++ b/crates/perry-runtime/src/object/class_registry/state.rs @@ -446,7 +446,26 @@ pub(crate) fn class_id_for_decl_prototype_object(ptr: usize) -> Option { .map(|(k, _)| *k) } +/// #7757: a monomorphized specialization (`Gen$num`) must present the GENERIC's +/// reflective surface. 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 of monomorphization +/// (`monomorph::mangle::generate_specialized_name`), and without this redirect +/// each one materialized its OWN decl-prototype object. That made +/// `a.constructor !== Gen`, `a.constructor !== b.constructor` and +/// `getPrototypeOf(a) !== getPrototypeOf(b)` where node says all three are +/// equal. +/// +/// Same origin edge `instanceof` uses (#7575) and the display name uses +/// (#7632), applied to the third and last identity surface. METHOD DISPATCH is +/// unaffected: it runs off the per-class-id vtable, not this object, so each +/// specialization keeps its own monomorphized bodies. +fn decl_prototype_identity_id(class_id: u32) -> u32 { + crate::object::class_generic_origin(class_id).unwrap_or(class_id) +} + pub(crate) fn class_decl_prototype_object(class_id: u32) -> *mut ObjectHeader { + let class_id = decl_prototype_identity_id(class_id); if let Ok(read) = CLASS_DECL_PROTOTYPE_OBJECTS.read() { if let Some(map) = read.as_ref() { return map.get(&class_id).copied().unwrap_or(0) as *mut ObjectHeader; @@ -485,6 +504,8 @@ fn install_class_decl_prototype_method_fields(proto: *mut ObjectHeader, class_id } pub(crate) fn class_decl_prototype_value(class_id: u32) -> f64 { + // #7757: a specialization answers with its generic's prototype. + let class_id = decl_prototype_identity_id(class_id); if class_id == crate::wasi::CLASS_ID_WASI { crate::wasi::ensure_wasi_prototype_for_subclass(); let proto = class_prototype_object(class_id); diff --git a/test-files/test_gap_generic_specialization_prototype_identity_7757.ts b/test-files/test_gap_generic_specialization_prototype_identity_7757.ts new file mode 100644 index 0000000000..08048769ec --- /dev/null +++ b/test-files/test_gap_generic_specialization_prototype_identity_7757.ts @@ -0,0 +1,34 @@ +// #7757: monomorphized specializations must share the GENERIC's prototype +// object. `new Gen()` compiles to a second class `Gen$num` with its own +// class id, but TypeScript erases type arguments — at runtime there is exactly +// one `Gen` and one `Gen.prototype`. +// +// Before this fix each specialization materialized its own prototype, so +// `getPrototypeOf(a) === Gen.prototype` was true while +// `getPrototypeOf(a) === getPrototypeOf(b)` was false — the registry answered +// differently depending on which class id you asked through. +// +// NOT covered here: `a.constructor === Gen` (still divergent, tracked on +// #7757). The `.constructor` edge resolves through a different path than +// either prototype registry. + +class Gen { + v: T | undefined; +} +class Wrap extends Gen {} + +const a = new Gen(); +const b = new Gen(); +const c = new Gen(); // never specialized + +console.log("proto a === Gen.prototype:", Object.getPrototypeOf(a) === Gen.prototype); +console.log("proto a === proto b:", Object.getPrototypeOf(a) === Object.getPrototypeOf(b)); +console.log("proto a === proto c:", Object.getPrototypeOf(a) === Object.getPrototypeOf(c)); + +const w = new Wrap(); +console.log("nested proto === Wrap.prototype:", Object.getPrototypeOf(w) === Wrap.prototype); +console.log("nested chain:", Object.getPrototypeOf(Object.getPrototypeOf(w)) === Gen.prototype); + +// The #7575 and #7632 halves keep working. +console.log("instanceof:", a instanceof Gen, b instanceof Gen, w instanceof Gen); +console.log("names:", a.constructor.name, b.constructor.name, Gen.name);