diff --git a/CLAUDE.md b/CLAUDE.md index d29ed8825d..2774741392 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.1558 +**Current Version:** 0.5.1559 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index f40f875f4f..6a4abfd6d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5690,7 +5690,7 @@ checksum = "1542e48011813fbdf3c075da4a4ed53ee93c816eef62e36eb5064a6fd2be10a5" [[package]] name = "perry" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "anyhow", "base64 0.22.1", @@ -5754,7 +5754,7 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "perry-dispatch", "serde", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-audio-miniaudio" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "cc", "libc", @@ -5771,7 +5771,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "aho-corasick", "anyhow", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "anyhow", "perry-hir", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "anyhow", "perry-hir", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "anyhow", "perry-dispatch", @@ -5814,7 +5814,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "anyhow", "perry-hir", @@ -5822,7 +5822,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "anyhow", "base64 0.22.1", @@ -5834,7 +5834,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "anyhow", "perry-hir", @@ -5842,7 +5842,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "anyhow", "async-trait", @@ -5870,14 +5870,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "serde", "serde_json", @@ -5885,7 +5885,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1558" +version = "0.5.1559" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "anyhow", "clap", @@ -5911,7 +5911,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "block2", "objc2", @@ -5921,7 +5921,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "argon2", "perry-ffi", @@ -5930,7 +5930,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "perry-ffi", "reqwest", @@ -5939,7 +5939,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "bcrypt", "perry-ffi", @@ -5947,7 +5947,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "perry-ffi", "rusqlite", @@ -5955,7 +5955,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "perry-ffi", "scraper", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "perry-ffi", "perry-runtime", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "chrono", "cron", @@ -5981,7 +5981,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "chrono", "perry-ffi", @@ -5989,7 +5989,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "perry-ffi", "rust_decimal", @@ -5997,7 +5997,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "perry-ffi", "serde_json", @@ -6005,7 +6005,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "perry-ffi", "rand 0.10.2", @@ -6013,7 +6013,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "perry-ffi", "perry-runtime", @@ -6021,14 +6021,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "bytes", "http-body-util", @@ -6046,7 +6046,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "bytes", "lazy_static", @@ -6059,7 +6059,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "base64 0.22.1", "bytes", @@ -6091,7 +6091,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "lazy_static", "perry-ffi", @@ -6101,7 +6101,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "base64 0.22.1", "jsonwebtoken", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "lru", "perry-ffi", @@ -6121,7 +6121,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "chrono", "perry-ffi", @@ -6129,7 +6129,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "bson", "futures-util", @@ -6141,7 +6141,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "chrono", "perry-ffi", @@ -6153,7 +6153,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "nanoid", "perry-ffi", @@ -6162,7 +6162,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "bytes", "perry-ffi", @@ -6177,7 +6177,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "const-oid 0.10.2", "der 0.8.1", @@ -6196,7 +6196,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "lettre", "perry-ffi", @@ -6206,7 +6206,7 @@ dependencies = [ [[package]] name = "perry-ext-parcel-watcher" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "notify", "perry-ffi", @@ -6218,7 +6218,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "perry-ffi", "printpdf", @@ -6226,7 +6226,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "perry-ffi", "sqlx", @@ -6235,7 +6235,7 @@ dependencies = [ [[package]] name = "perry-ext-qs" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "perry-ffi", "perry-runtime", @@ -6244,7 +6244,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "governor", "perry-ffi", @@ -6252,7 +6252,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "fast_image_resize", "image", @@ -6263,7 +6263,7 @@ dependencies = [ [[package]] name = "perry-ext-streams" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "lazy_static", "perry-ffi", @@ -6272,7 +6272,7 @@ dependencies = [ [[package]] name = "perry-ext-typescript" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "anyhow", "perry-ffi", @@ -6292,7 +6292,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "perry-ffi", "perry-runtime", @@ -6301,7 +6301,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "perry-ffi", "uuid", @@ -6309,7 +6309,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "perry-ffi", "perry-validation", @@ -6318,7 +6318,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "futures-util", "lazy_static", @@ -6331,7 +6331,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "brotli", "flate2", @@ -6341,7 +6341,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "dashmap 6.2.1", "once_cell", @@ -6351,7 +6351,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "anyhow", "perry-api-manifest", @@ -6372,11 +6372,11 @@ dependencies = [ [[package]] name = "perry-native-registration" -version = "0.5.1558" +version = "0.5.1559" [[package]] name = "perry-parser" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "anyhow", "perry-diagnostics", @@ -6390,7 +6390,7 @@ dependencies = [ [[package]] name = "perry-perex" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "perex", "regex", @@ -6398,7 +6398,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "ahash", "anyhow", @@ -6458,14 +6458,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6560,14 +6560,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "anyhow", "perry-hir", @@ -6576,7 +6576,7 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "perry-ffi", "perry-ui-model", @@ -6584,7 +6584,7 @@ dependencies = [ [[package]] name = "perry-ui-android" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "base64 0.22.1", "itoa", @@ -6602,7 +6602,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "rand 0.10.2", "serde", @@ -6612,7 +6612,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "base64 0.22.1", "cairo-rs 0.22.9", @@ -6635,7 +6635,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "base64 0.22.1", "block2", @@ -6652,7 +6652,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "base64 0.22.1", "block2", @@ -6669,7 +6669,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1558" +version = "0.5.1559" [[package]] name = "perry-ui-test" @@ -6680,11 +6680,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1558" +version = "0.5.1559" [[package]] name = "perry-ui-tvos" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "base64 0.22.1", "block2", @@ -6701,7 +6701,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "base64 0.22.1", "block2", @@ -6718,7 +6718,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "block2", "libc", @@ -6732,7 +6732,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "base64 0.22.1", "libc", @@ -6751,7 +6751,7 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "base64 0.22.1", "libc", @@ -6764,7 +6764,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "anyhow", "base64 0.22.1", @@ -6780,7 +6780,7 @@ dependencies = [ [[package]] name = "perry-validation" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "idna", "regex", @@ -6790,7 +6790,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1558" +version = "0.5.1559" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 33d27e8f9e..3dd88af770 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -338,7 +338,7 @@ codegen-units = 1 codegen-units = 1 [workspace.package] -version = "0.5.1558" +version = "0.5.1559" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/10197-namespace-spread-call-var.md b/changelog.d/10197-namespace-spread-call-var.md new file mode 100644 index 0000000000..faf4cbcc73 --- /dev/null +++ b/changelog.d/10197-namespace-spread-call-var.md @@ -0,0 +1,3 @@ +### Fixed + +- `ns.member(...spread)` on a `const`-bound rest-parameter export (`export const mergeAll = (...ctxs) => …`, e.g. effect's `Context.mergeAll(...contexts)` inside `Layer.mergeAll`) now calls the closure; the spread fast path treated the export's value getter as the function body and evaluated to the function instead (#10197). diff --git a/changelog.d/10203-traversal-feedback-hot-tls.md b/changelog.d/10203-traversal-feedback-hot-tls.md new file mode 100644 index 0000000000..a0a93a3d17 --- /dev/null +++ b/changelog.d/10203-traversal-feedback-hot-tls.md @@ -0,0 +1,3 @@ +### fix(json): route the traversal-feedback counters through the hot thread-local cache + +`json/traversal_feedback.rs` declared its two per-thread counters with a raw `thread_local!`, which the thread-local policy ratchet (`scripts/check_thread_locals.py`) rejects; they now use `crate::perry_thread_local!` like every other runtime declaration, so the per-parse read lands in the hot TLS cache instead of a `_tlv_get_addr` call. diff --git a/changelog.d/10210-static-getter-call-path.md b/changelog.d/10210-static-getter-call-path.md new file mode 100644 index 0000000000..30ed28f824 --- /dev/null +++ b/changelog.d/10210-static-getter-call-path.md @@ -0,0 +1,12 @@ +Fix method calls inside static getters of classes that extend a call expression +(effect v4 `class Svc extends Context.Service()(id) {}` and +OpenCode's `ConfigService.Service` wrapper). `this.of(x)` in such a getter threw +"of is not a function" although reading `this.of` returned the function: the +dynamic method-call dispatcher resolved only the static-method vtable for a +per-evaluation class object, and the class-ref read path looked for the +function-valued parent only on the receiver's own class. The class-object call +arm now resolves the name exactly as the read path does (static accessors, the +parent class object's statics, the ancestor function's swapped prototype) and +calls the closure with `this` bound to the receiver; string- and symbol-keyed +static reads walk the class chain for the function-valued ancestor. This +unblocks OpenCode v1.18.30's runtime bootstrap (#10210). diff --git a/crates/perry-codegen/src/expr/call_spread.rs b/crates/perry-codegen/src/expr/call_spread.rs index eb219ff084..314b950d37 100644 --- a/crates/perry-codegen/src/expr/call_spread.rs +++ b/crates/perry-codegen/src/expr/call_spread.rs @@ -431,7 +431,24 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { } else { ctx.imported_func_has_rest.contains(property) }; - if ctx.namespace_imports.contains(ns_name) && has_rest && declared_count == 1 { + // #10197: a `let`/`const`-bound export (`export const + // mergeAll = (...ctxs) => …`) has no callable + // `perry_fn___` — that symbol is its zero-arg + // VALUE getter — so bundling the spread into it returns + // the closure instead of calling it (effect's + // `Context.mergeAll(...contexts)` in `Layer.mergeAll`). + // Mirror `try_lower_namespace_member_call`: vars take the + // closure-callee path below, which reads the value and + // applies the spread through + // `js_closure_call_apply_with_spread`. + let is_var = ctx + .imported_vars + .contains(&crate::namespace_member_var_key(ns_name, property)); + if ctx.namespace_imports.contains(ns_name) + && has_rest + && declared_count == 1 + && !is_var + { let source_prefix_opt = ctx .namespace_member_prefixes .get(&(ns_name.clone(), property.clone())) diff --git a/crates/perry-runtime/src/json/traversal_feedback.rs b/crates/perry-runtime/src/json/traversal_feedback.rs index f3017b0305..55f3ea2d2f 100644 --- a/crates/perry-runtime/src/json/traversal_feedback.rs +++ b/crates/perry-runtime/src/json/traversal_feedback.rs @@ -40,9 +40,8 @@ const SCORE_MAX: u8 = 8; /// the evidence keeps being refreshed. const RESAMPLE_EVERY: u8 = 16; -// Two byte counters read once per parse: plain `thread_local!`, not the hot-TLS -// macro, and no heap pointer can live in either. -thread_local! { +// Two byte counters read once per parse. No heap pointer can live in either. +crate::perry_thread_local! { static SCORE: Cell = const { Cell::new(0) }; static EAGER_RUN: Cell = const { Cell::new(0) }; } diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs index 55910e1fd6..721af36d12 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs @@ -1461,8 +1461,13 @@ pub extern "C" fn js_object_get_field_by_name( // named static off the parent closure — its OWN props // (`Svc.key` → "Svc") plus, via the closure getter, its // static prototype (`Svc._op` → "Tag" on TagProto). + // #10210: the edge is keyed by the class that directly + // `extends `, which may be an ANCESTOR of this + // class (`class Flags extends ConfigTag {}` where + // `ConfigTag extends Context.Service()(id)`), so walk the + // parent chain like `super()` dispatch does. if let Some(closure_ptr) = - super::super::class_registry::class_parent_closure(class_id) + super::super::class_registry::parent_closure_in_chain(class_id) { let v = crate::closure::closure_get_dynamic_prop(closure_ptr, name); let vb = JSValue::from_bits(v.to_bits()); diff --git a/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs b/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs index 25f6984852..5b5a174680 100644 --- a/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs @@ -359,6 +359,45 @@ pub(super) unsafe fn dispatch_primitive( )); } } + // #10210: the name may resolve where the static-method vtable cannot + // see it — a static ACCESSOR whose getter returns a function + // (`static get layer()`), a static of a per-evaluation parent class + // object, or the swapped prototype of a FUNCTION-valued ancestor + // (effect v4 `Context.Service<..>()(id)` is `function KeyClass(){}` + // + `Object.setPrototypeOf(KeyClass, ServiceProto)`, and a static + // getter on a subclass calls `this.of(x)` with `this` = this class + // object). The read path already resolves all of these, so mirror + // the class-ref arm (#5437): read the property exactly as + // `const f = C.of; f.call(C, x)` would, then call it with `this` + // bound to the receiver. Only a closure is dispatched here; anything + // else keeps falling through to the generic scan and the normal + // not-a-function error. + if class_id != 0 && !method_name_ptr.is_null() && method_name_len > 0 { + let key = crate::string::js_string_from_bytes( + method_name_ptr as *const u8, + method_name_len as u32, + ); + let receiver = JSValue::from_bits(object_handle.get_nanbox_f64().to_bits()) + .as_pointer::(); + let method = js_object_get_field_by_name(receiver, key); + if method.is_pointer() + && crate::closure::is_closure_ptr(crate::value::js_nanbox_get_pointer( + f64::from_bits(method.bits()), + ) as usize) + { + let method = root_scope.root_nanbox_u64(method.bits()); + let receiver = object_handle.get_nanbox_f64(); + let bound = + crate::closure::clone_closure_rebind_this(method.get_nanbox_u64(), receiver); + let _this = ImplicitThisScope::bind(receiver); + let args = refreshed_args(); + return Some(crate::closure::js_native_call_value( + f64::from_bits(bound), + args.as_ptr(), + args.len(), + )); + } + } } // #5142: a promise can carry user-attached own expando methods. diff --git a/crates/perry-runtime/src/symbol/get.rs b/crates/perry-runtime/src/symbol/get.rs index 0762d90c02..1fa3794a4d 100644 --- a/crates/perry-runtime/src/symbol/get.rs +++ b/crates/perry-runtime/src/symbol/get.rs @@ -648,8 +648,9 @@ pub unsafe extern "C" fn js_object_get_symbol_property(obj_f64: f64, sym_f64: f6 // the parent closure — own symbol props plus, via the closure symbol // getter, its static prototype (`Svc[TagTypeId]`/`Svc[EffectTypeId]` // live on TagProto). Recurse into the closure-aware getter so its proto - // walk fires. - if let Some(closure_ptr) = crate::object::class_parent_closure(class_id) { + // walk fires. #10210: the edge may sit on an ancestor class — walk + // the chain. + if let Some(closure_ptr) = crate::object::parent_closure_in_chain(class_id) { let closure_f64 = f64::from_bits(crate::value::js_nanbox_pointer(closure_ptr as i64).to_bits()); let v = js_object_get_symbol_property(closure_f64, sym_f64); diff --git a/crates/perry/tests/issue_10210_static_getter_call.rs b/crates/perry/tests/issue_10210_static_getter_call.rs new file mode 100644 index 0000000000..f156f77a80 --- /dev/null +++ b/crates/perry/tests/issue_10210_static_getter_call.rs @@ -0,0 +1,147 @@ +//! Regression for #10210: a static getter on a class whose `extends` clause is a +//! call expression (effect v4's `Context.Service<..>()(id)` returns a plain +//! `function KeyClass(){}` whose `[[Prototype]]` was swapped with +//! `Object.setPrototypeOf`) must be able to call `this.of(x)` — the CALL path +//! has to resolve the name wherever the READ path does (static accessors, +//! per-evaluation parent class objects, and the function-valued ancestor's +//! swapped prototype), and the closure-parent edge must be found on an +//! ancestor of the receiver's class, not only on the class itself. + +use std::path::PathBuf; +use std::process::Command; +use std::sync::Once; + +fn perry_bin() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_perry")) +} + +fn workspace_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .canonicalize() + .expect("canonicalize workspace root") +} + +fn runtime_dir() -> PathBuf { + static BUILD_RUNTIME: Once = Once::new(); + BUILD_RUNTIME.call_once(|| { + let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); + let build = Command::new(cargo) + .current_dir(workspace_root()) + .arg("build") + .arg("-p") + .arg("perry-runtime-static") + .arg("-p") + .arg("perry-stdlib-static") + .output() + .expect("build static runtime archives"); + assert!( + build.status.success(), + "static runtime build failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&build.stdout), + String::from_utf8_lossy(&build.stderr) + ); + }); + let target = std::env::var_os("CARGO_TARGET_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| workspace_root().join("target")); + target.join("debug") +} + +const SOURCE: &str = r#" +const Proto: any = { of(self: any) { return self } } +const Key = function () { + function K() {} + Object.setPrototypeOf(K, Proto) + ;(K as any).isK = true + return function (key: string) { (K as any).key = key; return K } +} +const t = (name: string, f: () => any) => { + try { console.log(name, JSON.stringify(f())) } catch (e: any) { console.log(name, "THROW", e.message) } +} +// nested class + subclass, static getter calls this.of(x) +const mk1 = (id: string) => { class T extends (Key as any)()(id) { static get g() { return (this as any).of({ n: 1 }) } }; return T as any } +class S1 extends mk1("S1") {} +t("N1", () => S1.g) +// same receiver, read first then call +const mk2 = (id: string) => { class T extends (Key as any)()(id) { static get g() { const f = (this as any).of; return [typeof f, f.call(this, { n: 2 })] } }; return T as any } +class S2 extends mk2("S2") {} +t("N2", () => S2.g) +// alias `this` into a local first (opencode's `const tag = this`) +const mk4 = (id: string) => { class T extends (Key as any)()(id) { static get g() { const tag = this as any; return tag.of({ n: 4 }) } }; return T as any } +class S4 extends mk4("S4") {} +t("N4", () => S4.g) +// nested, no subclass +t("N5", () => mk1("T5").g) +// top-level + subclass +class T6 extends (Key as any)()("T6") { static get g() { return (this as any).of({ n: 6 }) } } +class S6 extends T6 {} +t("N6", () => S6.g) +// top-level, no subclass +t("N7", () => T6.g) +// the closure-parent edge sits on an ancestor: inherited reads on a grandchild +class T8 extends (Key as any)()("T8") {} +class S8 extends T8 {} +t("N8", () => [S8.key, (S8 as any).isK, typeof (S8 as any).of]) +// static METHOD with the same body +const mk10 = (id: string) => { class T extends (Key as any)()(id) { static m() { return (this as any).of({ n: 10 }) } }; return T as any } +class S10 extends mk10("S10") {} +t("N10", () => S10.m()) +// calling the function a static getter returns, on a class extending a call expression +const mk11 = (id: string) => { class T extends (Key as any)()(id) { static get g() { return () => 11 } }; return T as any } +t("M1", () => mk11("T11").g()) +// the closure returned by a static getter calls tag.of (config-service.ts shape) +const mk12 = (id: string) => { class T extends (Key as any)()(id) { static get layer() { const tag = this as any; return () => tag.of({ n: 12 }) } }; return T as any } +class S12 extends mk12("S12") {} +t("M2", () => { const thunk = S12.layer; return thunk() }) +"#; + +const EXPECTED: &str = "N1 {\"n\":1} +N2 [\"function\",{\"n\":2}] +N4 {\"n\":4} +N5 {\"n\":1} +N6 {\"n\":6} +N7 {\"n\":6} +N8 [\"T8\",true,\"function\"] +N10 {\"n\":10} +M1 11 +M2 {\"n\":12} +"; + +#[test] +fn static_getter_calls_this_of_on_call_expression_heritage() { + let dir = tempfile::tempdir().expect("tempdir"); + let entry = dir.path().join("main.ts"); + let output = dir.path().join("main_bin"); + std::fs::write(&entry, SOURCE).expect("write entry"); + + let compile = Command::new(perry_bin()) + .current_dir(dir.path()) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&output) + .arg("--no-cache") + .env("PERRY_NO_AUTO_OPTIMIZE", "1") + .env("PERRY_RUNTIME_DIR", runtime_dir()) + .output() + .expect("run perry compile"); + assert!( + compile.status.success(), + "perry compile failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&compile.stdout), + String::from_utf8_lossy(&compile.stderr) + ); + + let run = Command::new(&output) + .current_dir(dir.path()) + .output() + .expect("run compiled binary"); + assert!( + run.status.success(), + "compiled binary failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); + assert_eq!(String::from_utf8_lossy(&run.stdout), EXPECTED); +} diff --git a/crates/perry/tests/source_graph_export_regressions.rs b/crates/perry/tests/source_graph_export_regressions.rs index 12c85b80fe..c250983905 100644 --- a/crates/perry/tests/source_graph_export_regressions.rs +++ b/crates/perry/tests/source_graph_export_regressions.rs @@ -956,3 +956,5 @@ fn mixed_type_and_value_specifier_import_keeps_runtime_edge() { } #[path = "source_graph_export_regressions/issue_10160.rs"] mod issue_10160; +#[path = "source_graph_export_regressions/issue_10197.rs"] +mod issue_10197; diff --git a/crates/perry/tests/source_graph_export_regressions/issue_10197.rs b/crates/perry/tests/source_graph_export_regressions/issue_10197.rs new file mode 100644 index 0000000000..c6a9a66112 --- /dev/null +++ b/crates/perry/tests/source_graph_export_regressions/issue_10197.rs @@ -0,0 +1,41 @@ +//! `ns.member(...spread)` on a `const`-bound rest-parameter export must call +//! the closure, not evaluate to it (#10197). + +use super::{compile_and_run, write}; + +fn lib(dir: &std::path::Path) { + write( + dir, + "lib.ts", + "export const count = (...xs: any[]) => xs.length\n\ + export const mergeAll = (...ctxs: any[]) => {\n\ + \x20 const m = new Map()\n\ + \x20 for (let i = 0; i < ctxs.length; i++) ctxs[i].mapUnsafe.forEach((v: any, k: any) => m.set(k, v))\n\ + \x20 return { mapUnsafe: m }\n\ + }\n\ + export function countFn(...xs: any[]) { return xs.length }\n\ + export const mk = (k: string) => ({ mapUnsafe: new Map([[k, k.length]]) })\n\ + export const fill = (n: number, f: (i: number) => any) => { const out = new Array(n); for (let i = 0; i < n; i++) out[i] = f(i); return out }\n", + ); +} + +#[test] +fn namespace_member_spread_call_invokes_const_rest_exports() { + let dir = tempfile::tempdir().unwrap(); + lib(dir.path()); + write( + dir.path(), + "main.ts", + "import * as Lib from \"./lib\"\n\ + import { fill, mk } from \"./lib\"\n\ + const other = fill(3, (i) => mk(\"k\".repeat(i + 1)))\n\ + console.log(Lib.count(...other), Lib.count(0, ...other), Lib.countFn(...other), Lib.countFn(0, ...other))\n\ + console.log(Lib.mergeAll(...other).mapUnsafe.size, (Lib as any)[\"mergeAll\"](...other).mapUnsafe.size)\n\ + const inner = (context: any) => Lib.mergeAll(...(context as any))\n\ + console.log(inner(other).mapUnsafe.size, Lib.mk(...([\"zz\"] as [string])).mapUnsafe.size)\n", + ); + assert_eq!( + compile_and_run(dir.path(), "main.ts"), + "3 4 3 4\n3 3\n3 1\n" + ); +}