From 185e87b44dd38b817f8d07a10305a2f7ad2e7b72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 6 Aug 2026 17:38:53 +0200 Subject: [PATCH 1/7] perf(array): copy an ordinary dense array's spread instead of driving the iterator protocol (#7533) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `[...arr]` ran the full ECMA-262 GetIterator + per-element `.next()` protocol even for a plain dense array. On the `object_deep_clone` app-pattern kernel — the worst row in the public artifact at 37.5x bun — a symbolicated profile puts 90.45% of the whole process inside `array_from_spread_value`, and the identical copy through `Array.from`'s memcpy tail is 66x cheaper (0.09 s vs 5.93 s at N=500,000). Two structural costs, neither observable for an ordinary array: `@@iterator` resolves through the by-name prototype tower and builds a named bound closure (30.89% inclusive), and every `.next()` allocates five heap objects in `build_iter_result` (the `{value, done}` object, its two key strings, its keys array) plus the iterator object once per spread — ~25 allocations for a 3-element spread. `dense_spread_source` proves ordinariness (real GC_TYPE_ARRAY via `try_read_gc_header`, which rejects the handle band and the header-less small-buffer slab without a deref; not exotic; `Array.prototype[Symbol.iterator]` unmodified; no own `[Symbol.iterator]`) and `dense_spread_copy` copies the elements, normalising TAG_HOLE to undefined. Anything unproven falls through to the unchanged protocol. The own-symbol guard is a new non-invoking `symbol::has_own_symbol_property`: `own_symbol_property` answers by reading, which calls a user getter, and the slow path reads the property again — a getter must observe exactly one call. Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- .../7534-dense-array-spread-fast-path.md | 86 ++++++++ crates/perry-runtime/src/array/flat_clone.rs | 100 ++++++++++ crates/perry-runtime/src/array/iterator.rs | 18 ++ crates/perry-runtime/src/array/mod.rs | 4 +- .../src/array/spread_dense_tests.rs | 186 ++++++++++++++++++ crates/perry-runtime/src/symbol.rs | 2 +- crates/perry-runtime/src/symbol/get.rs | 29 +++ 7 files changed, 423 insertions(+), 2 deletions(-) create mode 100644 changelog.d/7534-dense-array-spread-fast-path.md create mode 100644 crates/perry-runtime/src/array/spread_dense_tests.rs diff --git a/changelog.d/7534-dense-array-spread-fast-path.md b/changelog.d/7534-dense-array-spread-fast-path.md new file mode 100644 index 0000000000..0bd8a6377b --- /dev/null +++ b/changelog.d/7534-dense-array-spread-fast-path.md @@ -0,0 +1,86 @@ +### Performance + +**`[...arr]` on an ordinary dense array is now an element copy, not a full +iterator protocol (#7533).** `object_deep_clone` was the worst row in the public +benchmark artifact by a wide margin — 657 ms against bun's 17.5 ms, **37.5×**, +where the next worst row is 11.4×. A symbolicated profile on the pinned quiet +host says the whole gap is one line of the kernel: + +```ts +tags: [...o.meta.tags], // a 3-element string array +``` + +**90.45% of the entire process is inside `array_from_spread_value`.** The same +copy written as `Array.from(o.meta.tags)` costs 0.09 s where the spread costs +5.93 s — **66×** for an identical result — because `Array.from` reaches +`js_array_clone`'s memcpy tail and the spread does not. + +Decomposition at N=500 000 (best-of-3 wall, quiet M1 mini; `dc_*` probes isolate +one construct each): + +| probe | best | isolates | +|---|--:|---| +| `dc_full` (the kernel) | 6.20 s | everything | +| **`dc_spread`** | **5.93 s** | **just `[...o.meta.tags]`** | +| `dc_map` | 0.32 s | `.map(x => ({…}))` | +| `dc_lit` | 0.15 s | the two object literals | +| `dc_slice` | 0.12 s | `tags.slice()` | +| `dc_arrfrom` | 0.09 s | `Array.from(tags)` | +| `dc_read` | 0.05 s | property reads only (loop floor) | + +Inside the spread (4665 leaf samples, `PERRY_DEBUG_SYMBOLS=1`): + +| stage | inclusive | +|---|--:| +| `array_from_spread_value` | **95.80%** | +| ├ `js_object_get_symbol_property` — resolving `@@iterator` | 30.89% | +| ├ `js_iterator_to_array` — the `.next()` drain | 34.34% | +| │ ├ `js_native_call_method` → `dispatch_array_iterator_method` | 16.38% | +| │ ├ `js_object_get_field_by_name` (`.value` / `.done`) | 6.95% | +| │ └ `build_iter_result` | 4.14% | +| └ classification probes + `RuntimeHandleScope` | ~13% + ~5% | +| cross-cutting `_tlv_get_addr` | ~17% | + +Two things are structurally wrong there, and neither is observable for an +ordinary array. `@@iterator` resolves through the **by-name** prototype tower — +`js_object_get_field_by_name_f64` → `get_field_by_name_object_tail` → +`array_prototype_property_value` → recursion → `default_object_prototype_ +property_value` → `fetch_subclass_handle_id` — and then builds a *named bound +closure* for the method it found. And every `.next()` allocates **five** heap +objects in `build_iter_result`: the `{ value, done }` object, its two key +strings, its keys array, plus the iterator object itself once per spread. A +3-element `[...tags]` therefore costs ~25 allocations where bun does one +allocation and a 24-byte `memcpy`. + +`array_from_spread_value` now takes a dense fast path first. +`dense_spread_source` proves the array is ordinary — real `GC_TYPE_ARRAY` via +`addr_class::try_read_gc_header` (which rejects the handle band and the +header-less small-buffer slab *without* dereferencing), not exotic per +`array_iteration_is_exotic`, `Array.prototype[Symbol.iterator]` unmodified, and +no own `[Symbol.iterator]` shadowing it — and `dense_spread_copy` then copies the +elements, normalising `TAG_HOLE` to `undefined` (the one place a raw copy and the +drain disagree: `[...[1, , 3]]` must stay `[1, undefined, 3]`). Anything it +cannot prove falls through to the unchanged protocol. + +The own-symbol probe is a new **non-invoking** `symbol::has_own_symbol_property` +rather than `own_symbol_property`: the latter answers by *reading*, which calls a +user getter, and the slow path it falls back to reads the property again — a +getter must observe exactly one call. + +### Fixed + +Nothing behavioural. `crates/perry-runtime/src/array/spread_dense_tests.rs` pins +`dense_spread_source`'s **verdict** in every case, not only the resulting +elements: the slow path is a correct fallback, so a test comparing elements alone +would stay green if the fast path silently stopped applying — CLAUDE.md's fourth +way a gate can be unable to fail. + +### Notes + +Two `[...arr]` divergences from node were found while building the semantics +matrix for this change. **Both predate this work and are unchanged by it** +(verified byte-identical at `f06270d06`, before the #7495/#7516/#7527 rooting +stack): `[...MyArr.from([1,2,3])]` on a `class MyArr extends Array` throws +`value is not iterable`, and a replaced `Array.prototype[Symbol.iterator]` is +ignored by spread (`[...[1,2,3]]` yields `[1,2,3]` where node yields the patched +iterator's output). Filed separately rather than folded in here. diff --git a/crates/perry-runtime/src/array/flat_clone.rs b/crates/perry-runtime/src/array/flat_clone.rs index 48990bd25a..880f0e7608 100644 --- a/crates/perry-runtime/src/array/flat_clone.rs +++ b/crates/perry-runtime/src/array/flat_clone.rs @@ -25,6 +25,106 @@ unsafe fn receiver_gc_type(ptr: *const ArrayHeader) -> u8 { (*gc_header).obj_type } +/// Is `value` an ordinary dense Array whose `[...value]` is *observably* a +/// straight element copy — i.e. an iteration nobody can intercept? +/// +/// Spread on a plain array is by far the most common spread in real code, and +/// per ECMA-262 it is `GetIterator` → `%ArrayIteratorPrototype%.next()` per +/// element. Perry implements that literally, which is why `[...tags]` on a +/// 3-element array cost ~66x what `Array.from(tags)` cost: the protocol resolves +/// `@@iterator` through the by-name prototype tower, builds a bound closure, +/// allocates an iterator object, and then allocates a fresh `{ value, done }` +/// result object (plus its two key strings and its keys array — five heap +/// allocations) for every element AND for the terminating step. None of that is +/// observable when the array is ordinary, so this predicate proves ordinariness +/// and lets the caller memcpy instead (#7533). +/// +/// Every gate rejects a way the copy could differ from the drain: +/// - `try_read_gc_header` + `GC_TYPE_ARRAY`: a real dense array, not a +/// Set/Map/Buffer/TypedArray/lazy array (each has its own `obj_type`), not a +/// proxy or native handle (rejected by band, without a deref), and not a +/// small-buffer slab allocation (which carries no `GcHeader` at all). A +/// `class X extends Array` instance is object-backed (`GC_TYPE_OBJECT`), so +/// it is excluded here and keeps its snapshot path. +/// - `array_iteration_is_exotic`: no per-index accessor descriptors, no +/// `Array.prototype` / `Object.prototype` index properties shadowing the +/// dense slots, and no live indices past the dense backing store — the three +/// cases where `arr[i]` is not the raw slot. +/// - `array_proto_iterator_modified`: user code replaced or deleted +/// `Array.prototype[Symbol.iterator]`, so the builtin walk is no longer what +/// a spread must run. +/// - `has_own_symbol_property`: the instance carries its OWN `[Symbol.iterator]`, +/// which shadows the prototype's. Existence is probed WITHOUT invoking an +/// accessor, so falling through to the slow path calls a user getter exactly +/// once, as the spec requires. +pub(crate) fn dense_spread_source(value: f64) -> Option<*const ArrayHeader> { + let raw = crate::value::js_nanbox_get_pointer(value) as usize; + let header = unsafe { crate::value::addr_class::try_read_gc_header(raw)? }; + if header.obj_type != crate::gc::GC_TYPE_ARRAY { + return None; + } + let arr = raw as *const ArrayHeader; + if crate::array::array_iteration_is_exotic(arr) { + return None; + } + if crate::array::array_proto_iterator_modified() { + return None; + } + let iter_sym = crate::symbol::well_known_symbol("iterator"); + if iter_sym.is_null() { + return None; + } + let sym_value = f64::from_bits(crate::value::JSValue::pointer(iter_sym as *const u8).bits()); + if unsafe { crate::symbol::has_own_symbol_property(value, sym_value) } { + return None; + } + Some(arr) +} + +/// Element-copy an array [`dense_spread_source`] has already proven ordinary. +/// +/// `value` is the NaN-boxed receiver rather than a raw pointer because +/// `js_array_alloc` below is a collection point: pre-#7497 the sibling +/// `js_array_clone` derived its source elements from the pre-collection address +/// and memcpy'd retired from-space. Root first, allocate, then re-read BOTH +/// addresses from their handles. +/// +/// Holes are the one place a raw copy and the iterator drain disagree: the drain +/// reads `arr[i]`, which yields `undefined` for a hole, while the slot itself +/// holds `TAG_HOLE`. Normalize on the way out so `[...[1, , 3]]` stays +/// `[1, undefined, 3]` and `1 in [...[1, , 3]]` stays `true`. +pub(crate) fn dense_spread_copy(value: f64) -> *mut ArrayHeader { + let scope = crate::gc::RuntimeHandleScope::new(); + let src_h = scope.root_nanbox_f64(value); + unsafe { + let len = (*(crate::value::js_nanbox_get_pointer(src_h.get_nanbox_f64()) + as *const ArrayHeader)) + .length; + let result_h = + scope.root_nanbox_f64(crate::value::js_nanbox_pointer(js_array_alloc(len) as i64)); + let src = crate::value::js_nanbox_get_pointer(src_h.get_nanbox_f64()) as *const ArrayHeader; + let result = + crate::value::js_nanbox_get_pointer(result_h.get_nanbox_f64()) as *mut ArrayHeader; + if len > 0 { + let src_elements = + (src as *const u8).add(std::mem::size_of::()) as *const u64; + let dst_elements = + (result as *mut u8).add(std::mem::size_of::()) as *mut u64; + // GC_STORE_AUDIT(BARRIERED): bulk copy into an unpublished array, + // followed by the exact layout/barrier rebuild below. + ptr::copy_nonoverlapping(src_elements, dst_elements, len as usize); + for i in 0..len as usize { + if ptr::read(dst_elements.add(i)) == crate::value::TAG_HOLE { + ptr::write(dst_elements.add(i), crate::value::TAG_UNDEFINED); + } + } + (*result).length = len; + rebuild_array_layout_exact(result); + } + result + } +} + /// Return a real `ArrayHeader` only when `value` satisfies ECMAScript's /// `IsArray` check. This unwraps proxy targets, rejects every other /// `POINTER_TAG` heap object by its GC type, and materializes lazy arrays diff --git a/crates/perry-runtime/src/array/iterator.rs b/crates/perry-runtime/src/array/iterator.rs index a719fd451a..0a09faceeb 100644 --- a/crates/perry-runtime/src/array/iterator.rs +++ b/crates/perry-runtime/src/array/iterator.rs @@ -762,6 +762,24 @@ pub(crate) fn array_from_spread_value(value: f64) -> *mut ArrayHeader { if raw_ptr() == 0 { throw_not_iterable(value()); } + + // #7533: the overwhelmingly common spread — an ordinary dense array — is a + // straight element copy that nobody can observe as anything else. Take it + // before the classification probes and long before the `@@iterator` walk + // below: on the `object_deep_clone` app-pattern kernel that walk plus the + // `.next()` drain it feeds was 90% of the whole process, and the identical + // copy through `Array.from`'s memcpy was ~66x cheaper. + // + // `dense_spread_source` proves ordinariness (see its doc comment for each + // gate); anything it cannot prove falls through to the unchanged protocol. + // It runs before `entries_array_for_small_handle_id` / `is_registered_buffer` + // only because it never dereferences an unvalidated address itself — + // `try_read_gc_header` rejects the handle band and the header-less + // small-buffer slab without touching memory. + if crate::array::dense_spread_source(value()).is_some() { + return crate::array::dense_spread_copy(value()); + } + if let Some(entries) = entries_array_for_small_handle_id(raw_ptr() as i64) { return entries; } diff --git a/crates/perry-runtime/src/array/mod.rs b/crates/perry-runtime/src/array/mod.rs index a2d0d0b887..d616e27783 100644 --- a/crates/perry-runtime/src/array/mod.rs +++ b/crates/perry-runtime/src/array/mod.rs @@ -24,6 +24,8 @@ mod species; mod splice_slice; mod subclass; +#[cfg(test)] +mod spread_dense_tests; #[cfg(test)] mod tests; @@ -172,7 +174,7 @@ pub use self::splice_slice::{ pub(crate) use self::alloc::array_length_from_property_value_or_throw; pub(crate) use self::alloc::{js_array_from_arraylike, js_array_from_string_codepoints}; -pub(crate) use self::flat_clone::flattenable_array_ptr; +pub(crate) use self::flat_clone::{dense_spread_copy, dense_spread_source, flattenable_array_ptr}; pub(crate) use self::header::{ array_byte_size, array_is_frozen, array_is_sealed_or_no_extend, array_named_property_delete, array_named_property_get, array_named_property_get_by_name, array_named_property_has, diff --git a/crates/perry-runtime/src/array/spread_dense_tests.rs b/crates/perry-runtime/src/array/spread_dense_tests.rs new file mode 100644 index 0000000000..23f0b2d8d7 --- /dev/null +++ b/crates/perry-runtime/src/array/spread_dense_tests.rs @@ -0,0 +1,186 @@ +//! `[...arr]` dense fast-path unit tests (#7533). +//! +//! These assert THE SUBJECT, not just the answer. `dense_spread_copy` produces +//! the same array the iterator drain would, so a test that only compared +//! elements would still pass if the fast path silently stopped applying — the +//! slow path is a correct fallback, which is exactly what makes a perf gate here +//! able to be green while measuring nothing (CLAUDE.md, "four ways a gate can be +//! unable to fail", case 4). So every case pins `dense_spread_source`'s verdict +//! as well: `is_some()` where the copy must be taken, `is_none()` where the +//! protocol must still run. + +use super::*; +use crate::value::{JSValue, TAG_HOLE, TAG_UNDEFINED}; + +fn boxed(arr: *mut ArrayHeader) -> f64 { + crate::value::js_nanbox_pointer(arr as i64) +} + +fn dense(values: &[f64]) -> *mut ArrayHeader { + let arr = js_array_alloc(values.len() as u32); + let mut cur = arr; + for v in values { + cur = js_array_push_f64(cur, *v); + } + cur +} + +unsafe fn slot_bits(arr: *const ArrayHeader, index: usize) -> u64 { + let elements = (arr as *const u8).add(std::mem::size_of::()) as *const u64; + std::ptr::read(elements.add(index)) +} + +fn iterator_symbol_value() -> f64 { + let sym = crate::symbol::well_known_symbol("iterator"); + assert!(!sym.is_null(), "well-known @@iterator must exist"); + f64::from_bits(JSValue::pointer(sym as *const u8).bits()) +} + +#[test] +fn plain_dense_array_takes_the_fast_path_and_copies_every_element() { + let src = dense(&[1.0, 2.0, 3.0]); + let value = boxed(src); + assert!( + dense_spread_source(value).is_some(), + "an ordinary dense array is exactly what the fast path exists for" + ); + let copy = dense_spread_copy(value); + assert_ne!( + copy as usize, src as usize, + "spread must produce a new array" + ); + unsafe { + assert_eq!((*copy).length, 3); + for i in 0..3 { + assert_eq!(slot_bits(copy, i), slot_bits(src, i)); + } + } +} + +#[test] +fn the_copy_is_independent_of_its_source() { + let src = dense(&[1.0, 2.0, 3.0]); + let copy = dense_spread_copy(boxed(src)); + let _ = js_array_push_f64(copy, 99.0); + unsafe { + assert_eq!( + (*src).length, + 3, + "appending to the copy must not grow the source" + ); + assert_eq!((*copy).length, 4); + } +} + +#[test] +fn an_empty_array_copies_to_an_empty_array() { + let src = js_array_alloc(0); + assert!(dense_spread_source(boxed(src)).is_some()); + let copy = dense_spread_copy(boxed(src)); + unsafe { assert_eq!((*copy).length, 0) }; +} + +#[test] +fn holes_become_undefined_not_hole_slots() { + // The drain reads `arr[i]`, which is `undefined` for a hole; a raw memcpy + // would hand the caller a TAG_HOLE slot, and `1 in [...[1, , 3]]` would flip + // from true to false. This is the one place copy and drain disagree. + let src = js_array_alloc(3); + let mut cur = js_array_push_f64(src, 1.0); + cur = js_array_push_hole(cur); + cur = js_array_push_f64(cur, 3.0); + unsafe { + assert_eq!( + slot_bits(cur, 1), + TAG_HOLE, + "probe must really plant a hole" + ) + }; + + assert!(dense_spread_source(boxed(cur)).is_some()); + let copy = dense_spread_copy(boxed(cur)); + unsafe { + assert_eq!((*copy).length, 3); + assert_eq!(slot_bits(copy, 0), 1.0f64.to_bits()); + assert_eq!(slot_bits(copy, 1), TAG_UNDEFINED); + assert_eq!(slot_bits(copy, 2), 3.0f64.to_bits()); + } +} + +#[test] +fn an_own_symbol_iterator_sends_the_spread_back_to_the_protocol() { + let src = dense(&[1.0, 2.0, 3.0]); + let value = boxed(src); + assert!( + dense_spread_source(value).is_some(), + "must be eligible BEFORE the shadowing install, or this test proves nothing" + ); + let sym = iterator_symbol_value(); + unsafe { + crate::symbol::js_object_set_symbol_property(value, sym, 1.0); + } + assert!( + dense_spread_source(value).is_none(), + "an own [Symbol.iterator] shadows the builtin walk — the copy is not equivalent" + ); +} + +#[test] +fn existence_probe_does_not_invoke_an_own_symbol_accessor() { + // `own_symbol_property` answers by READING, which calls a user getter. The + // guard must not: the slow path it falls back to reads the property again, + // and a getter called twice is observable. + let src = dense(&[1.0]); + let value = boxed(src); + let sym = iterator_symbol_value(); + unsafe { + assert!( + !crate::symbol::has_own_symbol_property(value, sym), + "no own @@iterator yet" + ); + crate::symbol::js_object_set_symbol_property(value, sym, 7.0); + assert!( + crate::symbol::has_own_symbol_property(value, sym), + "existence must be visible without invoking anything" + ); + } +} + +#[test] +fn a_non_array_receiver_is_never_eligible() { + // Every one of these has its own `obj_type`, and each has a spread meaning + // the element copy would get wrong. + let obj = crate::object::js_object_alloc(0, 2); + assert!(dense_spread_source(crate::value::js_nanbox_pointer(obj as i64)).is_none()); + + let s = crate::string::js_string_from_bytes(b"abc".as_ptr(), 3); + assert!(dense_spread_source(f64::from_bits(JSValue::string_ptr(s).bits())).is_none()); + + assert!(dense_spread_source(f64::from_bits(TAG_UNDEFINED)).is_none()); + assert!(dense_spread_source(f64::from_bits(crate::value::TAG_NULL)).is_none()); + assert!(dense_spread_source(42.0).is_none()); +} + +#[test] +fn a_handle_band_id_is_rejected_without_dereferencing_it() { + // Web-Fetch / stream / timer ids are NaN-boxed POINTER values in the handle + // band. Dereferencing `id - 8` as a GcHeader is a SIGSEGV (#7526), so the + // rejection has to come from the band test, not from reading the header. + for id in [1i64, 0x1000, 0x40000, 0xE0000] { + assert!( + dense_spread_source(crate::value::js_nanbox_pointer(id)).is_none(), + "handle id {id:#x} must be rejected by band" + ); + } +} + +#[test] +fn a_sparse_array_whose_length_outruns_its_storage_is_not_eligible() { + // Live indices past the dense backing store live in the named-property map, + // which the element copy never reads — `array_iteration_is_exotic`'s third + // arm. Without this gate `[...sparse]` would drop them. + let src = dense(&[1.0, 2.0]); + assert!(dense_spread_source(boxed(src)).is_some()); + unsafe { (*src).length = (*src).capacity + 1 }; + assert!(dense_spread_source(boxed(src)).is_none()); +} diff --git a/crates/perry-runtime/src/symbol.rs b/crates/perry-runtime/src/symbol.rs index 51ba67ba76..5b600847f9 100644 --- a/crates/perry-runtime/src/symbol.rs +++ b/crates/perry-runtime/src/symbol.rs @@ -51,7 +51,7 @@ pub use properties::{ // Symbol-keyed property reads. pub use get::js_object_get_symbol_property; -pub(crate) use get::{inherited_symbol_property, own_symbol_property}; +pub(crate) use get::{has_own_symbol_property, inherited_symbol_property, own_symbol_property}; // Iterator protocol, getOwnPropertySymbols, ToPrimitive. pub(crate) use iterator::class_ref_resolves_iterator; diff --git a/crates/perry-runtime/src/symbol/get.rs b/crates/perry-runtime/src/symbol/get.rs index 1a6306c3e7..c8f117b0fa 100644 --- a/crates/perry-runtime/src/symbol/get.rs +++ b/crates/perry-runtime/src/symbol/get.rs @@ -25,6 +25,35 @@ fn well_known_symbol_method_name(sym_key: usize) -> Option<&'static str> { None } +/// Does `obj` carry an OWN symbol-keyed property under `sym`, **without +/// invoking** an accessor for it? +/// +/// [`own_symbol_property`] answers the same question by performing the read, +/// which for an accessor property means calling the user getter. A caller that +/// only wants to know whether the builtin behaviour has been shadowed — the +/// `[...arr]` dense fast path in `array::flat_clone` — must not run that getter, +/// because the slow path it falls back to will read the property again and the +/// getter would then observe two calls where the spec mandates one. +/// +/// Deliberately mirrors [`own_symbol_property`]'s two lookups in the same order +/// (accessor table, then the raw `SYMBOL_PROPERTIES` data table) so the two can +/// never disagree about existence; only the *invocation* differs. +pub(crate) unsafe fn has_own_symbol_property(obj_f64: f64, sym_f64: f64) -> bool { + if accessors::symbol_accessor_property(obj_f64, sym_f64).is_some() { + return true; + } + let obj_key = obj_key_from_f64(obj_f64); + let sym_key = sym_key_from_f64(sym_f64); + if obj_key == 0 || sym_key == 0 { + return false; + } + let guard = crate::gc::lock_gc_root_registry(&SYMBOL_PROPERTIES); + match guard.as_ref().and_then(|map| map.get(&obj_key)) { + Some(entries) => entries.iter().any(|&(sk, _)| sk == sym_key), + None => false, + } +} + /// #1758: the OWN symbol-property lookup — the raw `SYMBOL_PROPERTIES` /// side-table read keyed by the object's address (no class-ref / no prototype /// chain). Used by `js_object_get_symbol_property` and by From 5ae629c1c41fc0d2f49499bbaa6c581cd1a99d50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 6 Aug 2026 18:04:58 +0200 Subject: [PATCH 2/7] fix(array): follow the grow-forwarding header in the dense spread fast path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `js_array_grow` (issue #233) leaves a `GC_FLAG_FORWARDED` header at the OLD address, whose first eight bytes — where `length`/`capacity` used to live — now hold the forwarding pointer. `dense_spread_source` took the raw NaN-boxed address and read `length` off that retired header, and the memcpy sized from it walked off the heap: `[...sparse]` after `sparse.length = 5` and `[...beyond]` after `beyond[9] = 9` both took EXC_BAD_ACCESS in `_platform_memmove`. Band/slab validation still runs first (`try_read_gc_header` must reject a handle id without dereferencing it), then `clean_arr_ptr` follows the chain, then the `GC_TYPE_ARRAY` check is re-read off the POST-forwarding header. `dense_spread_copy` cleans on both of its reads, so the post-allocation re-read cannot regress to the stale address either. `a_grown_arrays_forwarding_header_is_followed_not_copied` pins it, and asserts the probe really forced a grow-and-forward (`cleaned != src`) before testing anything — a version that failed to grow would pass vacuously. Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- crates/perry-runtime/src/array/flat_clone.rs | 44 ++++++++++++++++--- .../src/array/spread_dense_tests.rs | 40 +++++++++++++++++ 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/crates/perry-runtime/src/array/flat_clone.rs b/crates/perry-runtime/src/array/flat_clone.rs index 880f0e7608..bc4ecc2ce4 100644 --- a/crates/perry-runtime/src/array/flat_clone.rs +++ b/crates/perry-runtime/src/array/flat_clone.rs @@ -59,11 +59,27 @@ unsafe fn receiver_gc_type(ptr: *const ArrayHeader) -> u8 { /// once, as the spec requires. pub(crate) fn dense_spread_source(value: f64) -> Option<*const ArrayHeader> { let raw = crate::value::js_nanbox_get_pointer(value) as usize; - let header = unsafe { crate::value::addr_class::try_read_gc_header(raw)? }; + // Band/slab validation BEFORE any deref: rejects handle ids and the + // header-less small-buffer slab without touching memory. + unsafe { crate::value::addr_class::try_read_gc_header(raw)? }; + // Only THEN follow the forwarding chain. `js_array_grow` (issue #233) leaves + // a `GC_FLAG_FORWARDED` header at the OLD address whose first eight bytes — + // where `length`/`capacity` used to live — now hold the forwarding pointer. + // Reading `length` off the stale header yields a garbage element count, and + // the copy below then memcpy'd from an address derived from it: `[...sparse]` + // after `sparse.length = 5`, and `[...beyond]` after `beyond[9] = 9`, both + // took EXC_BAD_ACCESS in `_platform_memmove`. Every other array helper cleans + // first, including `js_array_clone`'s own memcpy tail; this one must too. + let arr = crate::array::clean_arr_ptr(raw as *const ArrayHeader); + if arr.is_null() { + return None; + } + // Re-read the type off the POST-forwarding header: the stale one is the + // address the caller named, the live one is the array we would copy. + let header = unsafe { crate::value::addr_class::try_read_gc_header(arr as usize)? }; if header.obj_type != crate::gc::GC_TYPE_ARRAY { return None; } - let arr = raw as *const ArrayHeader; if crate::array::array_iteration_is_exotic(arr) { return None; } @@ -93,18 +109,34 @@ pub(crate) fn dense_spread_source(value: f64) -> Option<*const ArrayHeader> { /// reads `arr[i]`, which yields `undefined` for a hole, while the slot itself /// holds `TAG_HOLE`. Normalize on the way out so `[...[1, , 3]]` stays /// `[1, undefined, 3]` and `1 in [...[1, , 3]]` stays `true`. +/// +/// Both reads of the source go through `clean_arr_ptr`, so a `js_array_grow` +/// forwarding header (#233) is followed rather than mistaken for the array. pub(crate) fn dense_spread_copy(value: f64) -> *mut ArrayHeader { let scope = crate::gc::RuntimeHandleScope::new(); let src_h = scope.root_nanbox_f64(value); + let read_src = || { + clean_arr_ptr( + crate::value::js_nanbox_get_pointer(src_h.get_nanbox_f64()) as *const ArrayHeader + ) + }; unsafe { - let len = (*(crate::value::js_nanbox_get_pointer(src_h.get_nanbox_f64()) - as *const ArrayHeader)) - .length; + let src = read_src(); + if src.is_null() { + return js_array_alloc(0); + } + let len = (*src).length; let result_h = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(js_array_alloc(len) as i64)); - let src = crate::value::js_nanbox_get_pointer(src_h.get_nanbox_f64()) as *const ArrayHeader; + // Re-read AFTER the allocation: `js_array_alloc` is a collection point, + // and pre-#7497 the sibling `js_array_clone` memcpy'd from the + // pre-collection address, i.e. out of retired from-space. + let src = read_src(); let result = crate::value::js_nanbox_get_pointer(result_h.get_nanbox_f64()) as *mut ArrayHeader; + if src.is_null() || result.is_null() { + return js_array_alloc(0); + } if len > 0 { let src_elements = (src as *const u8).add(std::mem::size_of::()) as *const u64; diff --git a/crates/perry-runtime/src/array/spread_dense_tests.rs b/crates/perry-runtime/src/array/spread_dense_tests.rs index 23f0b2d8d7..7236039b7e 100644 --- a/crates/perry-runtime/src/array/spread_dense_tests.rs +++ b/crates/perry-runtime/src/array/spread_dense_tests.rs @@ -174,6 +174,46 @@ fn a_handle_band_id_is_rejected_without_dereferencing_it() { } } +#[test] +fn a_grown_arrays_forwarding_header_is_followed_not_copied() { + // `js_array_grow` leaves a GC_FLAG_FORWARDED header at the OLD address, and + // its first eight bytes — where length/capacity used to live — now hold the + // forwarding pointer. Reading `length` off the stale header yields a garbage + // element count, and the memcpy derived from it took EXC_BAD_ACCESS. The JS + // shapes were `[...sparse]` after `sparse.length = 5` and `[...beyond]` after + // `beyond[9] = 9`; both grow past capacity while the caller still names the + // pre-grow address. + let src = dense(&[1.0, 2.0]); + let stale = boxed(src); + js_array_set_length(src, 40.0); + + let cleaned = clean_arr_ptr(src as *const ArrayHeader); + assert_ne!( + cleaned as usize, src as usize, + "probe must really force a grow-and-forward, or it proves nothing" + ); + + // Everything below is driven from the STALE value the caller would hold. + assert_eq!( + dense_spread_source(stale).map(|p| p as usize), + Some(cleaned as usize), + "the guard must report the forwarded array, not the retired header" + ); + let copy = dense_spread_copy(stale); + unsafe { + assert_eq!((*copy).length, 40); + assert_eq!(slot_bits(copy, 0), 1.0f64.to_bits()); + assert_eq!(slot_bits(copy, 1), 2.0f64.to_bits()); + for i in 2..40 { + assert_eq!( + slot_bits(copy, i), + TAG_UNDEFINED, + "slot {i} past the original length reads undefined, like arr[i]" + ); + } + } +} + #[test] fn a_sparse_array_whose_length_outruns_its_storage_is_not_eligible() { // Live indices past the dense backing store live in the named-property map, From f9c153717fc648e07befa24b7517e024dac7bdf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 6 Aug 2026 18:26:22 +0200 Subject: [PATCH 3/7] fix(gc): root js_array_map_discard's callback across the dispatch loop (#6081 sibling) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `js_array_map` roots its callback (#6081): a callback allocated by a frameless caller — the arrow in `xs.map(x => …)` — is reachable only through the raw parameter and the native stack, which an evacuating minor does not scan, so from the second element on the dispatch reads a moved-or-swept closure. `js_array_map_discard` was missed and kept using the bare parameter across `js_closure_call3`, which allocates. Latent until now, because a stale root only bites when a collection lands inside its window. #7533's dense-spread fast path removes ~25 allocations per iteration of `object_deep_clone` and moved every subsequent collection; one now lands squarely in this loop and `PERRY_GC_PROTECT_FROMSPACE=1` faults on a retired `obj_type=4` (GC_TYPE_CLOSURE) at `js_array_map_discard + 788`. The kernel faults under the same instrument at the PREVIOUS commit too — at a different site, inside `array_from_spread_value` — so this is a pre-existing defect exposed by new timing, not one introduced by it. Rooted NaN-boxed rather than via `root_raw_const_ptr`, so the per-callsite read-back is a `get_nanbox_f64` and the module stays out of `scripts/raw_handle_debt.py`'s ledger (unchanged at 999). Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- .../perry-runtime/src/array/iter_methods.rs | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/crates/perry-runtime/src/array/iter_methods.rs b/crates/perry-runtime/src/array/iter_methods.rs index 9159279c5e..e719e19f85 100644 --- a/crates/perry-runtime/src/array/iter_methods.rs +++ b/crates/perry-runtime/src/array/iter_methods.rs @@ -273,6 +273,31 @@ pub extern "C" fn js_array_map_discard(arr: *const ArrayHeader, callback: *const let length = (*arr).length; let scope = crate::gc::RuntimeHandleScope::new(); let rooted = RootedIterArray::new(&scope, arr); + // The callback needs the same root its sibling `js_array_map` gives it + // (#6081), and for the same reason: a callback allocated by a frameless + // caller — the arrow in `xs.map(x => …)` — is reachable ONLY through this + // raw parameter and the native stack, which an evacuating minor does not + // scan. Every `js_closure_call3` below allocates, so from the second + // element on the dispatch reads a moved-or-swept closure. + // + // This arm was missed when #6081 rooted `js_array_map`, and stayed latent: + // a stale root only bites when a collection lands inside its window, and + // nothing put one there. #7533's dense-spread fast path removed ~25 + // allocations per loop iteration from `object_deep_clone`, which moved + // every subsequent collection and dropped one squarely inside this loop — + // `PERRY_GC_PROTECT_FROMSPACE=1` then faults here on a retired + // `obj_type=4` (GC_TYPE_CLOSURE). The kernel faults under the instrument + // BEFORE that change too, at a different site, so this is a pre-existing + // defect exposed by new timing, not one introduced by it. + // + // NaN-boxed rather than `root_raw_const_ptr`, so the read-back at each + // callsite is a `get_nanbox_f64` and this module stays out of + // `scripts/raw_handle_debt.py`'s ledger (same shape as + // `js_iterator_to_array`'s `next` handle). + let cb_handle = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(callback as i64)); + let current_callback = || { + crate::value::js_nanbox_get_pointer(cb_handle.get_nanbox_f64()) as *const ClosureHeader + }; let _tg = DenseThisGuard::bind_undefined(); if crate::array::array_iteration_is_exotic(arr) { for i in 0..length as usize { @@ -281,7 +306,7 @@ pub extern "C" fn js_array_map_discard(arr: *const ArrayHeader, callback: *const continue; } let element = crate::array::array_spec_get(arr, i as u32); - let _ = js_closure_call3(callback, element, i as f64, rooted.receiver()); + let _ = js_closure_call3(current_callback(), element, i as f64, rooted.receiver()); } return; } @@ -289,7 +314,7 @@ pub extern "C" fn js_array_map_discard(arr: *const ArrayHeader, callback: *const let Some(element) = rooted.present(i) else { continue; }; - let _ = js_closure_call3(callback, element, i as f64, rooted.receiver()); + let _ = js_closure_call3(current_callback(), element, i as f64, rooted.receiver()); } } } From da1c85b73b33de70790c448ad791c520f894b8ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 6 Aug 2026 18:32:31 +0200 Subject: [PATCH 4/7] test(array): use the canonical addr_class band constants in the spread tests The addr-class ratchet flags bare band literals. Same coverage, named bands. Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- .../7534-dense-array-spread-fast-path.md | 62 +++++++++++++++++-- .../src/array/spread_dense_tests.rs | 14 ++++- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/changelog.d/7534-dense-array-spread-fast-path.md b/changelog.d/7534-dense-array-spread-fast-path.md index 0bd8a6377b..554ebf5263 100644 --- a/changelog.d/7534-dense-array-spread-fast-path.md +++ b/changelog.d/7534-dense-array-spread-fast-path.md @@ -67,13 +67,65 @@ rather than `own_symbol_property`: the latter answers by *reading*, which calls user getter, and the slow path it falls back to reads the property again — a getter must observe exactly one call. +**Result on the pinned quiet mini** (same host, same method, best-of-7 wall): + +| | before | after | node v26.5.1 | +|---|--:|--:|--:| +| `object_deep_clone` (the kernel, N=50 000) | 610 ms | **40 ms** | 60 ms | +| `dc_spread` alone (N=500 000) | 5.92 s | **0.12 s** | — | +| `dc_full` (N=500 000) | 6.20 s | **0.36 s** | — | + +**15.3× on the kernel; 49× on the spread itself.** Against the artifact's bun +figure the row moves from **37.5× to ~2.3×**, and Perry goes from 11.5× node to +**0.67× node — a win**. The 657 ms → 40 ms row should stop being the worst cell +in the artifact by a wide margin; it is now among the better ones. + +Verified byte-identical to the node oracle under **both** link modes (auto-optimize +and `PERRY_NO_AUTO_OPTIMIZE=1`), `scripts/auto_opt_app_patterns.sh` 12/12, and a +32-case `[...arr]` semantics matrix byte-identical to pre-change Perry. Under +`PERRY_GC_ZEAL=1 PERRY_GC_PROTECT_FROMSPACE=1 +PERRY_GC_PROTECT_FROMSPACE_DEPTH=800` on a `PERRY_GC_MOVING_LOOP_POLLS=1` build: +**50 005 retired page sets quarantined, zero faults**, correct checksum — the +instrument proven live by its `[gc-fromspace-protect] mode=… retired_set=#N` +lines rather than assumed. + ### Fixed -Nothing behavioural. `crates/perry-runtime/src/array/spread_dense_tests.rs` pins -`dense_spread_source`'s **verdict** in every case, not only the resulting -elements: the slow path is a correct fallback, so a test comparing elements alone -would stay green if the fast path silently stopped applying — CLAUDE.md's fourth -way a gate can be unable to fail. +**`js_array_map_discard` never rooted its callback (#6081's missed sibling).** +`js_array_map` roots it — a callback allocated by a frameless caller (the arrow +in `xs.map(x => …)`) is reachable only through the raw parameter and the native +stack, which an evacuating minor does not scan, so from the second element on the +dispatch reads a moved-or-swept closure. The discard variant kept using the bare +parameter across `js_closure_call3`, which allocates. + +It stayed latent because a stale root only bites when a collection lands inside +its window, and nothing put one there. Removing ~25 allocations per loop +iteration moved every subsequent collection in this kernel and dropped one +squarely inside that loop: the kernel started failing `TypeError: value is not a +function`, and `PERRY_GC_PROTECT_FROMSPACE=1` named the site precisely — a +retired `obj_type=4` (GC_TYPE_CLOSURE) at `js_array_map_discard + 788`. **The +kernel faults under the same instrument at the previous commit too**, at a +different site inside `array_from_spread_value`, which is how the defect was +established as pre-existing rather than introduced. Rooted NaN-boxed, so the +read-back is a `get_nanbox_f64` and `scripts/raw_handle_debt.py` stays at 999. + +**The dense fast path itself shipped one bug during development, worth recording +because the shape recurs**: it read `length` straight off the address in the +NaN-box, without `clean_arr_ptr`. `js_array_grow` (issue #233) leaves a +`GC_FLAG_FORWARDED` header at the OLD address whose first eight bytes — where +`length`/`capacity` used to live — now hold the forwarding pointer, so the memcpy +was sized from a forwarding pointer reinterpreted as a length. `[...sparse]` after +`sparse.length = 5` and `[...beyond]` after `beyond[9] = 9` both took +EXC_BAD_ACCESS in `_platform_memmove`. Band/slab validation still runs before any +deref; the chain is followed only after that, and the `GC_TYPE_ARRAY` check is +re-read off the post-forwarding header. + +`crates/perry-runtime/src/array/spread_dense_tests.rs` pins `dense_spread_source`'s +**verdict** in every case, not only the resulting elements: the slow path is a +correct fallback, so a test comparing elements alone would stay green if the fast +path silently stopped applying — CLAUDE.md's fourth way a gate can be unable to +fail. The forwarding case likewise asserts the probe really forced a +grow-and-forward before testing anything. ### Notes diff --git a/crates/perry-runtime/src/array/spread_dense_tests.rs b/crates/perry-runtime/src/array/spread_dense_tests.rs index 7236039b7e..124b06e19c 100644 --- a/crates/perry-runtime/src/array/spread_dense_tests.rs +++ b/crates/perry-runtime/src/array/spread_dense_tests.rs @@ -166,9 +166,19 @@ fn a_handle_band_id_is_rejected_without_dereferencing_it() { // Web-Fetch / stream / timer ids are NaN-boxed POINTER values in the handle // band. Dereferencing `id - 8` as a GcHeader is a SIGSEGV (#7526), so the // rejection has to come from the band test, not from reading the header. - for id in [1i64, 0x1000, 0x40000, 0xE0000] { + use crate::value::addr_class::{ + COMMON_HANDLE_BAND_END, FETCH_HANDLE_BAND_START, HANDLE_BAND_MAX, ZLIB_HANDLE_BAND_START, + }; + let ids = [ + 1usize, + COMMON_HANDLE_BAND_END, + FETCH_HANDLE_BAND_START, + ZLIB_HANDLE_BAND_START, + HANDLE_BAND_MAX - 1, + ]; + for id in ids { assert!( - dense_spread_source(crate::value::js_nanbox_pointer(id)).is_none(), + dense_spread_source(crate::value::js_nanbox_pointer(id as i64)).is_none(), "handle id {id:#x} must be rejected by band" ); } From 30be3ab250212b4ed87c8d58fff6dc29fb5e2355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 6 Aug 2026 18:34:07 +0200 Subject: [PATCH 5/7] docs(changelog): key the fragment to its PR number (#7540) 7534 was already taken by the engine-plan baseline fragment on main. Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- ...y-spread-fast-path.md => 7540-dense-array-spread-fast-path.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{7534-dense-array-spread-fast-path.md => 7540-dense-array-spread-fast-path.md} (100%) diff --git a/changelog.d/7534-dense-array-spread-fast-path.md b/changelog.d/7540-dense-array-spread-fast-path.md similarity index 100% rename from changelog.d/7534-dense-array-spread-fast-path.md rename to changelog.d/7540-dense-array-spread-fast-path.md From 56f7e78584ea3a4784ec007e0770f6dbce33954c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 6 Aug 2026 18:36:02 +0200 Subject: [PATCH 6/7] docs(changelog): name the two pre-existing spread divergences by issue (#7541, #7542) Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- changelog.d/7540-dense-array-spread-fast-path.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/7540-dense-array-spread-fast-path.md b/changelog.d/7540-dense-array-spread-fast-path.md index 554ebf5263..c4f5cde5b7 100644 --- a/changelog.d/7540-dense-array-spread-fast-path.md +++ b/changelog.d/7540-dense-array-spread-fast-path.md @@ -135,4 +135,4 @@ matrix for this change. **Both predate this work and are unchanged by it** stack): `[...MyArr.from([1,2,3])]` on a `class MyArr extends Array` throws `value is not iterable`, and a replaced `Array.prototype[Symbol.iterator]` is ignored by spread (`[...[1,2,3]]` yields `[1,2,3]` where node yields the patched -iterator's output). Filed separately rather than folded in here. +iterator's output). Filed as #7541 and #7542 rather than folded in here. From a3bb5d6b0c89a4e66561705a6025191d73d56be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 6 Aug 2026 21:06:45 +0200 Subject: [PATCH 7/7] chore: bump version to 0.5.1308 --- CLAUDE.md | 2 +- Cargo.lock | 152 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 3 files changed, 78 insertions(+), 78 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0ef88ad86e..bf377a4a51 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.1307 +**Current Version:** 0.5.1308 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 62fb8838ef..4df908da33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1307" +version = "0.5.1308" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1307" +version = "0.5.1308" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1307" +version = "0.5.1308" [[package]] name = "perry-ui-tvos" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1307" +version = "0.5.1308" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index f8f5239c36..87407c2363 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1307" +version = "0.5.1308" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"