diff --git a/CLAUDE.md b/CLAUDE.md index 772f16009f..a8991d7496 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.1400 +**Current Version:** 0.5.1401 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index d0b104578d..55034c3605 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1400" +version = "0.5.1401" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1400" +version = "0.5.1401" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1400" +version = "0.5.1401" [[package]] name = "perry-ui-tvos" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1400" +version = "0.5.1401" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index eb35b33d7a..43911cd566 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1400" +version = "0.5.1401" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/7691-static-method-value-name-collision.md b/changelog.d/7691-static-method-value-name-collision.md new file mode 100644 index 0000000000..9b09597fbd --- /dev/null +++ b/changelog.d/7691-static-method-value-name-collision.md @@ -0,0 +1,12 @@ +Fixed a static method extracted as a value off its class (`const f = C.m; f(...)`) +resolving to the same-named *instance* method instead of the static (#7689). +`js_class_method_bind`'s method-identity canonicalization treated a constructor +class ref like an instance receiver and consulted only the instance vtable, so +any class declaring both `static m()` and `m()` handed out the prototype method; +invoked bare, it ran with an unconstructed `this`. This broke the `marked` npm +package outright — its `Lexer.lex`/`Parser.parse` are exactly this collision, and +every `marked.parse` threw `TypeError: Cannot read properties of undefined +(reading 'pedantic')`. Constructor refs now skip the instance-vtable canonical +path and dispatch statics-first at call time; `C.prototype.m` reads are +unchanged. Covered by a runtime unit test (verified to fail pre-fix) and +`test_gap_static_method_value_name_collision.ts`. diff --git a/crates/perry-runtime/src/object/native_module.rs b/crates/perry-runtime/src/object/native_module.rs index fac79dc8a3..f614a9f0bc 100644 --- a/crates/perry-runtime/src/object/native_module.rs +++ b/crates/perry-runtime/src/object/native_module.rs @@ -1051,7 +1051,22 @@ pub extern "C" fn js_class_method_bind( if let Ok(name) = unsafe { std::str::from_utf8(std::slice::from_raw_parts(method_name_ptr, method_name_len)) } { - if bound_native_method_length(name).is_none() { + // #7689: a CONSTRUCTOR class-ref receiver (`const f = C.m`) must + // never canonicalize to the INSTANCE vtable method of the same + // name — in JS `C.m` sees only statics (`class C { static lex(){} + // lex(){} }` has `C.lex` === the static; the instance `lex` lives + // on `C.prototype`). `class_id_from_method_receiver` treats a + // class ref like an instance, so marked's `const lexer2 = + // _Lexer.lex; lexer2(src, opt)` extracted the instance `lex`, + // whose bare invocation read `this.options` off an unconstructed + // receiver. Fall through to `build_bound_method_closure`: its + // call-time dispatch (`js_native_call_method`'s 0x7FFE arm) + // resolves statics-first for constructor refs. PROTOTYPE refs + // (`C.prototype.m`) keep the canonical path — the instance method + // is exactly what they name. + let receiver_is_constructor_ref = + class_ref_id(instance).is_some() && class_prototype_ref_id(instance).is_none(); + if !receiver_is_constructor_ref && bound_native_method_length(name).is_none() { if let Some(class_id) = class_id_from_method_receiver(instance) { if let Some(owner) = super::class_registry::method_owner_class_id(class_id, name) diff --git a/crates/perry-runtime/src/object/tests.rs b/crates/perry-runtime/src/object/tests.rs index f5ea104a9a..f89562ac9a 100644 --- a/crates/perry-runtime/src/object/tests.rs +++ b/crates/perry-runtime/src/object/tests.rs @@ -1424,3 +1424,71 @@ fn array_receiver_is_never_read_as_a_class_id() { "a real class instance must still resolve to its class id" ); } + +/// #7689: `const f = C.m; f(...)` — a method value read off a CONSTRUCTOR +/// class ref — must invoke the STATIC method when the class declares both a +/// static and an instance method of the same name. +/// +/// `js_class_method_bind`'s #446 method-identity canonicalization resolved +/// the name against the INSTANCE vtable (`class_id_from_method_receiver` +/// treats a class ref like an instance receiver), so the extracted value was +/// the prototype method. marked's `Lexer` has exactly this collision +/// (`static lex` + instance `lex`): `const lexer2 = _Lexer.lex; +/// lexer2(src, opt)` ran the instance `lex` with no constructed receiver and +/// every `marked.parse` threw "Cannot read properties of undefined (reading +/// 'pedantic')". +#[test] +fn constructor_ref_method_value_resolves_static_over_instance_method() { + // Unique id so the process-global registries don't collide with other tests. + const CLASS_ID: u32 = 0x7689; + const NAME: &[u8] = b"lex"; + + extern "C" fn static_lex_7689() -> f64 { + 42.0 + } + extern "C" fn instance_lex_7689(_this: f64) -> f64 { + 7.0 + } + + unsafe { + super::class_registry::js_register_class_method( + CLASS_ID as i64, + NAME.as_ptr(), + NAME.len() as i64, + instance_lex_7689 as usize as i64, + 0, + 0, + 0, + ); + super::class_registry::js_register_class_static_method( + CLASS_ID as i64, + NAME.as_ptr(), + NAME.len() as i64, + static_lex_7689 as usize as i64, + 0, + 0, + ); + } + + let class_ref = super::native_module::class_constructor_ref_value(CLASS_ID); + let bound = super::native_module::js_class_method_bind(class_ref, NAME.as_ptr(), NAME.len()); + let result = unsafe { crate::closure::js_native_call_value(bound, std::ptr::null(), 0) }; + assert_eq!( + result, 42.0, + "a method value extracted off the CONSTRUCTOR ref must dispatch the \ + static `lex`, not the same-named instance method" + ); + + // The guard must not over-narrow: the PROTOTYPE ref names the instance + // method, and an extracted `C.prototype.lex` must keep resolving it. + let proto_ref = super::native_module::class_prototype_ref_value(CLASS_ID); + let bound_proto = + super::native_module::js_class_method_bind(proto_ref, NAME.as_ptr(), NAME.len()); + let result_proto = + unsafe { crate::closure::js_native_call_value(bound_proto, std::ptr::null(), 0) }; + assert_eq!( + result_proto, 7.0, + "a method value extracted off the PROTOTYPE ref must still dispatch \ + the instance `lex`" + ); +} diff --git a/test-files/test_gap_static_method_value_name_collision.ts b/test-files/test_gap_static_method_value_name_collision.ts new file mode 100644 index 0000000000..519c7b7935 --- /dev/null +++ b/test-files/test_gap_static_method_value_name_collision.ts @@ -0,0 +1,63 @@ +// #7689: a static method extracted as a VALUE off the constructor must stay +// the static when an instance method shares its name (marked's Lexer.lex / +// Parser.parse shape: `const lexer2 = _Lexer.lex; lexer2(src, opt)`). +const defaults: any = { pedantic: false, gfm: true }; + +const Lexer = class __Lexer { + options: any; + constructor(o?: any) { + this.options = o || defaults; + } + static lex(src: string, o?: any) { + const l = new (__Lexer as any)(o); + return l.lex(src); + } + static lexInline(src: string, o?: any) { + const l = new (__Lexer as any)(o); + return "inline:" + l.lex(src); + } + lex(src: string): string { + return this.blockTokens(src); + } + blockTokens(src: string): string { + if (this.options.pedantic) return "PED"; + return "ok:" + src.length; + } +}; + +// Extracted unbound through a ternary, exactly like marked's parseMarkdown. +function run(blockType: boolean, opt: any): string { + const lexer2 = blockType ? (Lexer as any).lex : (Lexer as any).lexInline; + return lexer2("# hi", opt); +} +console.log(run(true, { ...defaults })); +console.log(run(false, { ...defaults })); + +// Plain extraction without the ternary. +const f = (Lexer as any).lex; +console.log(f("# hello", { ...defaults })); + +// Class DECLARATION form of the same collision. +class Parser { + options: any; + constructor(o?: any) { + this.options = o || defaults; + } + static parse(tokens: string[], o?: any) { + const p = new Parser(o); + return p.parse(tokens); + } + parse(tokens: string[]): string { + return "parsed:" + tokens.length + ":" + String(this.options.gfm); + } +} +const g = (Parser as any).parse; +console.log(g(["a", "b"], { ...defaults })); + +// Direct calls on the class keep working. +console.log((Lexer as any).lex("# direct", { ...defaults })); +console.log(Parser.parse(["x"], { ...defaults })); + +// The prototype ref still names the INSTANCE method. +const pm = (Parser.prototype as any).parse; +console.log(typeof pm);