From aa2e8ad021653b577b08e78b494af362fc6db957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 6 Aug 2026 19:58:28 +0200 Subject: [PATCH 1/6] fix(gc): root the overflow store's value across the spill allocations (#7538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the lazy JSON tape and the generational GC both on — the shipped defaults — a full element-wise scan of a parsed top-level array could leave one record's overflow field naming a pre-collection address, and `JSON.stringify` then emitted `{"field0":…,"field1":…}` where the source said `{"x":…,"y":…}`. Values and order correct, key names gone, no throw and no diagnostic. `spill_set_slow` rooted the owner across `object_meta_ensure` and `js_array_alloc_with_length` but took the value as plain `u64` bits. Both are collection points; an evacuating minor at either one moved the value, rewrote every live reference, and left the parameter naming the from-space copy — which the function then published into the overflow slot after the collector was done. Alive but relocated, so no verifier could see it. Two same-shape holes close with it: `field_set_by_name/tail.rs` snapshotted the overflow bits above the allocating `js_array_push`, and `json_tape::lazy_get` recorded its out-of-object sparse-cache store with the in-object barrier (#7500 — that abort is gone). Reachable only via the tape's element-wise materializer, which grows every record by name and so overflows past `INLINE_SLOT_FLOOR`; the direct parser pre-sizes and never does. Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- changelog.d/7539-tape-overflow-key-loss.md | 18 +++ .../tests/runtime_roots/callback_scanners.rs | 88 ++++++++++++++ .../tests/runtime_roots/transient_handles.rs | 115 ++++++++++++++++++ crates/perry-runtime/src/json_tape.rs | 35 +++++- .../src/object/field_set_by_name/tail.rs | 53 ++++++-- crates/perry-runtime/src/object/mod.rs | 2 + crates/perry-runtime/src/object/spill.rs | 56 ++++++++- 7 files changed, 347 insertions(+), 20 deletions(-) create mode 100644 changelog.d/7539-tape-overflow-key-loss.md diff --git a/changelog.d/7539-tape-overflow-key-loss.md b/changelog.d/7539-tape-overflow-key-loss.md new file mode 100644 index 0000000000..6621acdb77 --- /dev/null +++ b/changelog.d/7539-tape-overflow-key-loss.md @@ -0,0 +1,18 @@ +### Fixed + +- **`JSON.parse` + generational GC: a record's overflow field could keep a pre-collection address, so `JSON.stringify` emitted `{"field0":…,"field1":…}` (#7538).** Silent wrong output on the shipped defaults — no throw, no diagnostic, correct values under invented key names. + + **Root cause — `object/spill.rs::spill_set_slow`.** Overflow ("spill") storage holds every field past the object's inline allocation. Its slow path rooted the *owner* across the two allocations it makes (`object_meta_ensure` for the meta record, `js_array_alloc_with_length` for the buffer) but took the *value* as a plain `u64` parameter. Both allocations are collection points. An evacuating minor at either one moved the value, rewrote every live reference to it — and left this parameter naming the from-space copy, which the function then published into the overflow slot *after* the collector had finished rewriting. Nothing corrected it afterwards and nothing could report it: the target is alive, merely relocated, so `PERRY_GC_VERIFY_EVACUATION` (which only flags forwarded-but-unrewritten slots reachable through a traced descriptor) walked straight past it, and the from-space quarantine only faults if the stale address is later *dereferenced*. The fix roots the value in the same handle scope and re-reads it immediately before the store. + + Downstream, reading a record through that stale pointer returned plausible field *values* (from-space still holds the old bytes) but a `keys_array` pointer that had itself been recycled — which is why `json/stringify.rs`'s `field{n}` fallback fired for exactly one record per run while every value stayed correct. + + **Why the lazy JSON tape and only the lazy JSON tape.** `json_tape::materialize_object` builds each record with `js_object_alloc(0, 0)` plus one `js_object_set_field_by_name` per key, so any record with more than `INLINE_SLOT_FLOOR` (4) keys parks its last field in overflow. The direct parser pre-sizes from a known field count and never overflows — which is exactly why `PERRY_JSON_TAPE=0` was clean, and why the two-switch `idiomatic` row in the public baseline (tape *and* gen-GC off together) never exercised the failing combination. + + Two further holes of the same shape are closed alongside it: + + - `object/field_set_by_name/tail.rs` snapshotted the overflow store's bits (`let vbits = value.to_bits()`) *above* the `js_array_push` that grows the keys array — an allocation, so a collection point — and stored that dead copy after `refresh_roots_after_alloc!()` had refreshed `value`. Both overflow arms now derive the bits from the refreshed value, through a shared `overflow_store_bits` helper. + - `json_tape::lazy_get` recorded its sparse-cache store with the *in-object* write barrier (**#7500**). The cache is a separate `GC_TYPE_STRING` allocation, born old at ≥2049 elements, so that barrier marked the page the slot sits on; the minor's dirty-page scan then walked the objects on that page, found the cache's own leaf header, and scanned nothing. The only descriptor that can read those slots is `GcRewriteDescriptorKind::LazyArray` on the owner, whose pages stayed clean — so cached element pointers were neither marked nor rewritten. It now uses `runtime_write_barrier_external_slot`, which records `(slot page → owner header)`. This is what made `PERRY_GC_VERIFY_EVACUATION=1` abort on every `PERRY_JSON_TAPE=1` workload; that abort is gone. + + `test_dirty_lazy_array_external_cache_scan_marks_bitmap_selected_child` had covered the *consumer* of that external entry since it landed — from an entry planted by hand. It was green the whole time no producer ever wrote one. + + **Validation.** The issue's reproducer goes 1/53 → 0/53, ten consecutive runs, byte-identical to node, on the default configuration. Two regression tests, both verified red against the unfixed code: `test_transient_runtime_handle_object_overflow_set_gc` (asserts the overflow slot holds the *post*-collection address; the string-content assertion alone passes against the bug, which is what made it silent) and `test_json_tape_lazy_get_records_its_cache_store_as_an_external_edge` (drives the real producer and asserts the external remembered-set entry exists). Both assert their subject was live — that the value actually moved, that the header is genuinely born-old, that the cache and header sit on different pages. diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs index afbb286e7c..6c3b20b6de 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs @@ -302,6 +302,94 @@ fn test_json_tape_lazy_get_header_handle_survives_copied_minor_gc() { } } +/// #7538 / #7500: `lazy_get`'s sparse-cache store must be recorded as an +/// EXTERNAL old→young edge. +/// +/// The cache is not part of the `LazyArrayHeader` allocation — it is a +/// separate `GC_TYPE_STRING` block, born old at ≥2049 elements. The +/// in-object barrier `lazy_get` used marks the page the SLOT sits on, and the +/// minor's dirty-page scan then walks the objects on that page and finds the +/// cache's own `GC_TYPE_STRING` header, a GC leaf with no child slots. The +/// only descriptor that can read those slots is +/// `GcRewriteDescriptorKind::LazyArray`, which hangs off the OWNER header — +/// whose pages stay clean. So the cached element pointers were neither marked +/// nor rewritten by a copying minor. +/// +/// `test_dirty_lazy_array_external_cache_scan_marks_bitmap_selected_child` +/// covers the CONSUMER of that entry, but plants the entry by hand — it was +/// green the entire time no producer wrote one. This drives the real producer. +#[test] +fn test_json_tape_lazy_get_records_its_cache_store_as_an_external_edge() { + let _guard = CopyingNurseryTestGuard::new(0); + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + register_runtime_handle_root_scanner_for_tests(); + + // Born-old header: >16 KB of inline tape. That is the shape the #7538 + // workload had and the only one where the in-object/external distinction + // bites — a nursery header is traced directly and its descriptor reaches + // the cache without any remembered-set entry at all. + let elements = 4096; + let mut input = String::with_capacity(elements * 8 + 2); + input.push('['); + for i in 0..elements { + if i > 0 { + input.push(','); + } + input.push_str("{\"a\":1}"); + } + input.push(']'); + let hdr = unsafe { test_alloc_lazy_json_array(input.as_bytes()) }; + assert!( + crate::arena::pointer_in_old_gen(hdr as usize), + "the lazy header must be born old, or this test exercises nothing" + ); + + let scope = RuntimeHandleScope::new(); + let hdr_handle = scope.root_raw_mut_ptr(hdr); + let first = unsafe { crate::json_tape::lazy_get(hdr_handle.get_raw_mut_ptr(), 7) }; + let element_addr = first.bits() & POINTER_MASK; + assert_ne!( + element_addr, 0, + "element 7 should materialize to a heap object" + ); + assert!( + crate::arena::pointer_in_nursery(element_addr as usize), + "the materialized element must be young, or no old→young edge exists" + ); + + let hdr_after = hdr_handle.get_raw_mut_ptr::(); + let (cache_slot, header_addr) = unsafe { + let cache = (*hdr_after).materialized_elements; + assert!( + !cache.is_null(), + "cold lazy_get should allocate the sparse cache" + ); + ( + cache.add(7) as usize, + header_from_user_ptr(hdr_after as *const u8) as usize, + ) + }; + assert_ne!( + crate::arena::generation_page_for_addr(cache_slot), + crate::arena::generation_page_for_addr(hdr_after as usize), + "cache and header must land on different pages, or the in-object barrier \ + would have covered the slot by accident" + ); + + let snapshot = crate::gc::barrier::remembered_dirty_snapshot(); + let slot_page = crate::arena::generation_page_for_addr(cache_slot); + assert!( + snapshot + .external_dirty_entries + .iter() + .any(|&(page, header)| page == slot_page && header == header_addr), + "lazy_get must record its sparse-cache store as an EXTERNAL dirty slot naming the \ + owning LazyArrayHeader. Recording only the slot's page (the in-object barrier) is \ + inert: the dirty scan walks the objects on that page and finds the cache's own \ + GC_TYPE_STRING header, a leaf with no child slots, so nothing is marked or rewritten." + ); +} + #[test] fn test_json_tape_force_materialize_sparse_cache_handles_survive_copied_minor_gc() { let _guard = CopyingNurseryTestGuard::new(0); diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/transient_handles.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/transient_handles.rs index 88aa9114b4..cccdf1730d 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/transient_handles.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/transient_handles.rs @@ -471,6 +471,121 @@ fn test_transient_runtime_handle_object_set_gc() { } } +thread_local! { + static SPILL_HOOK_FIRED: std::cell::Cell = const { std::cell::Cell::new(false) }; +} + +/// Force one copying minor at the spill allocation window, once. +fn spill_force_minor_gc_hook(_obj_ptr: usize) { + if SPILL_HOOK_FIRED.with(|fired| fired.replace(true)) { + return; + } + let _ = crate::gc::gc_collect_minor(); +} + +struct SpillSafepointHookGuard { + previous: Option, +} + +impl SpillSafepointHookGuard { + fn new() -> Self { + SPILL_HOOK_FIRED.with(|fired| fired.set(false)); + let previous = + crate::object::test_set_spill_safepoint_hook(Some(spill_force_minor_gc_hook)); + Self { previous } + } + + fn assert_fired(&self) { + assert!( + SPILL_HOOK_FIRED.with(|fired| fired.get()), + "the spill allocation window was never entered — this test proves nothing" + ); + } +} + +impl Drop for SpillSafepointHookGuard { + fn drop(&mut self) { + crate::object::test_set_spill_safepoint_hook(self.previous); + SPILL_HOOK_FIRED.with(|fired| fired.set(false)); + } +} + +/// #7538: the value handed to the spill (overflow) store must survive a +/// copying minor landing on the spill's OWN allocations. +/// +/// `spill_set_slow` rooted the owner but took the value as plain `u64` bits. +/// `object_meta_ensure` and the buffer `js_array_alloc_with_length` are both +/// collection points, so an evacuating minor at either one moved the value and +/// left the parameter naming the from-space copy — which the function then +/// published into the overflow slot, AFTER the collector had finished +/// rewriting live references. Nothing corrected it and no verifier could see +/// it: the target is alive, merely relocated, so +/// `PERRY_GC_VERIFY_EVACUATION` (which only reports forwarded-but-unrewritten +/// slots reachable through a traced descriptor) walked straight past it. +/// +/// The observable end of that in #7538 was `JSON.stringify` emitting +/// `{"field0":…,"field1":…}` for one record in a parsed array: the stale +/// pointer still read back plausible field VALUES out of retired from-space, +/// but its `keys_array` named a keys array that had itself been recycled. +/// +/// The address assertion is the load-bearing one. `assert_string_bytes` alone +/// passes against the unfixed code, because from-space still holds the old +/// bytes — which is exactly why this shipped silently. +#[test] +fn test_transient_runtime_handle_object_overflow_set_gc() { + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); + let _guard = CopyingNurseryTestGuard::new(2); + let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + register_runtime_handle_root_scanner_for_tests(); + + // `js_object_alloc(0, 0)` is what `json_tape`'s element-wise materializer + // uses, so every key past `INLINE_SLOT_FLOOR` parks in overflow storage. + let obj = crate::object::js_object_alloc(0, 0); + js_shadow_slot_set(0, ptr_bits(obj as usize)); + let obj_now = || (js_shadow_slot_get(0) & POINTER_MASK) as *mut crate::object::ObjectHeader; + + // Fill the inline region so the next key overflows. + for i in 0..crate::object::INLINE_SLOT_FLOOR { + let name = format!("k{i}"); + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + crate::object::js_object_set_field_by_name(obj_now(), key, i as f64); + } + let overflow_index = crate::object::INLINE_SLOT_FLOOR; + + let value = crate::string::js_string_from_bytes(b"overflow-payload".as_ptr(), 16); + js_shadow_slot_set(1, string_bits(value as usize)); + let overflow_key = crate::string::js_string_from_bytes(b"spilled".as_ptr(), 7); + + let hook = SpillSafepointHookGuard::new(); + crate::object::js_object_set_field_by_name( + obj_now(), + overflow_key, + f64::from_bits(js_shadow_slot_get(1)), + ); + hook.assert_fired(); + + let value_after = (js_shadow_slot_get(1) & POINTER_MASK) as *const crate::StringHeader; + assert_ne!( + value_after as usize, value as usize, + "the copying minor did not relocate the value — the window under test never opened" + ); + + let stored = crate::object::overflow_get(obj_now() as usize, overflow_index) + .expect("the overflow slot must hold the stored value"); + assert_eq!(stored & TAG_MASK, STRING_TAG); + assert_eq!( + (stored & POINTER_MASK) as usize, + value_after as usize, + "the overflow store must publish the POST-collection address" + ); + unsafe { + assert_string_bytes( + (stored & POINTER_MASK) as *const crate::StringHeader, + b"overflow-payload", + ); + } +} + #[test] fn test_transient_runtime_handle_closure_captures_gc() { let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); diff --git a/crates/perry-runtime/src/json_tape.rs b/crates/perry-runtime/src/json_tape.rs index f13f205a33..fd2065ece7 100644 --- a/crates/perry-runtime/src/json_tape.rs +++ b/crates/perry-runtime/src/json_tape.rs @@ -1205,6 +1205,35 @@ unsafe fn note_lazy_raw_slot(hdr: *mut LazyArrayHeader, slot_addr: usize, child_ crate::gc::runtime_write_barrier_slot(hdr as usize, slot_addr, child_addr as u64); } +/// Barrier for a store into the sparse element cache (#7538). +/// +/// The cache is NOT part of the `LazyArrayHeader` allocation — it is a +/// separate `GC_TYPE_STRING` block hanging off `materialized_elements`, and at +/// ≥2049 elements (`cached_length * 8 > LARGE_OBJECT_THRESHOLD_BYTES`) it is +/// born directly in old-gen. That makes the ordinary in-object barrier +/// ([`note_lazy_raw_slot`]) the WRONG one here, and silently so: +/// `remember_old_to_young_slot` marks the page the SLOT lives on, and the +/// minor's dirty-page scan then walks the objects on that page and finds the +/// cache's own `GC_TYPE_STRING` header — a GC leaf with no child slots, so it +/// scans nothing. The only descriptor that can read those slots is +/// `GcRewriteDescriptorKind::LazyArray`, which hangs off the OWNER header, +/// whose pages stay clean. A copying minor therefore neither marked nor +/// rewrote the cached element pointers: element identity survived (the bitmap +/// still says "cached"), but the pointer named a retired from-space copy. +/// Reading a record through it returned the pre-collection `keys_array`, which +/// is why `JSON.stringify` emitted `{"field0":…,"field1":…}` for exactly one +/// record per run with the values still correct. +/// +/// `runtime_write_barrier_external_slot` is the out-of-object form: it records +/// `(slot page → owner header)` so the scan re-enters through the owner's +/// LazyArray descriptor, which is what +/// `test_dirty_lazy_array_external_cache_scan_marks_bitmap_selected_child` +/// has always exercised — from a hand-planted entry no producer ever wrote. +#[inline] +unsafe fn note_lazy_cache_slot(hdr: *mut LazyArrayHeader, slot_addr: usize, value_bits: u64) { + crate::gc::runtime_write_barrier_external_slot(hdr as usize, slot_addr, value_bits); +} + /// Count top-level elements in the tape's root array. Hops forward /// from `root_idx + 1` via the `link` field on container kinds to /// skip nested subtrees — O(top-level-count), not O(total-nodes). @@ -1367,11 +1396,7 @@ pub unsafe fn lazy_get(hdr: *mut LazyArrayHeader, i: u32) -> JSValue { if !bitmap.is_null() && !cache.is_null() { let value_bits = value_handle.get_nanbox_u64(); *cache.add(i as usize) = JSValue::from_bits(value_bits); - crate::gc::runtime_write_barrier_slot( - hdr as usize, - cache.add(i as usize) as usize, - value_bits, - ); + note_lazy_cache_slot(hdr, cache.add(i as usize) as usize, value_bits); let word_idx = (i as usize) / 64; let bit_idx = (i as usize) % 64; *bitmap.add(word_idx) |= 1u64 << bit_idx; diff --git a/crates/perry-runtime/src/object/field_set_by_name/tail.rs b/crates/perry-runtime/src/object/field_set_by_name/tail.rs index e028699e7a..42f6df0dfd 100644 --- a/crates/perry-runtime/src/object/field_set_by_name/tail.rs +++ b/crates/perry-runtime/src/object/field_set_by_name/tail.rs @@ -14,6 +14,23 @@ use super::write_helpers::{ }; use super::*; +/// Bits to publish into an overflow slot for a NEWLY appended key. +/// +/// Must be called AFTER the last `refresh_roots_after_alloc!()` on the path, +/// so it reads the post-collection `value` (#7538). Also scrubs the +/// `0x7FFD_0000_0000_0000` null-POINTER_TAG shape that must never reach +/// overflow storage. +#[inline] +fn overflow_store_bits(value: f64, obj: *mut ObjectHeader, new_index: usize) -> u64 { + let vbits = value.to_bits(); + if (vbits >> 48) == 0x7FFD && (vbits & 0x0000_FFFF_FFFF_FFFF) == 0 { + eprintln!("[WARN_NULL_PTR] overflow new store: null POINTER_TAG at obj={obj:p} new_index={new_index} — replacing with undefined"); + crate::value::TAG_UNDEFINED + } else { + vbits + } +} + /// Tail of `js_object_set_field_by_name`, entered with `obj` already stripped /// of its NaN-box tag and vetted against the handle band, typed arrays, and /// the `arr.length` special case. Body moved verbatim. @@ -684,12 +701,6 @@ pub(super) fn set_field_by_name_object_tail( }; let new_index = key_count; if new_index >= alloc_limit { - let vbits = value.to_bits(); - let vbits = if (vbits >> 48) == 0x7FFD && (vbits & 0x0000_FFFF_FFFF_FFFF) == 0 { - crate::value::TAG_UNDEFINED - } else { - vbits - }; let owned_keys_handle = scope.root_raw_mut_ptr(owned_keys); let new_keys = crate::array::js_array_push(owned_keys, JSValue::string_ptr(key as *mut _)); @@ -708,6 +719,9 @@ pub(super) fn set_field_by_name_object_tail( if !keys_shared { super::shapes::shape_keys_grown(prev_keys_usize, new_keys); } + // #7538: derive the stored bits from the REFRESHED `value` — + // see the twin below the linear scan. + let vbits = overflow_store_bits(value, obj, new_index); overflow_set(obj as usize, new_index, vbits); refresh_roots_after_alloc!(); mirror_class_object_static_write(obj, key, value); @@ -888,13 +902,6 @@ pub(super) fn set_field_by_name_object_tail( if new_index >= alloc_limit { // No inline room — store in the overflow HashMap so the value is not lost. // Also add the key to keys_array so Object.keys() sees it. - let vbits = value.to_bits(); - let vbits = if (vbits >> 48) == 0x7FFD && (vbits & 0x0000_FFFF_FFFF_FFFF) == 0 { - eprintln!("[WARN_NULL_PTR] overflow new store: null POINTER_TAG at obj={:p} new_index={} — replacing with undefined", obj, new_index); - crate::value::TAG_UNDEFINED - } else { - vbits - }; let owned_keys_handle = scope.root_raw_mut_ptr(owned_keys); let new_keys = crate::array::js_array_push(owned_keys, JSValue::string_ptr(key as *mut _)); @@ -910,6 +917,26 @@ pub(super) fn set_field_by_name_object_tail( if !keys_shared { super::shapes::shape_keys_grown(prev_keys_usize, new_keys); } + // #7538: the bits stored into overflow must come from the + // REFRESHED `value`. This was snapshotted ABOVE the + // `js_array_push` that grows the keys array — an allocation, so a + // collection point. `refresh_roots_after_alloc!` re-reads `value` + // from its handle, but a `let vbits = value.to_bits()` taken + // before the push is a dead copy the macro cannot reach, and this + // is the branch that stores it. An evacuating minor inside the + // push therefore published the from-space address of the value + // into the owner's overflow slot: the collector had already + // rewritten every live reference, so nothing ever corrected it + // and no verifier could see it (the target is live, just moved). + // + // Reachable for any object grown past its inline allocation by + // NAME — which is exactly what `json_tape`'s element-wise + // materializer does (`js_object_alloc(0, 0)` + one + // `js_object_set_field_by_name` per key), so a parsed record with + // more than `INLINE_SLOT_FLOOR` keys parks its last field here. + // The direct parser pre-sizes from a known field count and never + // overflows, which is why `PERRY_JSON_TAPE=0` did not reproduce. + let vbits = overflow_store_bits(value, obj, new_index); overflow_set(obj as usize, new_index, vbits); refresh_roots_after_alloc!(); mirror_class_object_static_write(obj, key, value); diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index c2e620e2a6..35dc743670 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -104,6 +104,8 @@ mod spill; pub(crate) use spill::{learned_inline_field_count, overflow_get, overflow_set}; #[cfg(test)] use spill::{object_spill_enabled, spill_capable_owner, spill_get, SPILL_MAX_FIELD_INDEX}; +#[cfg(test)] +pub(crate) use spill::{test_set_spill_safepoint_hook, SpillSafepointHook}; mod string_proto_thunks; #[cfg(feature = "temporal")] mod temporal_proto; diff --git a/crates/perry-runtime/src/object/spill.rs b/crates/perry-runtime/src/object/spill.rs index 22fbc843e8..4595d8f0d8 100644 --- a/crates/perry-runtime/src/object/spill.rs +++ b/crates/perry-runtime/src/object/spill.rs @@ -152,8 +152,43 @@ pub(crate) fn spill_set(obj_ptr: usize, field_index: usize, vbits: u64) { } } +#[cfg(test)] +pub(crate) type SpillSafepointHook = fn(usize); + +#[cfg(test)] +thread_local! { + static SPILL_SAFEPOINT_HOOK: std::cell::Cell> = + const { std::cell::Cell::new(None) }; +} + +#[cfg(test)] +pub(crate) fn test_set_spill_safepoint_hook( + hook: Option, +) -> Option { + SPILL_SAFEPOINT_HOOK.with(|slot| { + let previous = slot.get(); + slot.set(hook); + previous + }) +} + +#[cfg(test)] +#[inline] +fn spill_safepoint(obj_ptr: usize) { + SPILL_SAFEPOINT_HOOK.with(|slot| { + if let Some(hook) = slot.get() { + hook(obj_ptr); + } + }); +} + +#[cfg(not(test))] +#[inline] +fn spill_safepoint(_obj_ptr: usize) {} + /// Allocation path: ensure the meta record and a buffer wide enough for -/// `field_index`, then store. Roots the owner across the allocations. +/// `field_index`, then store. Roots the owner AND the incoming value across +/// the allocations. #[cold] fn spill_set_slow(obj_ptr: usize, field_index: usize, vbits: u64) { unsafe { @@ -166,7 +201,24 @@ fn spill_set_slow(obj_ptr: usize, field_index: usize, vbits: u64) { // minor GC. Reload through the handle after every allocation. let scope = crate::gc::RuntimeHandleScope::new(); let obj_handle = scope.root_raw_mut_ptr(obj); + // #7538: root the VALUE too. It arrives as plain `u64` bits — a + // by-value copy of a NaN-boxed pointer the caller may well have + // rooted, which does the callee no good: `object_meta_ensure` and + // `js_array_alloc_with_length` below are both collection points, and + // an evacuating minor at either one rewrites the caller's handle + // while this local keeps naming the from-space copy. The store at the + // end then publishes that address into a slot the collector has + // already finished rewriting, so the stale pointer is never fixed and + // never reported (its target is live, just relocated). Re-read the + // bits from the handle immediately before the store. + let value_handle = scope.root_nanbox_u64(vbits); object_meta_ensure(obj); + // Test-only stand-in for the collection `object_meta_ensure` (and the + // buffer allocation below) can genuinely take. Same pattern, and same + // reason, as `json_tape::json_tape_safepoint`: the window is one + // allocation wide, so a test that waits for the arena trigger to land + // in it is a coin flip. Compiles to nothing outside `cfg(test)`. + spill_safepoint(obj_handle.get_raw_mut_ptr::() as usize); let obj = obj_handle.get_raw_mut_ptr::(); let meta = (*obj).meta; let spill = (*meta).spill as *mut crate::array::ArrayHeader; @@ -212,7 +264,7 @@ fn spill_set_slow(obj_ptr: usize, field_index: usize, vbits: u64) { let obj = obj_handle.get_raw_mut_ptr::(); let meta = (*obj).meta; let spill = (*meta).spill as *mut crate::array::ArrayHeader; - spill_store_slot(spill, field_index, vbits); + spill_store_slot(spill, field_index, value_handle.get_nanbox_u64()); } } From ea21173cae1021941081d4610bedf81ab909ec02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 6 Aug 2026 20:47:04 +0200 Subject: [PATCH 2/6] fix(gc): decide external slot coverage by containment, not generation (#7500) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The remembered set has two coverage forms. A bare dirty old page is found again by walking the objects ON that page, so it only proves reachability for a slot inside a heap object whose own descriptor enumerates it. A slot in a buffer the parent merely points AT is reachable only through the parent's descriptor, and needs the `(page -> owner header)` external pair. Three places chose between them by generation — "not old implies external" — which is right for a malloc side buffer and wrong for every old-gen buffer a GC object owns but does not contain. The lazy JSON array's sparse element cache is exactly that: a separate GC_TYPE_STRING block, born old at >=2049 elements, readable only by the LazyArray rewrite descriptor on the owning LazyArrayHeader. * `json_tape::lazy_get`'s store (fixed in the previous commit), * the post-cycle sticky re-arm, which re-armed surviving edges the same inert way — so a cache slot went stale one collection AFTER its own barrier entry had been consumed and cleared, * the verifier's own `old_young_slot_covered`, which agreed with the wrong producer and could not see the hole. All three now share one containment predicate. PERRY_GC_VERIFY_EVACUATION runs clean on the tape workloads it used to abort on. Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- changelog.d/7539-tape-overflow-key-loss.md | 7 +- .../tests/runtime_roots/callback_scanners.rs | 14 ++-- crates/perry-runtime/src/gc/verify.rs | 76 ++++++++++++++++--- crates/perry-runtime/src/object/spill.rs | 2 +- 4 files changed, 78 insertions(+), 21 deletions(-) diff --git a/changelog.d/7539-tape-overflow-key-loss.md b/changelog.d/7539-tape-overflow-key-loss.md index 6621acdb77..c0f1613fa4 100644 --- a/changelog.d/7539-tape-overflow-key-loss.md +++ b/changelog.d/7539-tape-overflow-key-loss.md @@ -11,7 +11,12 @@ Two further holes of the same shape are closed alongside it: - `object/field_set_by_name/tail.rs` snapshotted the overflow store's bits (`let vbits = value.to_bits()`) *above* the `js_array_push` that grows the keys array — an allocation, so a collection point — and stored that dead copy after `refresh_roots_after_alloc!()` had refreshed `value`. Both overflow arms now derive the bits from the refreshed value, through a shared `overflow_store_bits` helper. - - `json_tape::lazy_get` recorded its sparse-cache store with the *in-object* write barrier (**#7500**). The cache is a separate `GC_TYPE_STRING` allocation, born old at ≥2049 elements, so that barrier marked the page the slot sits on; the minor's dirty-page scan then walked the objects on that page, found the cache's own leaf header, and scanned nothing. The only descriptor that can read those slots is `GcRewriteDescriptorKind::LazyArray` on the owner, whose pages stayed clean — so cached element pointers were neither marked nor rewritten. It now uses `runtime_write_barrier_external_slot`, which records `(slot page → owner header)`. This is what made `PERRY_GC_VERIFY_EVACUATION=1` abort on every `PERRY_JSON_TAPE=1` workload; that abort is gone. + - **`PERRY_GC_VERIFY_EVACUATION=1` aborting on every `PERRY_JSON_TAPE=1` workload (#7500)** was one mistake made in three places: *external* slot coverage was decided by GENERATION ("not old ⟹ external") instead of by CONTAINMENT. A bare dirty old page is found again by walking the objects on that page, so it only proves reachability for a slot living *inside* a heap object whose own descriptor enumerates it; a slot in a buffer the parent merely points at is reachable only via the parent's descriptor, which needs the `(page → owner header)` pair. The lazy array's sparse element cache is precisely that — a separate `GC_TYPE_STRING` block, born old at ≥2049 elements, readable only by `GcRewriteDescriptorKind::LazyArray` on the owning `LazyArrayHeader`. The three: + - `json_tape::lazy_get` recorded its cache store with the in-object barrier, so the minor's dirty-page scan walked the cache's own leaf header and scanned nothing → now `runtime_write_barrier_external_slot`; + - the post-cycle sticky re-arm (`gc/verify.rs::remember_evacuated_old_to_young_slot`) re-armed surviving edges the same inert way, so a cache slot went stale one collection *after* its own barrier entry had been consumed and cleared; + - the verifier's own `old_young_slot_covered` dispatched identically, so it agreed with the wrong producer and could not see the hole — and simultaneously reported the *correct* external form as missing. + + All three now share one `slot_is_external_to` containment predicate. `PERRY_GC_VERIFY_EVACUATION=1` runs clean on the tape workloads (reproducer, `bench_field_access`, `bench.ts` roundtrip), and `PERRY_GC_PROTECT_FROMSPACE=1` with depth 2000 takes no fault across 18 retired quarantine sets. `test_dirty_lazy_array_external_cache_scan_marks_bitmap_selected_child` had covered the *consumer* of that external entry since it landed — from an entry planted by hand. It was green the whole time no producer ever wrote one. diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs index 6c3b20b6de..3dc4707636 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs @@ -344,9 +344,10 @@ fn test_json_tape_lazy_get_records_its_cache_store_as_an_external_edge() { "the lazy header must be born old, or this test exercises nothing" ); - let scope = RuntimeHandleScope::new(); - let hdr_handle = scope.root_raw_mut_ptr(hdr); - let first = unsafe { crate::json_tape::lazy_get(hdr_handle.get_raw_mut_ptr(), 7) }; + // No handle scope: the header is old-gen (asserted above), automatic + // triggers are suppressed, and nothing here collects — so `hdr` cannot + // move for the length of this test. + let first = unsafe { crate::json_tape::lazy_get(hdr, 7) }; let element_addr = first.bits() & POINTER_MASK; assert_ne!( element_addr, 0, @@ -357,21 +358,20 @@ fn test_json_tape_lazy_get_records_its_cache_store_as_an_external_edge() { "the materialized element must be young, or no old→young edge exists" ); - let hdr_after = hdr_handle.get_raw_mut_ptr::(); let (cache_slot, header_addr) = unsafe { - let cache = (*hdr_after).materialized_elements; + let cache = (*hdr).materialized_elements; assert!( !cache.is_null(), "cold lazy_get should allocate the sparse cache" ); ( cache.add(7) as usize, - header_from_user_ptr(hdr_after as *const u8) as usize, + header_from_user_ptr(hdr as *const u8) as usize, ) }; assert_ne!( crate::arena::generation_page_for_addr(cache_slot), - crate::arena::generation_page_for_addr(hdr_after as usize), + crate::arena::generation_page_for_addr(hdr as usize), "cache and header must land on different pages, or the in-object barrier \ would have covered the slot by accident" ); diff --git a/crates/perry-runtime/src/gc/verify.rs b/crates/perry-runtime/src/gc/verify.rs index 7df3ca728b..2b98a272b5 100644 --- a/crates/perry-runtime/src/gc/verify.rs +++ b/crates/perry-runtime/src/gc/verify.rs @@ -121,11 +121,44 @@ pub(super) unsafe fn remember_evacuated_old_to_young_slot( if child_addr == 0 || !crate::gc::barrier::remembered_child_needs_tracking(child_addr) { return; } - let external = !matches!( + sticky.remember_slot( + parent_header, + slot, + slot_is_external_to(parent_header, slot), + ); +} + +/// Is `slot` outside `parent_header`'s own allocation, or on a page the +/// old-page modbuf cannot describe? +/// +/// #7538: this decides whether the re-arm records a bare page (found again by +/// walking the objects ON that page) or a `(page → owner header)` pair (found +/// again by re-entering the OWNER's descriptor). Deciding it by GENERATION — +/// "not old ⟹ external" — is right for a malloc side buffer and wrong for +/// every old-gen buffer a GC object owns but does not contain. The lazy JSON +/// array's sparse element cache is exactly that: a separate `GC_TYPE_STRING` +/// block, born old at ≥2049 elements, whose slots only +/// `GcRewriteDescriptorKind::LazyArray` on the owning `LazyArrayHeader` can +/// read. Re-armed as a bare old page, the next minor's dirty scan walked that +/// page, found the cache's own leaf header, and scanned nothing — so a cached +/// element pointer went stale one collection AFTER the store's own barrier +/// entry had been consumed and cleared. Containment is the question the +/// modbuf is actually asking. +#[inline] +unsafe fn slot_is_external_to(parent_header: *mut GcHeader, slot: *mut u64) -> bool { + if !matches!( crate::arena::classify_heap_generation(slot as usize), crate::arena::HeapGeneration::Old - ); - sticky.remember_slot(parent_header, slot, external); + ) { + return true; + } + let start = parent_header as usize; + let total_size = (*parent_header).size as usize; + if total_size == 0 { + return true; + } + let slot_addr = slot as usize; + slot_addr < start || slot_addr >= start + total_size } pub(super) unsafe fn remember_evacuated_old_copy_young_slots( @@ -389,21 +422,40 @@ pub(super) fn old_young_external_slot_covered( .any(|&(entry_page, entry_header)| entry_page == page && entry_header == parent_header) } +/// Can the next minor's dirty scan actually REACH `slot`? +/// +/// Two coverage forms, and which one applies is a question of CONTAINMENT, +/// not of generation (#7538). A bare dirty old page is found again by walking +/// the objects on it, so it only proves reachability for a slot that lives +/// inside a heap object whose own descriptor enumerates it. A slot in a buffer +/// the parent merely POINTS AT is reachable only through the parent's +/// descriptor, which needs the `(page → owner header)` external pair. +/// +/// Dispatching on generation instead accepted a bare page mark for any old-gen +/// external buffer. The lazy JSON array's sparse element cache is exactly +/// that — a separate `GC_TYPE_STRING` block, born old at ≥2049 elements, +/// readable only by `GcRewriteDescriptorKind::LazyArray` on the owning +/// `LazyArrayHeader` — so the verifier's own predicate agreed with the +/// producer's wrong barrier and neither could see the hole. +/// +/// An external pair is accepted for an in-object slot too: it strictly +/// implies reachability, and the malloc-parent barrier +/// (`runtime_write_barrier_gc_slot`) legitimately emits it. #[inline] -pub(super) fn old_young_slot_covered( +pub(super) unsafe fn old_young_slot_covered( snapshot: &RememberedDirtySnapshot, parent_header: usize, slot: *mut u64, ) -> bool { - let page = crate::arena::generation_page_for_addr(slot as usize); - if matches!( - crate::arena::classify_heap_generation(slot as usize), - crate::arena::HeapGeneration::Old - ) { - snapshot.dirty_old_pages.contains(&page) - } else { - old_young_external_slot_covered(snapshot, parent_header, slot) + if old_young_external_slot_covered(snapshot, parent_header, slot) { + return true; } + if slot_is_external_to(parent_header as *mut GcHeader, slot) { + return false; + } + snapshot + .dirty_old_pages + .contains(&crate::arena::generation_page_for_addr(slot as usize)) } #[inline] diff --git a/crates/perry-runtime/src/object/spill.rs b/crates/perry-runtime/src/object/spill.rs index 4595d8f0d8..5ec6852b43 100644 --- a/crates/perry-runtime/src/object/spill.rs +++ b/crates/perry-runtime/src/object/spill.rs @@ -218,7 +218,7 @@ fn spill_set_slow(obj_ptr: usize, field_index: usize, vbits: u64) { // reason, as `json_tape::json_tape_safepoint`: the window is one // allocation wide, so a test that waits for the arena trigger to land // in it is a coin flip. Compiles to nothing outside `cfg(test)`. - spill_safepoint(obj_handle.get_raw_mut_ptr::() as usize); + spill_safepoint(obj_ptr); let obj = obj_handle.get_raw_mut_ptr::(); let meta = (*obj).meta; let spill = (*meta).spill as *mut crate::array::ArrayHeader; From 72728bace495f533609fffecc4de4fbd5144567d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 6 Aug 2026 21:05:05 +0200 Subject: [PATCH 3/6] chore(changelog): key the fragment to the PR number (#7546) Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- ...9-tape-overflow-key-loss.md => 7546-tape-overflow-key-loss.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{7539-tape-overflow-key-loss.md => 7546-tape-overflow-key-loss.md} (100%) diff --git a/changelog.d/7539-tape-overflow-key-loss.md b/changelog.d/7546-tape-overflow-key-loss.md similarity index 100% rename from changelog.d/7539-tape-overflow-key-loss.md rename to changelog.d/7546-tape-overflow-key-loss.md From c02daff4861c448b80cf7e60ed2e483c3168e8d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 6 Aug 2026 21:20:40 +0200 Subject: [PATCH 4/6] perf(gc): collapse adjacent duplicate external sticky entries (#7538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An owner's external buffer contributes one entry per SLOT — a lazy JSON array's sparse element cache is one 8-byte slot per element — and the re-arm visits them in address order, so a single adjacent-duplicate check collapses a whole page's worth of pushes into one. `restore` already dedupes inside `mark_dirty_external_slot_page`; this keeps the intermediate Vec from growing with the element count now that old-gen external buffers take this path. Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- crates/perry-runtime/src/gc/copying.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/perry-runtime/src/gc/copying.rs b/crates/perry-runtime/src/gc/copying.rs index 2781fd5341..18f0691192 100644 --- a/crates/perry-runtime/src/gc/copying.rs +++ b/crates/perry-runtime/src/gc/copying.rs @@ -356,7 +356,17 @@ impl StickyRememberedSet { } let page = crate::arena::generation_page_for_addr(slot as usize); if external { - self.external_pages.push((parent_header as usize, page)); + // #7538: an owner's external buffer can contribute thousands of + // slots (a lazy JSON array's sparse element cache is one 8-byte + // slot per element), and they are visited in address order — so + // one adjacent-duplicate check collapses a whole page's worth of + // pushes into a single entry. `restore` dedupes again inside + // `mark_dirty_external_slot_page`; this keeps the intermediate + // Vec from growing with the element count. + let entry = (parent_header as usize, page); + if self.external_pages.last() != Some(&entry) { + self.external_pages.push(entry); + } } else { self.old_pages.insert(page); } From daa3f83330d69010187c9531aaf0ed0d55c452d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 6 Aug 2026 21:44:32 +0200 Subject: [PATCH 5/6] docs(changelog): note the tail.rs sibling hole is not independently reproduced Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- changelog.d/7546-tape-overflow-key-loss.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/7546-tape-overflow-key-loss.md b/changelog.d/7546-tape-overflow-key-loss.md index c0f1613fa4..92358df8c8 100644 --- a/changelog.d/7546-tape-overflow-key-loss.md +++ b/changelog.d/7546-tape-overflow-key-loss.md @@ -10,7 +10,7 @@ Two further holes of the same shape are closed alongside it: - - `object/field_set_by_name/tail.rs` snapshotted the overflow store's bits (`let vbits = value.to_bits()`) *above* the `js_array_push` that grows the keys array — an allocation, so a collection point — and stored that dead copy after `refresh_roots_after_alloc!()` had refreshed `value`. Both overflow arms now derive the bits from the refreshed value, through a shared `overflow_store_bits` helper. + - `object/field_set_by_name/tail.rs` snapshotted the overflow store's bits (`let vbits = value.to_bits()`) *above* the `js_array_push` that grows the keys array — an allocation, so a collection point — and stored that dead copy after `refresh_roots_after_alloc!()` had refreshed `value`; the macro cannot reach a copy taken before it. Both overflow arms now derive the bits from the refreshed value, through a shared `overflow_store_bits` helper. Not independently reproduced — found by reading the same statement sequence, and closed because a live stale local on a shipped path is a hazard whether or not a current workload lands in its window. - **`PERRY_GC_VERIFY_EVACUATION=1` aborting on every `PERRY_JSON_TAPE=1` workload (#7500)** was one mistake made in three places: *external* slot coverage was decided by GENERATION ("not old ⟹ external") instead of by CONTAINMENT. A bare dirty old page is found again by walking the objects on that page, so it only proves reachability for a slot living *inside* a heap object whose own descriptor enumerates it; a slot in a buffer the parent merely points at is reachable only via the parent's descriptor, which needs the `(page → owner header)` pair. The lazy array's sparse element cache is precisely that — a separate `GC_TYPE_STRING` block, born old at ≥2049 elements, readable only by `GcRewriteDescriptorKind::LazyArray` on the owning `LazyArrayHeader`. The three: - `json_tape::lazy_get` recorded its cache store with the in-object barrier, so the minor's dirty-page scan walked the cache's own leaf header and scanned nothing → now `runtime_write_barrier_external_slot`; - the post-cycle sticky re-arm (`gc/verify.rs::remember_evacuated_old_to_young_slot`) re-armed surviving edges the same inert way, so a cache slot went stale one collection *after* its own barrier entry had been consumed and cleared; From 43b9a2fa74eef6c964d4e808d288a900cc14fc64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 6 Aug 2026 22:02:10 +0200 Subject: [PATCH 6/6] chore: bump version to 0.5.1309 --- 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 bf377a4a51..636e685f20 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.1308 +**Current Version:** 0.5.1309 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 4df908da33..7eac6d952b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1308" +version = "0.5.1309" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1308" +version = "0.5.1309" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1308" +version = "0.5.1309" [[package]] name = "perry-ui-tvos" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1308" +version = "0.5.1309" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 87407c2363..5a21b29397 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1308" +version = "0.5.1309" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"