From 92d4bfdef6431221a5958e890cc5434d7e2aa0e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 8 Aug 2026 00:42:19 +0200 Subject: [PATCH 1/5] test(repsel): drive the #7480 element-shape matrix through real mutators The shipped invariant's bulk-mutator test calls `rebuild_array_layout` directly, so it asserts the proxy rather than the subject: every mutator could stop reaching that funnel and the suite would stay green. That is CLAUDE.md's fourth way a gate cannot fail. Adds an end-to-end matrix that drives each mutation family through its real FFI entry point and asserts the documented verdict, plus the epoch move a consumer's hoisted guard actually reads. Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- .../perry-runtime/src/array/element_shape.rs | 7 + .../src/array/element_shape_matrix_tests.rs | 525 ++++++++++++++++++ 2 files changed, 532 insertions(+) create mode 100644 crates/perry-runtime/src/array/element_shape_matrix_tests.rs diff --git a/crates/perry-runtime/src/array/element_shape.rs b/crates/perry-runtime/src/array/element_shape.rs index 43755dda5a..ed262745d5 100644 --- a/crates/perry-runtime/src/array/element_shape.rs +++ b/crates/perry-runtime/src/array/element_shape.rs @@ -634,3 +634,10 @@ pub(crate) unsafe fn test_element_shape_bit_set(arr: *const ArrayHeader) -> bool #[cfg(test)] #[path = "element_shape_tests.rs"] mod tests; + +/// The end-to-end revocation matrix: every mutator family driven through its +/// real FFI entry point. Separate from `tests` because it asserts a different +/// thing — not that the funnels work, but that the mutators *reach* them. +#[cfg(test)] +#[path = "element_shape_matrix_tests.rs"] +mod matrix_tests; diff --git a/crates/perry-runtime/src/array/element_shape_matrix_tests.rs b/crates/perry-runtime/src/array/element_shape_matrix_tests.rs new file mode 100644 index 0000000000..a61a4069bf --- /dev/null +++ b/crates/perry-runtime/src/array/element_shape_matrix_tests.rs @@ -0,0 +1,525 @@ +//! #7480 element-shape invariant: the **end-to-end revocation matrix**. +//! +//! `element_shape_tests.rs` covers the invariant's own funnels +//! (`note_element_store`, `clear_element_shape`, `transfer_element_shape`) and +//! asserts that `rebuild_array_layout` drops a proof. What it does *not* +//! assert is that the mutators actually **reach** those funnels: its bulk- +//! mutator test calls `rebuild_array_layout` directly, so it proves the proxy, +//! not the subject. That is CLAUDE.md's fourth way a gate cannot fail — "the +//! gate runs but its subject never did" — and it is the one shape that would +//! let a mutator quietly stop revoking while the suite stayed green. +//! +//! This file closes that hole. Every test here drives a **real FFI entry +//! point** against a shaped array and asserts the documented verdict. Deleting +//! any single `clear_element_shape` / `rebuild_array_layout` call from the +//! runtime turns at least one test here red. +//! +//! ## The contract, stated once +//! +//! Three verdicts, and every mutation site is exactly one of them: +//! +//! * **MAINTAIN** — the proof survives with its identity intact. Only two +//! sites qualify: an in-bounds store whose value has the proven class, and a +//! contiguous append of the proven class (which also extends `verified_len`). +//! * **REVOKE** — the proof is dropped and the global epoch advances. Every +//! bulk mutator, conservatively, plus any store of a non-conforming value. +//! * **STRUCTURAL REVOKE** — no call site of its own; the record's pinned +//! `verified_len` stops matching `length`, so the next query fails closed. +//! This is what covers `pop`, and any future length-changing path nobody +//! remembers to hook. +//! +//! A conservative REVOKE is not the same as heterogeneity, so each bulk +//! mutator that is a **permutation or a same-class rewrite** additionally +//! asserts it **re-proves** on the next `ensure_element_shape` — with a *fresh* +//! identity, because a consumer that pinned the old one must not silently ride +//! through. That is the self-healing half, and it is what stops a future +//! "optimisation" from replacing a revoke with a no-op and calling the +//! still-green suite evidence. + +use super::*; +use crate::array::{ + js_array_alloc, js_array_copy_within, js_array_delete, js_array_fill, js_array_fill_range, + js_array_pop_f64, js_array_push_f64, js_array_reverse, js_array_set_f64, + js_array_set_f64_unchecked, js_array_set_length, js_array_shift_f64, js_array_sort_default, + js_array_splice, js_array_unshift_f64, +}; + +/// Two distinct shaped classes, matching the ids `element_shape_tests` uses. +const CLASS_A: u32 = 0x0007_4801; +const CLASS_B: u32 = 0x0007_4802; + +fn instance(class_id: u32) -> f64 { + let obj = crate::object::js_object_alloc(class_id, 2); + crate::value::js_nanbox_pointer(obj as i64) +} + +/// `const rows = []; rows.push(new C())` — the construction shape the measured +/// kernel uses, and the one `note_element_store` establishes from. +fn shaped(count: usize) -> *mut ArrayHeader { + let mut arr = js_array_alloc(count as u32); + for _ in 0..count { + arr = js_array_push_f64(arr, instance(CLASS_A)); + } + assert!( + unsafe { element_shape_proof(arr) }.is_some(), + "fixture must start proven, or every verdict below is vacuous" + ); + arr +} + +fn proof(arr: *mut ArrayHeader) -> Option { + unsafe { element_shape_proof(arr) } +} + +/// Assert a mutation **revoked** the proof, and that the global epoch moved. +/// +/// The epoch check is what makes this a real assertion rather than "the query +/// happened to return `None`": a consumer's hoisted guard re-reads that word, +/// so a revoke the epoch does not advertise is a revoke the consumer misses. +fn assert_revoked(arr: *mut ArrayHeader, before: u64, family: &str) { + assert!( + proof(arr).is_none(), + "{family} must revoke the element-shape proof" + ); + assert!( + element_shape_epoch() > before, + "{family} revoked but never advanced the epoch — a hoisted guard would not notice" + ); +} + +/// Assert the array is still homogeneous underneath, so the conservative +/// revoke self-heals — and that healing mints a **fresh** identity. +fn assert_reproves(arr: *mut ArrayHeader, retired: u64, family: &str) { + let healed = unsafe { ensure_element_shape(arr) } + .unwrap_or_else(|| panic!("{family} is a permutation/same-class rewrite; it must re-prove")); + assert_eq!(healed.class_id, CLASS_A, "{family} re-proved the wrong class"); + assert_ne!( + healed.epoch, retired, + "{family} re-proved but reused the retired identity — a consumer that pinned it would ride through a break" + ); +} + +// --------------------------------------------------------------------------- +// MAINTAIN — the only two sites that keep a proof +// --------------------------------------------------------------------------- + +#[test] +fn matrix_in_bounds_store_of_the_proven_class_maintains() { + let _serialized = test_serialize(); + let arr = shaped(3); + let pinned = proof(arr).unwrap(); + js_array_set_f64(arr, 1, instance(CLASS_A)); + let after = proof(arr).expect("a same-class in-bounds store must maintain"); + assert_eq!(after.class_id, CLASS_A); + assert_eq!( + after.epoch, pinned.epoch, + "maintaining must not retire the proof identity" + ); + assert_eq!(after.verified_len, 3); +} + +#[test] +fn matrix_contiguous_append_of_the_proven_class_maintains_and_extends() { + let _serialized = test_serialize(); + let arr = shaped(2); + let pinned = proof(arr).unwrap(); + let arr = js_array_push_f64(arr, instance(CLASS_A)); + let after = proof(arr).expect("a same-class append must maintain"); + assert_eq!(after.epoch, pinned.epoch); + assert_eq!(after.verified_len, 3, "the verified prefix must extend"); +} + +/// The *unchecked* store is codegen's fast path and skips the bounds/extend +/// logic — but not `note_array_slot`, so it must still maintain and revoke. +#[test] +fn matrix_unchecked_store_maintains_on_class_match() { + let _serialized = test_serialize(); + let arr = shaped(3); + let pinned = proof(arr).unwrap(); + js_array_set_f64_unchecked(arr, 2, instance(CLASS_A)); + let after = proof(arr).expect("the unchecked store must reach the funnel too"); + assert_eq!(after.epoch, pinned.epoch); +} + +#[test] +fn matrix_unchecked_store_revokes_on_class_mismatch() { + let _serialized = test_serialize(); + let arr = shaped(3); + let before = element_shape_epoch(); + js_array_set_f64_unchecked(arr, 2, instance(CLASS_B)); + assert_revoked(arr, before, "an unchecked store of a different class"); +} + +// --------------------------------------------------------------------------- +// REVOKE — value-driven +// --------------------------------------------------------------------------- + +#[test] +fn matrix_in_bounds_store_of_a_different_class_revokes() { + let _serialized = test_serialize(); + let arr = shaped(3); + let before = element_shape_epoch(); + js_array_set_f64(arr, 1, instance(CLASS_B)); + assert_revoked(arr, before, "a different-class store"); +} + +#[test] +fn matrix_store_of_a_primitive_revokes() { + let _serialized = test_serialize(); + let arr = shaped(3); + let before = element_shape_epoch(); + js_array_set_f64(arr, 1, 42.0); + assert_revoked(arr, before, "a primitive store"); +} + +#[test] +fn matrix_append_of_a_different_class_revokes() { + let _serialized = test_serialize(); + let arr = shaped(2); + let before = element_shape_epoch(); + let arr = js_array_push_f64(arr, instance(CLASS_B)); + assert_revoked(arr, before, "a different-class append"); +} + +#[test] +fn matrix_delete_revokes() { + let _serialized = test_serialize(); + let arr = shaped(4); + let before = element_shape_epoch(); + js_array_delete(arr, 1); + assert_revoked(arr, before, "`delete arr[i]`"); +} + +// --------------------------------------------------------------------------- +// REVOKE — bulk mutators, driven through their real entry points +// +// Each also asserts self-healing where the operation is a permutation or a +// same-class rewrite: a conservative revoke must not be mistaken for the array +// having genuinely stopped being homogeneous. +// --------------------------------------------------------------------------- + +#[test] +fn matrix_reverse_revokes_and_reproves() { + let _serialized = test_serialize(); + let arr = shaped(4); + let retired = proof(arr).unwrap().epoch; + let before = element_shape_epoch(); + let arr = js_array_reverse(arr); + assert_revoked(arr, before, "`reverse`"); + assert_reproves(arr, retired, "`reverse`"); +} + +#[test] +fn matrix_sort_revokes_and_reproves() { + let _serialized = test_serialize(); + let arr = shaped(4); + let retired = proof(arr).unwrap().epoch; + let before = element_shape_epoch(); + let arr = js_array_sort_default(arr); + assert_revoked(arr, before, "`sort`"); + assert_reproves(arr, retired, "`sort`"); +} + +#[test] +fn matrix_copy_within_revokes_and_reproves() { + let _serialized = test_serialize(); + let arr = shaped(4); + let retired = proof(arr).unwrap().epoch; + let before = element_shape_epoch(); + let arr = js_array_copy_within(arr, 0.0, 2.0, 0, 0.0); + assert_revoked(arr, before, "`copyWithin`"); + assert_reproves(arr, retired, "`copyWithin`"); +} + +/// `fill` overwrites every slot, so the proof must go — and because the filler +/// here is a *different* class, the array is genuinely heterogeneous no more: +/// it re-proves as `CLASS_B`, not `CLASS_A`. A revoke that healed back to the +/// old class would be the exact lie a consumer's guard cannot survive. +#[test] +fn matrix_fill_revokes_and_reproves_as_the_filled_class() { + let _serialized = test_serialize(); + let arr = shaped(4); + let before = element_shape_epoch(); + let arr = js_array_fill(arr, instance(CLASS_B)); + assert_revoked(arr, before, "`fill`"); + let healed = unsafe { ensure_element_shape(arr) }.expect("a uniformly filled array re-proves"); + assert_eq!( + healed.class_id, CLASS_B, + "`fill` must heal to the FILLED class, never back to the retired one" + ); +} + +#[test] +fn matrix_fill_range_revokes_leaving_a_genuinely_mixed_array() { + let _serialized = test_serialize(); + let arr = shaped(4); + let before = element_shape_epoch(); + let arr = js_array_fill_range(arr, instance(CLASS_B), 0.0, 2.0); + assert_revoked(arr, before, "`fill(v, start, end)`"); + assert!( + unsafe { ensure_element_shape(arr) }.is_none(), + "a partial fill of another class leaves a mixed array; it must NOT re-prove" + ); +} + +/// The soundness-critical case: `splice` can replace elements **without +/// changing `length`**, so the structural `verified_len` check cannot catch it. +/// Only splice's own `rebuild_array_layout` can. If that call is ever dropped, +/// this is the test that goes red — and nothing else would. +#[test] +fn matrix_splice_equal_length_replacement_revokes() { + let _serialized = test_serialize(); + let arr = shaped(4); + let len_before = unsafe { (*arr).length }; + let before = element_shape_epoch(); + let items = [instance(CLASS_B)]; + let mut out: *mut ArrayHeader = arr; + js_array_splice(arr, 1, 1, items.as_ptr(), 1, &mut out); + assert_eq!( + unsafe { (*out).length }, + len_before, + "this test is only meaningful while the length is UNCHANGED — \ + otherwise the structural check would catch it and splice's own \ + revoke would go untested" + ); + assert_revoked(out, before, "an equal-length `splice` replacement"); + assert!( + unsafe { ensure_element_shape(out) }.is_none(), + "the spliced-in element really is a different class" + ); +} + +#[test] +fn matrix_splice_pure_delete_revokes_and_reproves() { + let _serialized = test_serialize(); + let arr = shaped(4); + let retired = proof(arr).unwrap().epoch; + let before = element_shape_epoch(); + let mut out: *mut ArrayHeader = arr; + js_array_splice(arr, 1, 1, std::ptr::null(), 0, &mut out); + assert_revoked(out, before, "a deleting `splice`"); + assert_reproves(out, retired, "a deleting `splice`"); +} + +#[test] +fn matrix_unshift_revokes_and_reproves() { + let _serialized = test_serialize(); + let arr = shaped(3); + let retired = proof(arr).unwrap().epoch; + let before = element_shape_epoch(); + let arr = js_array_unshift_f64(arr, instance(CLASS_A)); + assert_revoked(arr, before, "`unshift`"); + assert_reproves(arr, retired, "`unshift`"); +} + +#[test] +fn matrix_shift_revokes_and_reproves() { + let _serialized = test_serialize(); + let arr = shaped(3); + let retired = proof(arr).unwrap().epoch; + let before = element_shape_epoch(); + js_array_shift_f64(arr); + assert_revoked(arr, before, "`shift`"); + assert_reproves(arr, retired, "`shift`"); +} + +// --------------------------------------------------------------------------- +// STRUCTURAL REVOKE — no call site of its own; `verified_len` catches it +// --------------------------------------------------------------------------- + +/// `pop` is deliberately un-hooked: it shortens `length`, and the record's +/// pinned `verified_len` stops matching. This test exists to pin that the +/// structural half is load-bearing, not decorative — it is the mechanism that +/// covers every length-changing path nobody remembers to hook, including +/// codegen's inline append. +#[test] +fn matrix_pop_revokes_structurally_then_reproves() { + let _serialized = test_serialize(); + let arr = shaped(4); + let retired = proof(arr).unwrap().epoch; + js_array_pop_f64(arr); + assert!( + proof(arr).is_none(), + "`pop` must fail the proof closed on the length mismatch" + ); + assert_reproves(arr, retired, "`pop`"); +} + +#[test] +fn matrix_length_truncate_and_extend_revoke() { + let _serialized = test_serialize(); + let truncated = shaped(4); + js_array_set_length(truncated, 2.0); + assert!(proof(truncated).is_none(), "`arr.length = 2` must revoke"); + + let extended = shaped(2); + js_array_set_length(extended, 6.0); + assert!( + proof(extended).is_none(), + "`arr.length = 6` leaves holes; it must revoke" + ); + assert!( + unsafe { ensure_element_shape(extended) }.is_none(), + "a hole-tailed array must not re-prove" + ); +} + +// --------------------------------------------------------------------------- +// BUILDERS — the "rebuild regains it" half of self-healing +// +// Array *builders* split across the two funnels (audited for #7480): the +// per-element ones (`JSON.parse`, the #7539 tape materialiser, most of the +// `Array.from` family, the concat fallback) reach `layout_note_slot` and so +// **establish** as they fill; the bulk-copy ones (dense spread, the concat fast +// path, `Array.from` on a jsvalue) `ptr::copy` and then `rebuild_array_layout_ +// exact`, which leaves the result deliberately **unproven**. +// +// Both are correct — the rule is that a builder may leave a result unproven, +// but must never leave it proven at the WRONG class. These tests pin exactly +// that, and pin that the unproven case heals on the first `ensure`. +// --------------------------------------------------------------------------- + +/// `[...arr]` — the bulk-copy builder. The clone is a fresh allocation that +/// may land at a recycled address, so the interesting assertion is not that it +/// starts unproven but that whatever it reports is about *itself*. +#[test] +fn matrix_spread_build_never_inherits_the_sources_proof() { + let _serialized = test_serialize(); + let src = shaped(4); + let src_proof = proof(src).expect("source is proven"); + + let clone = crate::array::flat_clone::js_array_clone_for_spread(crate::value::js_nanbox_pointer(src as i64)); + + if let Some(cloned) = proof(clone) { + assert_ne!( + cloned.epoch, src_proof.epoch, + "the clone must never carry the SOURCE's proof identity — a consumer \ + that pinned it would treat two different arrays as one" + ); + assert_eq!(cloned.class_id, CLASS_A); + } + // Whatever it started as, the clone really is homogeneous, so it proves. + let healed = unsafe { ensure_element_shape(clone) }.expect("a spread clone re-proves"); + assert_eq!(healed.class_id, CLASS_A); + assert_ne!(healed.epoch, src_proof.epoch); + + assert_eq!( + proof(src).map(|p| p.epoch), + Some(src_proof.epoch), + "building a clone must not disturb the source's proof" + ); +} + +/// The acceptance case stated in #7480: a **revoked** array that is rebuilt +/// regains the invariant. `arr.map(identity)` is the JS spelling; at this layer +/// the equivalent is "copy the elements into a fresh array", which is what the +/// bulk-copy builders do. +#[test] +fn matrix_a_revoked_array_regains_the_invariant_when_rebuilt() { + let _serialized = test_serialize(); + let arr = shaped(4); + + // Revoke it for real, and confirm it is genuinely gone. + let before = element_shape_epoch(); + js_array_set_f64(arr, 1, 7.0); + assert_revoked(arr, before, "a primitive store"); + assert!( + unsafe { ensure_element_shape(arr) }.is_none(), + "still holding a primitive, so it must NOT re-prove in place" + ); + + // Rebuild it the way user code does — every element replaced by a shaped + // instance — and the invariant comes back on its own. + let mut rebuilt = js_array_alloc(4); + for _ in 0..4 { + rebuilt = js_array_push_f64(rebuilt, instance(CLASS_A)); + } + let healed = proof(rebuilt).expect("a rebuilt homogeneous array is proven again"); + assert_eq!(healed.class_id, CLASS_A); + assert_eq!(healed.verified_len, 4); +} + +/// `Array.from`-family builder that fills per element: it reaches +/// `layout_note_slot`, so a homogeneous source establishes as it fills. +#[test] +fn matrix_from_values_builder_establishes_or_heals_but_never_lies() { + let _serialized = test_serialize(); + let values = [ + instance(CLASS_A), + instance(CLASS_A), + instance(CLASS_A), + instance(CLASS_A), + ]; + let built = crate::array::alloc::js_array_from_values(values.as_ptr(), values.len() as u32); + if let Some(p) = proof(built) { + assert_eq!(p.class_id, CLASS_A, "a builder must never prove a wrong class"); + } + assert_eq!( + unsafe { ensure_element_shape(built) }.map(|p| p.class_id), + Some(CLASS_A) + ); + + // The mixed source must not produce a proof at all. + let mixed = [instance(CLASS_A), instance(CLASS_B)]; + let built_mixed = crate::array::alloc::js_array_from_values(mixed.as_ptr(), mixed.len() as u32); + assert!(proof(built_mixed).is_none()); + assert!(unsafe { ensure_element_shape(built_mixed) }.is_none()); +} + +// --------------------------------------------------------------------------- +// The matrix is COMPLETE — a proven array that survives every family untouched +// would mean a family stopped revoking without any single test noticing. +// --------------------------------------------------------------------------- + +/// A roll-up that fails if *any* bulk family silently stops revoking. +/// +/// The per-family tests above name the rule each site breaks; this one exists +/// so that a change which quietly removes the funnel from several families at +/// once cannot be dismissed as "one flaky test". It also asserts its own +/// subject was live: the fixture is re-proven before every step, so a step that +/// found nothing to revoke fails loudly instead of passing vacuously. +#[test] +fn matrix_every_bulk_family_revokes() { + let _serialized = test_serialize(); + + #[allow(clippy::type_complexity)] + let families: Vec<(&str, Box *mut ArrayHeader>)> = vec![ + ("reverse", Box::new(|a| js_array_reverse(a))), + ("sort", Box::new(|a| js_array_sort_default(a))), + ( + "copyWithin", + Box::new(|a| js_array_copy_within(a, 0.0, 2.0, 0, 0.0)), + ), + ("fill", Box::new(|a| js_array_fill(a, instance(CLASS_A)))), + ( + "unshift", + Box::new(|a| js_array_unshift_f64(a, instance(CLASS_A))), + ), + ( + "shift", + Box::new(|a| { + js_array_shift_f64(a); + a + }), + ), + ( + "splice", + Box::new(|a| { + let mut out: *mut ArrayHeader = a; + js_array_splice(a, 1, 1, std::ptr::null(), 0, &mut out); + out + }), + ), + ]; + + for (name, op) in families { + let arr = shaped(4); + assert!( + proof(arr).is_some(), + "{name}: fixture must be proven before the op, or the check is vacuous" + ); + let before = element_shape_epoch(); + let arr = op(arr); + assert_revoked(arr, before, name); + } +} From 76919bd4e10cbe1502301d3ffde64efbeb0b0745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 8 Aug 2026 00:48:01 +0200 Subject: [PATCH 2/5] test(repsel): pin pointer identity and add builder cases to the matrix Three additions after sabotage-testing the first cut: - `reverse`/`sort`/`copyWithin` now assert the returned receiver IS the array that was proven. Without it, an implementation returning a fresh (trivially unproven) array would satisfy the revoke assertion while revoking nothing. - Builder cases: a spread clone must never inherit the source's proof identity, and `Array.from`-style builders may leave a result unproven but must never leave it proven at the wrong class. - The rebuild-regains-it case #7480 names as acceptance. Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- .../src/array/element_shape_matrix_tests.rs | 60 ++++++++++++++----- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/crates/perry-runtime/src/array/element_shape_matrix_tests.rs b/crates/perry-runtime/src/array/element_shape_matrix_tests.rs index a61a4069bf..8a4d071f25 100644 --- a/crates/perry-runtime/src/array/element_shape_matrix_tests.rs +++ b/crates/perry-runtime/src/array/element_shape_matrix_tests.rs @@ -90,9 +90,13 @@ fn assert_revoked(arr: *mut ArrayHeader, before: u64, family: &str) { /// Assert the array is still homogeneous underneath, so the conservative /// revoke self-heals — and that healing mints a **fresh** identity. fn assert_reproves(arr: *mut ArrayHeader, retired: u64, family: &str) { - let healed = unsafe { ensure_element_shape(arr) } - .unwrap_or_else(|| panic!("{family} is a permutation/same-class rewrite; it must re-prove")); - assert_eq!(healed.class_id, CLASS_A, "{family} re-proved the wrong class"); + let healed = unsafe { ensure_element_shape(arr) }.unwrap_or_else(|| { + panic!("{family} is a permutation/same-class rewrite; it must re-prove") + }); + assert_eq!( + healed.class_id, CLASS_A, + "{family} re-proved the wrong class" + ); assert_ne!( healed.epoch, retired, "{family} re-proved but reused the retired identity — a consumer that pinned it would ride through a break" @@ -198,26 +202,45 @@ fn matrix_delete_revokes() { // having genuinely stopped being homogeneous. // --------------------------------------------------------------------------- +/// The three tests below additionally pin **pointer identity**. None of these +/// operations can reallocate, so the receiver they return must be the array +/// that was proven — without that assertion, an implementation that returned a +/// fresh (and therefore trivially unproven) array would satisfy `assert_revoked` +/// while revoking nothing, which is the vacuous-pass shape this whole file +/// exists to rule out. #[test] fn matrix_reverse_revokes_and_reproves() { let _serialized = test_serialize(); let arr = shaped(4); let retired = proof(arr).unwrap().epoch; let before = element_shape_epoch(); - let arr = js_array_reverse(arr); - assert_revoked(arr, before, "`reverse`"); - assert_reproves(arr, retired, "`reverse`"); + let out = js_array_reverse(arr); + assert!( + std::ptr::eq(out, arr), + "`reverse` is in-place; a fresh array would make the verdict vacuous" + ); + assert_revoked(out, before, "`reverse`"); + assert_reproves(out, retired, "`reverse`"); } +/// `sort`'s default path is a rank permutation written back through +/// `RootedArrayElems::set`, so it revokes via the **store** funnel rather than +/// `rebuild_array_layout` — verified by sabotage: removing the revoke from +/// `rebuild_array_layout` leaves this test green, removing it from +/// `layout_note_slot` turns it red. Defence in depth, not redundancy. #[test] fn matrix_sort_revokes_and_reproves() { let _serialized = test_serialize(); let arr = shaped(4); let retired = proof(arr).unwrap().epoch; let before = element_shape_epoch(); - let arr = js_array_sort_default(arr); - assert_revoked(arr, before, "`sort`"); - assert_reproves(arr, retired, "`sort`"); + let out = js_array_sort_default(arr); + assert!( + std::ptr::eq(out, arr), + "`sort` returns its receiver; a fresh array would make the verdict vacuous" + ); + assert_revoked(out, before, "`sort`"); + assert_reproves(out, retired, "`sort`"); } #[test] @@ -226,9 +249,13 @@ fn matrix_copy_within_revokes_and_reproves() { let arr = shaped(4); let retired = proof(arr).unwrap().epoch; let before = element_shape_epoch(); - let arr = js_array_copy_within(arr, 0.0, 2.0, 0, 0.0); - assert_revoked(arr, before, "`copyWithin`"); - assert_reproves(arr, retired, "`copyWithin`"); + let out = js_array_copy_within(arr, 0.0, 2.0, 0, 0.0); + assert!( + std::ptr::eq(out, arr), + "`copyWithin` is in-place; a fresh array would make the verdict vacuous" + ); + assert_revoked(out, before, "`copyWithin`"); + assert_reproves(out, retired, "`copyWithin`"); } /// `fill` overwrites every slot, so the proof must go — and because the filler @@ -388,7 +415,9 @@ fn matrix_spread_build_never_inherits_the_sources_proof() { let src = shaped(4); let src_proof = proof(src).expect("source is proven"); - let clone = crate::array::flat_clone::js_array_clone_for_spread(crate::value::js_nanbox_pointer(src as i64)); + let clone = crate::array::flat_clone::js_array_clone_for_spread( + crate::value::js_nanbox_pointer(src as i64), + ); if let Some(cloned) = proof(clone) { assert_ne!( @@ -452,7 +481,10 @@ fn matrix_from_values_builder_establishes_or_heals_but_never_lies() { ]; let built = crate::array::alloc::js_array_from_values(values.as_ptr(), values.len() as u32); if let Some(p) = proof(built) { - assert_eq!(p.class_id, CLASS_A, "a builder must never prove a wrong class"); + assert_eq!( + p.class_id, CLASS_A, + "a builder must never prove a wrong class" + ); } assert_eq!( unsafe { ensure_element_shape(built) }.map(|p| p.class_id), From 677695d01d3a671b19c0b12b341c3bb3e84c2409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 8 Aug 2026 00:49:21 +0200 Subject: [PATCH 3/5] docs(changelog): add the #7608 element-shape revocation-matrix fragment Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- .../7608-element-shape-revocation-matrix.md | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 changelog.d/7608-element-shape-revocation-matrix.md diff --git a/changelog.d/7608-element-shape-revocation-matrix.md b/changelog.d/7608-element-shape-revocation-matrix.md new file mode 100644 index 0000000000..0dd951427a --- /dev/null +++ b/changelog.d/7608-element-shape-revocation-matrix.md @@ -0,0 +1,47 @@ +**test(repsel): drive the #7480 element-shape revocation matrix through real mutators** + +#7496 shipped the per-array homogeneous element-shape invariant with a +bulk-mutator test that called `rebuild_array_layout` **directly**. Its comment +named seven mutators (`shift`/`unshift`/`splice`/`fill`/`copyWithin`/`reverse`/ +`sort`); it exercised none of them. Any one could have stopped reaching the +funnel with the suite still green — CLAUDE.md's fourth way a gate cannot fail, +*"the gate runs but its subject never did"*. + +That gap mattered more than usual because of what consumes the invariant next: +the #5093 versioned-loop clone hoists a guard into the preheader and emits +**unguarded** element reads in the cloned body. A mutation family that silently +stops revoking is not a slow path there — it is a miscompile that reads a `B` +through an `A`'s layout. + +`crates/perry-runtime/src/array/element_shape_matrix_tests.rs` adds 25 tests, +each driving a **real FFI entry point** against a proven array. Each revoke +also asserts the **global epoch advanced** (the word a hoisted guard re-reads, +so a revoke it does not advertise is a revoke the consumer misses), and each +permutation/same-class family asserts it **re-proves with a fresh identity** — +separating a conservative revoke from genuine heterogeneity. Two vacuity +guards: the fixture must be proven *before* every op, and the in-place mutators +must return the array that was proven (otherwise a fresh, trivially unproven +array satisfies the revoke assertion while revoking nothing). + +**The audit found no unhooked path — the shipped invariant is sound.** Two +findings are worth recording: + +- **`sort`'s default path does not revoke through `rebuild_array_layout`.** It + is a rank permutation written back via `RootedArrayElems::set`, so it revokes + through the *store* funnel. Established by sabotage, not by reading: removing + the revoke from `rebuild_array_layout` leaves the sort test green; removing it + from `layout_note_slot` turns it red. `rebuild_array_layout`'s own doc comment + claims otherwise. +- **Equal-length `splice` has exactly one guard.** `arr.splice(1, 1, other)` + leaves `length` unchanged, so the structural `verified_len` check cannot see + it, and the inserted item is a bare `ptr::write` that never reaches the store + funnel. Only splice's own `rebuild_array_layout` catches it. + +Sabotage-verified three ways: dropping the revoke from `rebuild_array_layout` +turns 7 tests red; dropping the store funnel from `layout_note_slot` turns 39 +red; dropping it from `js_array_splice` alone turns **exactly one** red — the +equal-length splice test, confirming it is the unique guard for a case no other +mechanism covers. + +Test-only: one new `#[cfg(test)]` file plus its `mod` declaration. No runtime +or emitted-code change, so no standing cost and no behaviour to A/B. From 85d46e1f28a89dc50fddc81048a52046493af615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 8 Aug 2026 00:53:51 +0200 Subject: [PATCH 4/5] docs(runtime): rebuild_array_layout's comment wrongly claimed sort revokes through it Established by #7608's sabotage: removing the revoke here leaves the sort matrix test green -- sort's default path writes its rank permutation back through RootedArrayElems::set and revokes through the store funnel. --- crates/perry-runtime/src/array/header.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/perry-runtime/src/array/header.rs b/crates/perry-runtime/src/array/header.rs index 69acc2f815..95acf7a923 100644 --- a/crates/perry-runtime/src/array/header.rs +++ b/crates/perry-runtime/src/array/header.rs @@ -1701,10 +1701,13 @@ pub(crate) unsafe fn rebuild_array_layout(arr: *mut ArrayHeader) { if arr.is_null() { return; } - // #7480: this is the post-hoc funnel every bulk element mutator uses — - // `shift`, `unshift`, `splice`, `fill`, `copyWithin`, `reverse`, and the - // dense `sort` write-back all mutate slots with bare `ptr::write` / - // `ptr::copy` and then land here. They are permutations or arbitrary + // #7480: this is the post-hoc funnel most bulk element mutators use — + // `shift`, `unshift`, `splice`, `fill`, `copyWithin`, and `reverse` all + // mutate slots with bare `ptr::write` / `ptr::copy` and then land here. + // NOT `sort`: its default path writes the rank permutation back through + // `RootedArrayElems::set`, so it revokes through the STORE funnel + // (`layout_note_slot`) instead — established by sabotage in #7608's + // matrix (removing the revoke here leaves the sort test green). They are permutations or arbitrary // rewrites, so the element-shape proof is dropped conservatively; a // still-homogeneous array re-earns it on the next `ensure`. super::element_shape::clear_element_shape(arr); From 9ca67b58faa5c7ac1430bca5f85b397ab05493dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 8 Aug 2026 00:53:52 +0200 Subject: [PATCH 5/5] chore(version): bump to 0.5.1347 --- 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 70aa9200ca..b54fa5df77 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.1346 +**Current Version:** 0.5.1347 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 251b098497..26e716f81f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1346" +version = "0.5.1347" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1346" +version = "0.5.1347" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1346" +version = "0.5.1347" [[package]] name = "perry-ui-tvos" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1346" +version = "0.5.1347" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 68b3b272b4..c7de2a028a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1346" +version = "0.5.1347" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"