From 84da649f46bddd520f58f3607f742efdaadf5e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 7 Aug 2026 07:16:21 +0200 Subject: [PATCH 1/7] fix(runtime): a Map/Set subclass in a base-typed binding was read as a raw header (#7570) `const m: Map = new MyMap(); m.set("a", 1)` took SIGBUS (exit 138) before printing anything. Node prints `size: 1`. Codegen decides "this receiver is a Map" from the DECLARED TypeScript type (`is_map_expr` <= `Type::Generic { base: "Map" }`, plus the matching HIR folds and for-of fast paths), then emits a raw `js_map_*` call that dereferences the receiver as a `MapHeader`. A declared type is a hint, never a layout fact. Perry models a Map/Set subclass instance as a plain `ObjectHeader`, and the two headers overlay field-for-field, so `entries: *mut f64` reads `parent_class_id || field_count` -- two u32 class ids glued into a pointer -- and the first store through it faults. Resolve the receiver at the raw runtime entry points instead of tightening one codegen predicate at a time. `clean_map_ptr` / `clean_set_ptr` -- the funnels 27 and 30 `js_map_*` / `js_set_*` entries already share -- now brand-check what they are handed: a genuine `GC_TYPE_MAP`/`GC_TYPE_SET` header passes through (one `GcHeader.obj_type` load plus a compare), a subclass instance is redirected onto its hidden backing, and a plain object merely annotated `Map` resolves to null so each entry degrades through its existing null branch instead of dereferencing a forged pointer. Anything with no readable `GcHeader` is passed through exactly as before. Three sites needed more than the funnel: `js_set_add`/`has`/`delete`/`clear`/ `to_array` never called `clean_set_ptr` at all; the iterator-object constructors STORE the pointer rather than using it immediately; and `Map.prototype.set` / `Set.prototype.add` return their RECEIVER, which for a subclass is the instance and not the backing, so it is rooted across the store and handed back. Adds `test_gap_7570_map_set_declared_base_type.ts` (all five binding forms, the whole iteration surface, receiver identity, indirect subclasses, and non-subclass controls) plus four sabotage-shaped unit tests that assert the misread header byte is still present before asserting the resolved answer. Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- ...572-map-set-declared-base-type-receiver.md | 130 ++++++++++ .../src/collection_iter_object.rs | 11 + crates/perry-runtime/src/map.rs | 157 ++++++++---- .../src/object/map_set_subclass.rs | 234 ++++++++++++++++++ crates/perry-runtime/src/set.rs | 125 ++++++++-- ...est_gap_7570_map_set_declared_base_type.ts | 205 +++++++++++++++ 6 files changed, 793 insertions(+), 69 deletions(-) create mode 100644 changelog.d/7572-map-set-declared-base-type-receiver.md create mode 100644 test-files/test_gap_7570_map_set_declared_base_type.ts diff --git a/changelog.d/7572-map-set-declared-base-type-receiver.md b/changelog.d/7572-map-set-declared-base-type-receiver.md new file mode 100644 index 0000000000..25385c7246 --- /dev/null +++ b/changelog.d/7572-map-set-declared-base-type-receiver.md @@ -0,0 +1,130 @@ +### Bug fixes + +**A `class X extends Map | Set` instance held in a binding *annotated* with the +base type was dereferenced as a raw `MapHeader`/`SetHeader` — SIGBUS on the +first `.set()` (#7570).** + +```ts +class MyMap extends Map {} + +const m: Map = new MyMap(); +m.set("a", 1); // <-- SIGBUS, exit 138, before anything prints +console.log("size:", m.size); +``` + +Node prints `size: 1`. Annotating a binding with a base type is everyday +TypeScript, and a parameter (`function f(m: Map)`) is the more +likely way to hit it in real code — NestJS's `ModulesContainer extends Map` is +exactly this shape. + +#### Root cause + +Perry models a Map/Set subclass instance as a plain `ObjectHeader` carrying the +real collection under a hidden field (`object/map_set_subclass.rs`). The two +headers overlay field-for-field: + +| `MapHeader` field | offset | actually reads on an `ObjectHeader` | +|---|--:|---| +| `size: u32` | 0 | `object_type` (= 1) | +| `capacity: u32` | 4 | `class_id` | +| `entries: *mut f64` | 8 | `parent_class_id` ‖ `field_count` | + +so `entries` is two `u32` class ids glued into a pointer, and the first store +through it faults (`map_set_string_key_value + 708`, `str x21, [x20], #0x8`). + +The instance reaches those raw entry points because **"is a Map" was decided +from the declared TypeScript type**. `is_map_expr` / `is_set_expr` +(`perry-codegen/src/type_analysis/strings.rs:135`, `:13`) are satisfied by +`Type::Generic { base: "Map" }` with no subclass or runtime-brand check, and the +HIR fold that produces `Expr::MapSet` / `SetHas` / … keys on the same thing +(`perry-hir/src/lower/expr_call/local_array_methods.rs:915-1032`), as do the +for-of fast paths (`lower/stmt_loops.rs:1306`, `:1326`). + +A declared type is a **hint, never a layout fact** — CLAUDE.md's *Known +Limitations* says annotations are erased and nothing validates them at runtime. +The unannotated form (`const m = new MyMap()`) always worked precisely because +it types as the subclass and dispatches through `subclass_backing_of`; the +annotation is what routes the value onto the raw lowering. + +#### Fix + +Resolve the receiver at the **raw runtime entry points** rather than tightening +one codegen predicate at a time. `map::clean_map_ptr` and `set::clean_set_ptr` — +the funnels 27 and 30 `js_map_*` / `js_set_*` entries already share — now +brand-check what they are handed: + +* a genuine header (`GC_TYPE_MAP` / `GC_TYPE_SET`) passes straight through. This + is the only case that costs anything: one `GcHeader.obj_type` load, of the + 8 bytes immediately preceding the header, plus a compare; +* a `class X extends Map | Set` instance is redirected onto its hidden backing + (`redirect_collection_receiver`, `#[cold]`/`#[inline(never)]`); +* a plain object *merely annotated* `Map` resolves to null, so every entry + degrades through its existing null branch (`undefined` / `0` / `false`) + instead of dereferencing a forged pointer; +* anything with no readable `GcHeader` (handle-band ids, tag remnants, + non-pointer garbage) is passed through unchanged — exactly the pre-fix + behaviour. + +This is the fail-closed option: it covers every binding form and every future +caller. Chosen over "refuse the raw lowering when the static type is a +subclassable native base", which would have cost the fast path for every +`Map`-annotated binding in every program, and over a codegen-emitted guard, +which would have had to be repeated at each of the ~20 lowering sites. + +Three sites needed more than the funnel: + +* `js_set_add` / `js_set_has` / `js_set_delete` / `js_set_clear` / + `js_set_to_array` never called `clean_set_ptr` at all — they went straight to + `find_value_index` on the raw pointer. +* `collection_iter_object::{map,set}_iter_obj_raw` **store** the pointer into the + iterator object instead of using it immediately, so the redirect has to happen + before capture (`resolve_map_receiver` / `resolve_set_receiver`). +* `Map.prototype.set` and `Set.prototype.add` return their **receiver**. For a + subclass instance the receiver and the collection differ, so the write goes to + the backing while the instance comes back — otherwise `m.set(k, v) === m` was + false and chaining handed out the backing. The receiver is rooted across the + store (`RuntimeHandle::across_mut`), because it is a movable `ObjectHeader` and + the store allocates. + +#### Validation + +* `test-files/test_gap_7570_map_set_declared_base_type.ts` — byte-identical to + `node --experimental-strip-types`, exit 0. Covers all five binding forms + (`const`, parameter, class field, return type, `as` cast), the whole iteration + surface (`for-of`, spread, `Array.from`, `forEach`, `.entries()/.keys()/ + .values()`), `size`/`get`/`has`/`delete`/`clear`, receiver identity, indirect + subclasses, a subclass with its own constructor and fields, and non-subclass + controls including the specialized numeric- and string-keyed entry points. + With the runtime change reverted the same file exits 138 with no output. +* Four sabotage-shaped unit tests in `object/map_set_subclass.rs`: each first + asserts the header byte the pre-fix code misread is still there + (`object_type == 1` at `MapHeader.size`'s offset) and only then that the entry + point returns the resolved answer, so a green run proves the redirect fired + rather than that nothing threw. The genuine-Map test additionally asserts + `redirect_collection_receiver` returns 0 for a real `MapHeader`, so the + fast-path identity cannot have come from a redirect that happened to agree. +* Fast path unchanged: the change is runtime-only, and the emitted LLVM IR for a + probe exercising all the affected forms is **byte-identical** before and after + (`diff` on `--trace llvm` output, 8,910 lines, zero differences) — plain + `new Map()` still lowers to `js_map_set_string_number` / `js_map_get_string_key` + / `js_map_size` exactly as before. +* `cargo test -p perry-runtime`: 1816 passed, 0 failed. + +#### Not fixed here + +`class X extends Array` is the same hazard on a different family — an Array +subclass instance is also a plain `ObjectHeader` (`array/subclass.rs`), and +`is_array_expr` gates three tiers that do not brand-check: the bounded-index +element get/set, the inline `arr.length` load, and `lower_array_method`. The +general index-get tier already guards (`expr/index_get/guarded_array.rs` tests +`GC_TYPE_ARRAY` before the slot load). Filed separately. + +`m instanceof MyMap` is false for a Map/Set subclass instance with **or +without** the annotation — a pre-existing, unrelated gap in the class-registry +parent edge, not touched by this change. Filed separately. + +Every other native base in the sweep already re-validates at the runtime +boundary: Promise (`subclass_backing_promise` in `promise/checked_dispatch.rs`), +RegExp (`is_valid_regex_ptr`), Error (`object_type == OBJECT_TYPE_ERROR`), Date, +DataView / typed arrays / Buffer (`lookup_typed_array_kind`, +`is_registered_buffer`), and URLSearchParams (`resolve_search_params_receiver`). diff --git a/crates/perry-runtime/src/collection_iter_object.rs b/crates/perry-runtime/src/collection_iter_object.rs index 4331e25d8c..27d000378d 100644 --- a/crates/perry-runtime/src/collection_iter_object.rs +++ b/crates/perry-runtime/src/collection_iter_object.rs @@ -83,6 +83,15 @@ unsafe fn alloc_iterator(class_id: u32, coll_nanboxed: f64, kind: i32) -> f64 { /// Build a fresh Map iterator object for `map` (raw pointer) of the given /// kind. Returns the RAW iterator-object pointer as i64 (caller NaN-boxes). unsafe fn map_iter_obj_raw(map: *const MapHeader, kind: i32) -> i64 { + // #7570: these entries are reached from the DECLARED-type lowering of + // `m.entries()`/`.keys()`/`.values()`, so `map` can be a `class X extends + // Map` instance (a plain ObjectHeader) rather than a `MapHeader`. Every + // `next()` would then read `parent_class_id ‖ field_count` as the entries + // pointer. Resolve onto the hidden backing before the iterator captures it. + // Unlike the `js_map_*` entries this is not a `clean_map_ptr` caller — it + // stores the raw pointer into the iterator object, so the redirect has to + // happen here. + let map = crate::map::resolve_map_receiver(map); if map.is_null() { return 0; } @@ -91,6 +100,8 @@ unsafe fn map_iter_obj_raw(map: *const MapHeader, kind: i32) -> i64 { } unsafe fn set_iter_obj_raw(set: *const SetHeader, kind: i32) -> i64 { + // #7570 — see `map_iter_obj_raw`. + let set = crate::set::resolve_set_receiver(set); if set.is_null() { return 0; } diff --git a/crates/perry-runtime/src/map.rs b/crates/perry-runtime/src/map.rs index 27308ee082..a6a559763c 100644 --- a/crates/perry-runtime/src/map.rs +++ b/crates/perry-runtime/src/map.rs @@ -728,8 +728,12 @@ pub(crate) fn test_map_ptr_index_contains(map: *const MapHeader, key: f64) -> bo /// Strip NaN-boxing tags from a map pointer (defensive guard). /// If the pointer has NaN-boxing tags in the upper 16 bits, strip them. /// Returns null for undefined/null NaN-boxing tags. +/// +/// This is *identity* only — it answers "what value did the caller pass?", not +/// "which `MapHeader` does the operation run on". Use [`clean_map_ptr`] for the +/// latter; the two differ for a `class X extends Map` instance (#7570). #[inline(always)] -fn clean_map_ptr(map: *const MapHeader) -> *const MapHeader { +fn map_receiver_identity(map: *const MapHeader) -> *const MapHeader { let bits = map as u64; let top16 = bits >> 48; if top16 >= 0x7FF8 { @@ -742,11 +746,63 @@ fn clean_map_ptr(map: *const MapHeader) -> *const MapHeader { } } +/// Resolve a `Map` receiver to the `MapHeader` the operation must run on. +/// +/// Strips the NaN-box tag ([`map_receiver_identity`]) and then **brand-checks** +/// the result. Codegen picks the raw `js_map_*` lowering from the *declared* +/// TypeScript type of the receiver's binding, which is a hint and never a +/// layout fact, so what arrives here can be: +/// +/// * a genuine `MapHeader` — the overwhelmingly common case, and the only one +/// that costs anything: one `GcHeader.obj_type` load (the 8 bytes +/// immediately preceding the header) plus a compare; +/// * a `class X extends Map` INSTANCE, which perry models as a plain +/// `ObjectHeader` carrying the real collection under a hidden field — +/// redirected onto that backing (#7570); +/// * a plain object that was merely *annotated* `Map` — resolved to +/// null, so every entry point degrades through its existing null branch +/// instead of reading `parent_class_id ‖ field_count` as `entries`. +/// +/// Anything with no readable `GcHeader` (handle-band ids, tag remnants, +/// non-pointer garbage) is passed through unchanged: that is exactly the +/// pre-#7570 behaviour, and narrowing it is a separate, riskier change. +#[inline(always)] +fn clean_map_ptr(map: *const MapHeader) -> *const MapHeader { + let map = map_receiver_identity(map); + let addr = map as usize; + match unsafe { crate::value::addr_class::try_read_gc_header(addr) } { + // Genuine Map: `js_map_alloc` allocates the header with GC_TYPE_MAP. + Some(header) if header.obj_type == crate::gc::GC_TYPE_MAP => map, + // Only a plain object can be a Map subclass instance. + Some(header) if header.obj_type == crate::gc::GC_TYPE_OBJECT => { + crate::object::map_set_subclass::redirect_collection_receiver( + addr, + crate::object::map_set_subclass::CollectionKind::Map, + ) as *const MapHeader + } + _ => map, + } +} + #[inline(always)] fn clean_map_ptr_mut(map: *mut MapHeader) -> *mut MapHeader { clean_map_ptr(map as *const MapHeader) as *mut MapHeader } +#[inline(always)] +fn map_receiver_identity_mut(map: *mut MapHeader) -> *mut MapHeader { + map_receiver_identity(map as *const MapHeader) as *mut MapHeader +} + +/// [`clean_map_ptr`] for entry points OUTSIDE this module that take a raw +/// receiver and must not deref it as a `MapHeader` on faith — currently the +/// iterator-object constructors in `collection_iter_object`, which STORE the +/// pointer instead of using it immediately (#7570). +#[inline(always)] +pub(crate) fn resolve_map_receiver(map: *const MapHeader) -> *const MapHeader { + clean_map_ptr(map) +} + /// Map header - GC-movable address, entries allocated separately #[repr(C)] pub struct MapHeader { @@ -1324,14 +1380,45 @@ unsafe fn map_set_string_key_value( map } +/// Run `op` on the RESOLVED collection and return the RECEIVER. +/// +/// `Map.prototype.set` returns its receiver. For a `class X extends Map` +/// instance the two differ (#7570): the entry is written to the hidden backing +/// `MapHeader`, but `m.set(k, v)` must still evaluate to `m` — otherwise +/// chaining hands back the backing and `m.set(…) === m` is false. +/// +/// The common case (receiver IS the collection) costs one pointer compare and +/// takes no handle scope. The subclass case roots the receiver, because it is a +/// movable `ObjectHeader` and `op` allocates. +#[inline] +fn map_op_returning_receiver( + map: *mut MapHeader, + op: impl FnOnce(*mut MapHeader), +) -> *mut MapHeader { + let receiver = map_receiver_identity_mut(map); + let resolved = clean_map_ptr_mut(map); + if resolved.is_null() { + return receiver; + } + if std::ptr::eq(resolved, receiver) { + op(resolved); + return receiver; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let handle = scope.root_raw_mut_ptr(receiver); + let ((), receiver) = handle.across_mut::(|| op(resolved)); + receiver +} + /// Set a key-value pair in the map /// The map pointer is stable (never reallocated) #[no_mangle] pub extern "C" fn js_map_set(map: *mut MapHeader, key: f64, value: f64) -> *mut MapHeader { - let map = clean_map_ptr_mut(map); - if map.is_null() { - return map; - } + map_op_returning_receiver(map, |map| map_set_resolved(map, key, value)) +} + +/// `js_map_set`'s body, on a receiver already resolved to a genuine `MapHeader`. +fn map_set_resolved(map: *mut MapHeader, key: f64, value: f64) { let key = normalize_zero(key); unsafe { // Check if key already exists (O(1) via MAP_INDEX) @@ -1347,7 +1434,7 @@ pub extern "C" fn js_map_set(map: *mut MapHeader, key: f64, value: f64) -> *mut value_slot as usize, value.to_bits(), ); - return map; + return; } // Key doesn't exist, append a new entry. `ensure_capacity` can fire a @@ -1422,11 +1509,23 @@ pub extern "C" fn js_map_set(map: *mut MapHeader, key: f64, value: f64) -> *mut slot.insert(MapPtrKey(key), size); }); } - - map } } +/// Shared tail for the `js_map_set_string_*` specializations: store into the +/// RESOLVED collection, return the RECEIVER (#7570 — see +/// [`map_op_returning_receiver`]). +#[inline] +fn map_set_string_returning_receiver( + map: *mut MapHeader, + key: *const StringHeader, + value: f64, +) -> *mut MapHeader { + map_op_returning_receiver(map, |map| unsafe { + map_set_string_key_value(map, key, value); + }) +} + #[no_mangle] pub extern "C" fn js_map_set_number_key( map: *mut MapHeader, @@ -1445,11 +1544,7 @@ pub extern "C" fn js_map_set_string_number( key: *const StringHeader, value: f64, ) -> *mut MapHeader { - let map = clean_map_ptr_mut(map); - if map.is_null() { - return map; - } - unsafe { map_set_string_key_value(map, key, value) } + map_set_string_returning_receiver(map, key, value) } #[no_mangle] @@ -1458,11 +1553,7 @@ pub extern "C" fn js_map_set_string_key( key: *const StringHeader, value: f64, ) -> *mut MapHeader { - let map = clean_map_ptr_mut(map); - if map.is_null() { - return map; - } - unsafe { map_set_string_key_value(map, key, value) } + map_set_string_returning_receiver(map, key, value) } #[no_mangle] @@ -1471,12 +1562,8 @@ pub extern "C" fn js_map_set_string_i32( key: *const StringHeader, value: i32, ) -> *mut MapHeader { - let map = clean_map_ptr_mut(map); - if map.is_null() { - return map; - } let value_bits = crate::value::INT32_TAG | ((value as u32) as u64); - unsafe { map_set_string_key_value(map, key, f64::from_bits(value_bits)) } + map_set_string_returning_receiver(map, key, f64::from_bits(value_bits)) } #[no_mangle] @@ -1485,11 +1572,7 @@ pub extern "C" fn js_map_set_string_u32( key: *const StringHeader, value: u32, ) -> *mut MapHeader { - let map = clean_map_ptr_mut(map); - if map.is_null() { - return map; - } - unsafe { map_set_string_key_value(map, key, f64::from(value)) } + map_set_string_returning_receiver(map, key, f64::from(value)) } #[no_mangle] @@ -1498,11 +1581,7 @@ pub extern "C" fn js_map_set_string_f32( key: *const StringHeader, value: f32, ) -> *mut MapHeader { - let map = clean_map_ptr_mut(map); - if map.is_null() { - return map; - } - unsafe { map_set_string_key_value(map, key, f64::from(value)) } + map_set_string_returning_receiver(map, key, f64::from(value)) } #[no_mangle] @@ -1511,16 +1590,12 @@ pub extern "C" fn js_map_set_string_bool( key: *const StringHeader, value: i32, ) -> *mut MapHeader { - let map = clean_map_ptr_mut(map); - if map.is_null() { - return map; - } let value_bits = if value != 0 { crate::value::TAG_TRUE } else { crate::value::TAG_FALSE }; - unsafe { map_set_string_key_value(map, key, f64::from_bits(value_bits)) } + map_set_string_returning_receiver(map, key, f64::from_bits(value_bits)) } #[no_mangle] @@ -1529,11 +1604,7 @@ pub extern "C" fn js_map_set_string_string( key: *const StringHeader, value: *const StringHeader, ) -> *mut MapHeader { - let map = clean_map_ptr_mut(map); - if map.is_null() { - return map; - } - unsafe { map_set_string_key_value(map, key, boxed_heap_string_key(value)) } + map_set_string_returning_receiver(map, key, boxed_heap_string_key(value)) } /// Get a value from the map by key diff --git a/crates/perry-runtime/src/object/map_set_subclass.rs b/crates/perry-runtime/src/object/map_set_subclass.rs index cd5deec60d..4d78d01edf 100644 --- a/crates/perry-runtime/src/object/map_set_subclass.rs +++ b/crates/perry-runtime/src/object/map_set_subclass.rs @@ -99,6 +99,54 @@ pub(crate) fn subclass_backing_of(value: f64) -> Option { } } +/// #7570 — resolve a raw Map/Set RECEIVER address that is NOT a genuine +/// `MapHeader`/`SetHeader` to the collection the operation must actually run +/// on. `want` selects which backing kind the caller can use. +/// +/// Why this exists: codegen decides "this receiver is a Map" from the +/// **declared** TypeScript type of the binding (`is_map_expr` / +/// `Type::Generic { base: "Map" }`), then emits a raw `js_map_*` call whose +/// first act is to dereference the receiver as a `MapHeader`. A declared type +/// is a hint, never a layout fact (CLAUDE.md, *Known Limitations*: annotations +/// are erased, nothing validates them at runtime), so any binding annotated +/// with the BASE type — `const m: Map = new MyMap()`, a parameter, a +/// class field, a return type, an `as Map<…>` cast — can be holding a +/// SUBCLASS instance, which perry models as a plain `ObjectHeader`. The two +/// headers overlay field-for-field, so `entries: *mut f64` reads +/// `parent_class_id ‖ field_count` — two `u32` class ids glued into a pointer +/// — and the first `.set()` stores through it (SIGBUS). +/// +/// The unannotated path never had this problem because it dispatches through +/// [`subclass_backing_of`]. This is the same redirect, performed at the raw +/// runtime entry points so it is **fail-closed**: it covers every binding form +/// and every future caller, rather than one predicate at a time. +/// +/// Returns `0` for an object that is not a Map/Set subclass instance (a plain +/// object mis-annotated as a native collection), so the caller degrades to its +/// existing null handling — `undefined` / `0` / `false` — instead of +/// dereferencing a forged pointer. +/// +/// Marked `#[cold]`/`#[inline(never)]`: the genuine-header fast path never +/// reaches here, and keeping the body out of line preserves the inlined +/// receiver check at the ~57 `js_map_*` / `js_set_*` entry points. +#[cold] +#[inline(never)] +pub(crate) fn redirect_collection_receiver(addr: usize, want: CollectionKind) -> usize { + let boxed = f64::from_bits(JSValue::pointer(addr as *const u8).bits()); + match (subclass_backing_of(boxed), want) { + (Some(CollectionBacking::Map(m)), CollectionKind::Map) => m as usize, + (Some(CollectionBacking::Set(s)), CollectionKind::Set) => s as usize, + _ => 0, + } +} + +/// Which backing kind a [`redirect_collection_receiver`] caller can use. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum CollectionKind { + Map, + Set, +} + /// True when a Map/Set subclass INSTANCE carries a USER `[Symbol.iterator]` /// override anywhere on its class/prototype chain — an own /// `inst[Symbol.iterator] = …`, a symbol accessor, or a class method @@ -283,3 +331,189 @@ pub extern "C" fn js_map_set_subclass_init(this: f64, kind: i32, iterable: f64) js_object_set_field_by_name(obj, key, f64::from_bits(backing_bits)); this } + +/// #7570 — the receiver-resolution contract for the raw `js_map_*` / `js_set_*` +/// entry points. +/// +/// These are *sabotage* tests, not smoke tests: each one first asserts that the +/// header byte the pre-fix code would have misread is still sitting there +/// (`object_type == 1` at `MapHeader.size`'s offset), and only then that the +/// entry point returns the resolved answer instead. A green run therefore +/// proves the redirect fired, not merely that nothing crashed. +#[cfg(test)] +mod tests { + use super::*; + use crate::error::OBJECT_TYPE_REGULAR; + use crate::object::js_object_alloc; + + fn boxed(obj: *mut ObjectHeader) -> f64 { + f64::from_bits(JSValue::pointer(obj as *const u8).bits()) + } + + fn undefined() -> f64 { + f64::from_bits(crate::value::TAG_UNDEFINED) + } + + /// A `class X extends Map {}` instance, built the way `super()` builds it. + fn map_subclass_instance() -> *mut ObjectHeader { + let obj = js_object_alloc(9001, 2); + js_map_set_subclass_init(boxed(obj), 0, undefined()); + obj + } + + /// A `class X extends Set {}` instance. + fn set_subclass_instance() -> *mut ObjectHeader { + let obj = js_object_alloc(9002, 2); + js_map_set_subclass_init(boxed(obj), 1, undefined()); + obj + } + + #[test] + fn a_genuine_map_takes_the_fast_path_and_is_never_redirected() { + let map = crate::map::js_map_alloc(4); + assert_eq!( + crate::map::resolve_map_receiver(map) as usize, + map as usize, + "a real MapHeader must resolve to itself" + ); + // Assert the subject was live: the redirect is what would have to have + // produced this answer if the GC_TYPE_MAP fast path had NOT fired, and + // it cannot — it yields 0 for a non-object. So the identity above came + // from the fast path, not from a redirect that happened to agree. + assert_eq!( + redirect_collection_receiver(map as usize, CollectionKind::Map), + 0, + "the redirect must not claim a genuine Map" + ); + + let set = crate::set::js_set_alloc(4); + assert_eq!( + crate::set::resolve_set_receiver(set) as usize, + set as usize, + "a real SetHeader must resolve to itself" + ); + assert_eq!( + redirect_collection_receiver(set as usize, CollectionKind::Set), + 0, + "the redirect must not claim a genuine Set" + ); + } + + #[test] + fn a_map_subclass_instance_is_redirected_onto_its_backing() { + let obj = map_subclass_instance(); + let backing = match subclass_backing_of(boxed(obj)) { + Some(CollectionBacking::Map(m)) => m, + _ => panic!("super() should have installed a Map backing"), + }; + assert_ne!(backing as usize, obj as usize); + + // The pre-fix hazard, still present in the bytes: `MapHeader.size` + // overlays `ObjectHeader.object_type`, so `js_map_size` used to report + // 1 for an EMPTY subclass instance and `MapHeader.entries` was + // `parent_class_id ‖ field_count`. + assert_eq!(unsafe { (*obj).object_type }, OBJECT_TYPE_REGULAR); + assert_eq!( + js_map_size_of(obj), + 0, + "an empty Map subclass instance must report size 0, not object_type" + ); + + // Writes land in the backing; the receiver is what comes back. + let returned = crate::map::js_map_set(obj as *mut crate::map::MapHeader, 1.0, 2.0); + assert_eq!( + returned as usize, obj as usize, + "Map.prototype.set returns the RECEIVER, not the hidden backing" + ); + assert_eq!(crate::map::js_map_size(backing), 1); + assert_eq!(js_map_size_of(obj), 1); + assert_eq!( + crate::map::js_map_get(obj as *const crate::map::MapHeader, 1.0), + 2.0 + ); + // The instance header is untouched — no forged-pointer store landed in + // it, and it is still an ordinary object. + assert_eq!(unsafe { (*obj).object_type }, OBJECT_TYPE_REGULAR); + assert_eq!(unsafe { (*obj).class_id }, 9001); + } + + #[test] + fn a_set_subclass_instance_is_redirected_onto_its_backing() { + let obj = set_subclass_instance(); + let backing = match subclass_backing_of(boxed(obj)) { + Some(CollectionBacking::Set(s)) => s, + _ => panic!("super() should have installed a Set backing"), + }; + assert_ne!(backing as usize, obj as usize); + assert_eq!(unsafe { (*obj).object_type }, OBJECT_TYPE_REGULAR); + assert_eq!( + crate::set::js_set_size(obj as *const crate::set::SetHeader), + 0, + "an empty Set subclass instance must report size 0, not object_type" + ); + + let returned = crate::set::js_set_add(obj as *mut crate::set::SetHeader, 7.0); + assert_eq!( + returned as usize, obj as usize, + "Set.prototype.add returns the RECEIVER, not the hidden backing" + ); + assert_eq!(crate::set::js_set_size(backing), 1); + assert_eq!( + crate::set::js_set_has(obj as *const crate::set::SetHeader, 7.0), + 1 + ); + assert_eq!( + crate::set::js_set_has(obj as *const crate::set::SetHeader, 8.0), + 0 + ); + assert_eq!(unsafe { (*obj).class_id }, 9002); + } + + /// A plain object merely ANNOTATED `Map` / `Set` — the second way + /// a declared type lies about layout. There is nothing to redirect to, so + /// the entry points must degrade through their null branch rather than + /// treat `parent_class_id ‖ field_count` as an `entries` pointer. + #[test] + fn a_plain_object_annotated_as_a_collection_forges_no_pointer() { + let obj = js_object_alloc(9003, 3); + assert!(subclass_backing_of(boxed(obj)).is_none()); + assert_eq!( + redirect_collection_receiver(obj as usize, CollectionKind::Map), + 0 + ); + assert_eq!( + redirect_collection_receiver(obj as usize, CollectionKind::Set), + 0 + ); + + // Pre-fix these read the ObjectHeader as a MapHeader: `size` was + // `object_type` (= 1) and the very next `.set()` stored through + // `parent_class_id ‖ field_count`. + assert_eq!(unsafe { (*obj).object_type }, OBJECT_TYPE_REGULAR); + assert_eq!(js_map_size_of(obj), 0); + assert_eq!( + crate::map::js_map_get(obj as *const crate::map::MapHeader, 1.0).to_bits(), + crate::value::TAG_UNDEFINED + ); + let returned = crate::map::js_map_set(obj as *mut crate::map::MapHeader, 1.0, 2.0); + assert_eq!(returned as usize, obj as usize); + assert_eq!( + crate::set::js_set_size(obj as *const crate::set::SetHeader), + 0 + ); + assert_eq!( + crate::set::js_set_has(obj as *const crate::set::SetHeader, 1.0), + 0 + ); + crate::set::js_set_clear(obj as *mut crate::set::SetHeader); + + // Nothing wrote into the object's header. + assert_eq!(unsafe { (*obj).object_type }, OBJECT_TYPE_REGULAR); + assert_eq!(unsafe { (*obj).class_id }, 9003); + assert_eq!(unsafe { (*obj).field_count }, 3); + } + + fn js_map_size_of(obj: *mut ObjectHeader) -> u32 { + crate::map::js_map_size(obj as *const crate::map::MapHeader) + } +} diff --git a/crates/perry-runtime/src/set.rs b/crates/perry-runtime/src/set.rs index 5e0d74de49..1110c07495 100644 --- a/crates/perry-runtime/src/set.rs +++ b/crates/perry-runtime/src/set.rs @@ -845,9 +845,13 @@ pub extern "C" fn js_set_alloc(capacity: u32) -> *mut SetHeader { } } -/// Clean a set pointer that might have NaN-box tag bits +/// Clean a set pointer that might have NaN-box tag bits. +/// +/// *Identity* only — "what value did the caller pass?", not "which `SetHeader` +/// does the operation run on". Use [`clean_set_ptr`] for the latter; the two +/// differ for a `class X extends Set` instance (#7570). #[inline(always)] -fn clean_set_ptr(set: *const SetHeader) -> *const SetHeader { +fn set_receiver_identity(set: *const SetHeader) -> *const SetHeader { let bits = set as u64; let top16 = bits >> 48; if top16 >= 0x7FF8 { @@ -860,6 +864,64 @@ fn clean_set_ptr(set: *const SetHeader) -> *const SetHeader { } } +/// Resolve a `Set` receiver to the `SetHeader` the operation must run on. +/// +/// The `Set` twin of `map::clean_map_ptr` — see its doc comment for why a +/// *declared* TypeScript type cannot be trusted as a layout fact, and what each +/// arm means. Briefly: genuine `SetHeader` (`GC_TYPE_SET`) passes through, a +/// `class X extends Set` instance is redirected onto its hidden backing, a +/// plain object merely annotated `Set` resolves to null, and anything with +/// no readable `GcHeader` is left exactly as it was pre-#7570. +#[inline(always)] +fn clean_set_ptr(set: *const SetHeader) -> *const SetHeader { + let set = set_receiver_identity(set); + let addr = set as usize; + match unsafe { crate::value::addr_class::try_read_gc_header(addr) } { + Some(header) if header.obj_type == crate::gc::GC_TYPE_SET => set, + Some(header) if header.obj_type == crate::gc::GC_TYPE_OBJECT => { + crate::object::map_set_subclass::redirect_collection_receiver( + addr, + crate::object::map_set_subclass::CollectionKind::Set, + ) as *const SetHeader + } + _ => set, + } +} + +/// [`clean_set_ptr`] for entry points OUTSIDE this module — see +/// `map::resolve_map_receiver` (#7570). +#[inline(always)] +pub(crate) fn resolve_set_receiver(set: *const SetHeader) -> *const SetHeader { + clean_set_ptr(set) +} + +/// Run `op` on the RESOLVED collection and return the RECEIVER. +/// +/// `Set.prototype.add` returns its receiver; for a `class X extends Set` +/// instance that is the INSTANCE, not the hidden backing (#7570). The +/// `map::map_op_returning_receiver` twin — same zero-extra-cost common case +/// (one pointer compare), same rooting of the movable instance in the subclass +/// case. +#[inline] +fn set_op_returning_receiver( + set: *mut SetHeader, + op: impl FnOnce(*mut SetHeader), +) -> *mut SetHeader { + let receiver = set_receiver_identity(set as *const SetHeader) as *mut SetHeader; + let resolved = clean_set_ptr(set as *const SetHeader) as *mut SetHeader; + if resolved.is_null() { + return receiver; + } + if std::ptr::eq(resolved, receiver) { + op(resolved); + return receiver; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let handle = scope.root_raw_mut_ptr(receiver); + let ((), receiver) = handle.across_mut::(|| op(resolved)); + receiver +} + /// Get the number of elements in the set #[no_mangle] pub extern "C" fn js_set_size(set: *const SetHeader) -> u32 { @@ -874,6 +936,11 @@ pub extern "C" fn js_set_size(set: *const SetHeader) -> u32 { /// Returns the set pointer (always the same, stable address) #[no_mangle] pub extern "C" fn js_set_add(set: *mut SetHeader, value: f64) -> *mut SetHeader { + set_op_returning_receiver(set, |set| set_add_resolved(set, value)) +} + +/// `js_set_add`'s body, on a receiver already resolved to a genuine `SetHeader`. +fn set_add_resolved(set: *mut SetHeader, value: f64) { let value = normalize_zero(value); unsafe { // Check if value already exists @@ -881,7 +948,7 @@ pub extern "C" fn js_set_add(set: *mut SetHeader, value: f64) -> *mut SetHeader if idx >= 0 { // Value already exists, nothing to do - return set; + return; } // Value doesn't exist, need to add it @@ -913,7 +980,6 @@ pub extern "C" fn js_set_add(set: *mut SetHeader, value: f64) -> *mut SetHeader }); (*set).size = size + 1; - set } } @@ -930,16 +996,17 @@ pub extern "C" fn js_set_add_string( set: *mut SetHeader, value: *const StringHeader, ) -> *mut SetHeader { - let set = clean_set_ptr(set as *const SetHeader) as *mut SetHeader; - if set.is_null() { - return set; - } + set_op_returning_receiver(set, |set| set_add_string_resolved(set, value)) +} + +/// `js_set_add_string`'s body, on a resolved `SetHeader`. +fn set_add_string_resolved(set: *mut SetHeader, value: *const StringHeader) { let value = boxed_heap_string_value(value); unsafe { let idx = find_value_index(set, value); if idx >= 0 { - return set; + return; } let grew = ensure_capacity(set); @@ -968,7 +1035,6 @@ pub extern "C" fn js_set_add_string( }); (*set).size = size + 1; - set } } @@ -977,39 +1043,28 @@ fn boxed_i32_value(value: i32) -> f64 { f64::from_bits(crate::value::JSValue::int32(value).bits()) } +// The `_i32`/`_u32`/`_f32`/`_bool` specializations delegate to `js_set_add`, +// which now performs the receiver resolution AND returns the receiver identity +// (#7570); pre-resolving here would hand `js_set_add` the backing and lose the +// instance a `class X extends Set` receiver must return. + #[no_mangle] pub extern "C" fn js_set_add_i32(set: *mut SetHeader, value: i32) -> *mut SetHeader { - let set = clean_set_ptr(set as *const SetHeader) as *mut SetHeader; - if set.is_null() { - return set; - } js_set_add(set, boxed_i32_value(value)) } #[no_mangle] pub extern "C" fn js_set_add_u32(set: *mut SetHeader, value: u32) -> *mut SetHeader { - let set = clean_set_ptr(set as *const SetHeader) as *mut SetHeader; - if set.is_null() { - return set; - } js_set_add(set, f64::from(value)) } #[no_mangle] pub extern "C" fn js_set_add_f32(set: *mut SetHeader, value: f32) -> *mut SetHeader { - let set = clean_set_ptr(set as *const SetHeader) as *mut SetHeader; - if set.is_null() { - return set; - } js_set_add(set, f64::from(value)) } #[no_mangle] pub extern "C" fn js_set_add_bool(set: *mut SetHeader, value: i32) -> *mut SetHeader { - let set = clean_set_ptr(set as *const SetHeader) as *mut SetHeader; - if set.is_null() { - return set; - } let boxed = if value != 0 { f64::from_bits(crate::value::TAG_TRUE) } else { @@ -1022,6 +1077,12 @@ pub extern "C" fn js_set_add_bool(set: *mut SetHeader, value: i32) -> *mut SetHe /// Returns 1 if found, 0 if not found #[no_mangle] pub extern "C" fn js_set_has(set: *const SetHeader, value: f64) -> i32 { + // #7570: a `Set`-annotated binding can be holding a `class X extends + // Set` instance (a plain ObjectHeader), or a plain object. + let set = clean_set_ptr(set); + if set.is_null() { + return 0; + } let value = normalize_zero(value); unsafe { if find_value_index(set, value) >= 0 { @@ -1101,6 +1162,11 @@ pub extern "C" fn js_set_has_bool(set: *const SetHeader, value: i32) -> i32 { /// Returns 1 if deleted, 0 if value not found #[no_mangle] pub extern "C" fn js_set_delete(set: *mut SetHeader, value: f64) -> i32 { + // #7570: resolve a `class X extends Set` receiver onto its backing. + let set = clean_set_ptr(set as *const SetHeader) as *mut SetHeader; + if set.is_null() { + return 0; + } let value = normalize_zero(value); unsafe { let idx = find_value_index(set, value); @@ -1262,6 +1328,11 @@ static KEEP_JS_SET_DELETE_BOOL: extern "C" fn(*mut SetHeader, i32) -> i32 = js_s /// Clear all elements from the set #[no_mangle] pub extern "C" fn js_set_clear(set: *mut SetHeader) { + // #7570: resolve a `class X extends Set` receiver onto its backing. + let set = clean_set_ptr(set as *const SetHeader) as *mut SetHeader; + if set.is_null() { + return; + } unsafe { (*set).size = 0; } @@ -1306,6 +1377,8 @@ pub extern "C" fn js_set_value_at(set: *const SetHeader, i: u32) -> f64 { /// no concurrent modification, capacity is exact. #[no_mangle] pub extern "C" fn js_set_to_array(set: *const SetHeader) -> *mut crate::array::ArrayHeader { + // #7570: resolve a `class X extends Set` receiver onto its backing. + let set = clean_set_ptr(set); if set.is_null() { return crate::array::js_array_alloc(0); } diff --git a/test-files/test_gap_7570_map_set_declared_base_type.ts b/test-files/test_gap_7570_map_set_declared_base_type.ts new file mode 100644 index 0000000000..d19476a7b8 --- /dev/null +++ b/test-files/test_gap_7570_map_set_declared_base_type.ts @@ -0,0 +1,205 @@ +// #7570: a `class X extends Map | Set` instance held in a binding ANNOTATED +// with the BASE type was handed to the raw `js_map_*` / `js_set_*` entry points +// as if it were a real `MapHeader`/`SetHeader`. +// +// Perry models a Map/Set subclass instance as a plain `ObjectHeader`, and the +// two headers overlay field-for-field: +// +// MapHeader.size (+0) ← ObjectHeader.object_type +// MapHeader.capacity (+4) ← ObjectHeader.class_id +// MapHeader.entries (+8) ← parent_class_id ‖ field_count ← a FORGED POINTER +// +// so the first `.set()` stored through two glued-together `u32` class ids and +// the process took SIGBUS before printing anything. +// +// The cause is that "is a Map" was decided from the DECLARED TypeScript type +// (`is_map_expr` ⇐ `Type::Generic { base: "Map" }`), and a declared type is a +// hint, never a layout fact — nothing validates annotations at runtime. Every +// binding form that can carry the base type is therefore a way in; each one +// below crashed before the fix. The UNANNOTATED shape (covered by +// test_gap_6325_map_set_subclass.ts) always worked, because it dispatches +// through the hidden collection backing. +// +// Fixed by resolving the receiver at the raw runtime entry points: a genuine +// header passes straight through, a subclass instance is redirected onto its +// backing, and a plain object merely annotated `Map` degrades to the +// existing null handling instead of dereferencing a forged pointer. + +class MyMap extends Map {} +class MySet extends Set {} + +// ── 1. `const` annotated with the base type — the issue's exact repro ── +const m1: Map = new MyMap(); +m1.set("a", 1); +console.log("1 const-ann:", m1.get("a"), m1.size, m1.has("a"), m1.has("zz")); +console.log("1 delete:", m1.delete("zz"), m1.delete("a"), m1.size); + +const s1: Set = new MySet(); +s1.add(4); +s1.add(4); +console.log("1 set const-ann:", s1.size, s1.has(4), s1.has(5), s1.delete(4), s1.size); + +// ── 2. PARAMETER annotated with the base type ── +function takeMap(mm: Map): string { + let total = 0; + for (const [, v] of mm) total += v; + return `${total} ${mm.size} ${mm.get("x")}`; +} +console.log( + "2 param-ann:", + takeMap( + new MyMap([ + ["x", 5], + ["y", 6], + ]), + ), +); + +function takeSet(ss: Set): string { + let total = 0; + for (const v of ss) total += v; + return `${total} ${ss.size} ${ss.has(2)}`; +} +console.log("2 set param-ann:", takeSet(new MySet([1, 2, 3]))); + +// ── 3. CLASS FIELD annotated with the base type ── +class Holder { + m: Map = new MyMap(); + s: Set = new MySet(); + run(): string { + this.m.set("k", 7); + this.s.add(9); + return `${this.m.get("k")} ${this.m.size} ${this.s.has(9)} ${this.s.size}`; + } +} +console.log("3 field-ann:", new Holder().run()); + +// ── 4. RETURN TYPE annotated with the base type ── +function makeMap(): Map { + return new MyMap([["r", 3]]); +} +const m4 = makeMap(); +m4.set("q", 4); +console.log("4 ret-ann:", m4.get("r"), m4.get("q"), m4.size); + +function makeSet(): Set { + return new MySet([7]); +} +const s4 = makeSet(); +s4.add(8); +console.log("4 set ret-ann:", s4.size, s4.has(7), s4.has(8)); + +// ── 5. `as` CAST to the base type ── +const m5 = new MyMap() as Map; +m5.set("c", 8); +console.log("5 as-cast:", m5.get("c"), m5.size); + +const s5 = new MySet() as Set; +s5.add(11); +console.log("5 set as-cast:", s5.size, s5.has(11)); + +// ── 6. iteration surface through an annotated binding ── +const m6: Map = new MyMap([ + ["a", 1], + ["b", 2], +]); +console.log("6 values:", [...m6.values()].join(",")); +console.log("6 keys:", [...m6.keys()].join(",")); +console.log("6 entries:", JSON.stringify([...m6.entries()])); +console.log("6 spread:", JSON.stringify([...m6])); +console.log("6 Array.from:", JSON.stringify(Array.from(m6.keys()))); +let acc6 = 0; +m6.forEach((v, k) => { + acc6 += v + k.length; +}); +console.log("6 forEach:", acc6); +const seen6: string[] = []; +for (const [k, v] of m6) seen6.push(`${k}=${v}`); +console.log("6 for-of:", seen6.join("|")); + +const s6: Set = new MySet([1, 2, 3]); +console.log("6 set values:", [...s6.values()].join(",")); +console.log("6 set keys:", [...s6.keys()].join(",")); +console.log("6 set spread:", [...s6].join(",")); +let acc6s = 0; +s6.forEach((v) => { + acc6s += v; +}); +console.log("6 set forEach:", acc6s); +const seen6s: number[] = []; +for (const v of s6) seen6s.push(v); +console.log("6 set for-of:", seen6s.join(",")); + +// ── 7. RECEIVER IDENTITY: `.set()`/`.add()` return the INSTANCE, not the +// hidden backing collection. Chaining and `===` must both hold. ── +const m7: Map = new MyMap(); +console.log("7 set returns receiver:", m7.set("a", 1) === m7); +m7.set("b", 2).set("c", 3); +console.log("7 chained:", m7.size, m7.get("b"), m7.get("c")); +// NOTE: only the BASE `instanceof` is asserted here. `m7 instanceof MyMap` is +// a separate, pre-existing gap — it is false for a Map/Set subclass instance +// with or without the annotation, so it is not part of this fix. +console.log("7 still a Map:", m7 instanceof Map); + +const s7: Set = new MySet(); +console.log("7 add returns receiver:", s7.add(1) === s7); +s7.add(2).add(3); +console.log("7 set chained:", s7.size, s7.has(2), s7.has(3)); +console.log("7 still a Set:", s7 instanceof Set); + +// ── 8. `clear()` through an annotated binding ── +const m8: Map = new MyMap([["z", 1]]); +m8.clear(); +console.log("8 clear:", m8.size, m8.get("z")); + +const s8: Set = new MySet([1, 2]); +s8.clear(); +console.log("8 set clear:", s8.size, s8.has(1)); + +// ── 9. INDIRECT subclass and a subclass with its own ctor + fields, both +// reached through a base-typed binding ── +class MidMap extends Map {} +class LeafMap extends MidMap {} +const m9: Map = new LeafMap(); +m9.set("deep", 42); +console.log("9 indirect:", m9.get("deep"), m9.size); + +class TaggedMap extends Map { + tag: string; + constructor(tag: string) { + super([["seed", 1]]); + this.tag = tag; + } +} +const tagged = new TaggedMap("t"); +const m9b: Map = tagged; +m9b.set("more", 2); +console.log("9 own ctor:", m9b.get("seed"), m9b.get("more"), m9b.size, tagged.tag); + +// ── 10. NON-SUBCLASS CONTROLS. These take the raw fast path and must be +// byte-identical to before the fix. ── +const plain: Map = new Map([["p", 1]]); +plain.set("p2", 2); +console.log("10 plain:", plain.get("p"), plain.size, [...plain.keys()].join(",")); +console.log("10 plain identity:", plain.set("p3", 3) === plain, plain.size); +const plainSet: Set = new Set([1, 2]); +plainSet.add(3); +console.log("10 plainSet:", plainSet.size, plainSet.has(3), [...plainSet].join(",")); +console.log("10 plainSet identity:", plainSet.add(4) === plainSet, plainSet.size); + +// A number-keyed Map and a string→string Map exercise the SPECIALIZED +// `js_map_set_*` entry points (the crash frame in the issue was +// `map_set_string_key_value`). +const nums: Map = new Map(); +for (let i = 0; i < 4; i++) nums.set(i, i * i); +console.log("10 numeric:", nums.get(3), nums.size); +const strs: Map = new Map(); +strs.set("k", "v"); +console.log("10 string-string:", strs.get("k"), strs.size); + +const numsSub: Map = new MyMap(); +for (let i = 0; i < 4; i++) numsSub.set(i, i * i); +console.log("10 numeric subclass:", numsSub.get(3), numsSub.size); +const strsSub: Map = new MyMap(); +strsSub.set("k", "v"); +console.log("10 string-string subclass:", strsSub.get("k"), strsSub.size); From 4a55c66e842dd4f0c763fc8ebceaa9e4f5c6f356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 7 Aug 2026 07:26:13 +0200 Subject: [PATCH 2/7] chore(changelog): key the fragment to PR #7573 Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- ...pe-receiver.md => 7573-map-set-declared-base-type-receiver.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{7572-map-set-declared-base-type-receiver.md => 7573-map-set-declared-base-type-receiver.md} (100%) diff --git a/changelog.d/7572-map-set-declared-base-type-receiver.md b/changelog.d/7573-map-set-declared-base-type-receiver.md similarity index 100% rename from changelog.d/7572-map-set-declared-base-type-receiver.md rename to changelog.d/7573-map-set-declared-base-type-receiver.md From 629dac274d27e9b6254d0ffa370d2104f3727138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 7 Aug 2026 07:29:17 +0200 Subject: [PATCH 3/7] docs(runtime): record the #7213 allocation shape at the #7570 redirect Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- .../src/object/map_set_subclass.rs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/crates/perry-runtime/src/object/map_set_subclass.rs b/crates/perry-runtime/src/object/map_set_subclass.rs index 4d78d01edf..0d5b592c8c 100644 --- a/crates/perry-runtime/src/object/map_set_subclass.rs +++ b/crates/perry-runtime/src/object/map_set_subclass.rs @@ -129,6 +129,31 @@ pub(crate) fn subclass_backing_of(value: f64) -> Option { /// Marked `#[cold]`/`#[inline(never)]`: the genuine-header fast path never /// reaches here, and keeping the body out of line preserves the inlined /// receiver check at the ~57 `js_map_*` / `js_set_*` entry points. +/// +/// # This ALLOCATES, and its callers hold unrooted JSValue args +/// +/// [`subclass_backing_of`] builds the hidden field's key with +/// `js_string_from_bytes`, so reaching this arm is a collection point — and it +/// runs at the TOP of e.g. `js_map_set`, before that function roots its `key` / +/// `value` params. The exposure is the #7213 shape, and it is closed by the same +/// accident described in `string/alloc.rs`: an allocation here reaches the +/// alloc-point arm of `gc_check_trigger`, which takes +/// `ManualGcScanGuard::force_full_scan`, and a forced conservative stack scan +/// makes the copying minor ineligible. So the collection this can cause never +/// MOVES anything, and the same conservative scan finds the raw args on the +/// native stack. +/// +/// Recorded rather than pre-emptively fixed, for two reasons. The shape is +/// already load-bearing on hotter paths — `native_call_method`'s +/// `collection_methods.rs` calls `subclass_backing_of` on every native method +/// call on an object, and `field_get_set/get_field_by_name.rs` on every `.size` +/// read — so this adds no NEW class of exposure. And the obvious fix (a +/// thread-local caching the interned key `StringHeader`) is itself an unrooted +/// runtime cache of a heap pointer, the invisible-root hazard CLAUDE.md warns +/// about, which would have to be registered with +/// `gc_register_mutable_root_scanner` to be sound. If #7213's premise ever +/// changes — if the alloc-point arm stops forcing a conservative scan — this +/// call site must be revisited together with the two above. #[cold] #[inline(never)] pub(crate) fn redirect_collection_receiver(addr: usize, want: CollectionKind) -> usize { From 54985a1591d05a813c076c9c786db708d39e43a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 7 Aug 2026 07:36:59 +0200 Subject: [PATCH 4/7] fix(runtime): forEach's 3rd argument must stay the subclass instance (#7570) `js_map_foreach` / `js_set_foreach` derive the collection they report as the callback's 3rd argument (and the `self === m` identity) from the map being iterated. After the receiver resolution that is the hidden backing, not the instance, for a base-typed binding holding a `class X extends Map | Set`. Pass the receiver through as the collection override -- the same contract `js_map_foreach_with_collection` already serves for the unannotated path -- and only when the resolution actually moved, so a plain Map keeps `has_override == false` and behaves exactly as before. Gap test now asserts `self === m` inside forEach for the subclass, the Set twin, and the plain-Map control. Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- crates/perry-runtime/src/map.rs | 27 ++++++++++++++----- crates/perry-runtime/src/set.rs | 18 ++++++++----- ...est_gap_7570_map_set_declared_base_type.ts | 18 ++++++++++--- 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/crates/perry-runtime/src/map.rs b/crates/perry-runtime/src/map.rs index a6a559763c..33f8b02914 100644 --- a/crates/perry-runtime/src/map.rs +++ b/crates/perry-runtime/src/map.rs @@ -2360,12 +2360,27 @@ static KEEP_JS_MAP_FROM_ITERABLE: extern "C" fn(f64) -> *mut MapHeader = js_map_ /// when omitted at the call site. #[no_mangle] pub extern "C" fn js_map_foreach(map: *const MapHeader, callback: f64, this_arg: f64) { - js_map_foreach_impl( - map, - callback, - this_arg, - f64::from_bits(crate::value::TAG_UNDEFINED), - ); + js_map_foreach_impl(map, callback, this_arg, collection_override(map)); +} + +/// The `collection` argument `js_map_foreach_impl` should report as the 3rd +/// callback parameter and the `self === m` identity. +/// +/// `undefined` — meaning "derive it from the map being iterated" — unless the +/// receiver resolved to something else, i.e. it is a `class X extends Map` +/// instance reached through a base-typed binding (#7570). Iteration then runs +/// over the hidden backing, but the observable collection is still the +/// INSTANCE. This is the same contract `js_map_foreach_with_collection` already +/// serves for the unannotated path; without it, `m.forEach((v, k, self) => …)` +/// would hand user code the backing and `self === m` would be false. +#[inline(always)] +fn collection_override(map: *const MapHeader) -> f64 { + let receiver = map_receiver_identity(map); + let resolved = clean_map_ptr(map); + if resolved.is_null() || std::ptr::eq(resolved, receiver) { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + crate::value::js_nanbox_pointer(receiver as i64) } /// `Map.prototype.forEach` for a `class … extends Map` subclass instance: the diff --git a/crates/perry-runtime/src/set.rs b/crates/perry-runtime/src/set.rs index 1110c07495..e7b336f3f6 100644 --- a/crates/perry-runtime/src/set.rs +++ b/crates/perry-runtime/src/set.rs @@ -1538,12 +1538,18 @@ pub extern "C" fn js_set_from_iterable(value: f64) -> *mut SetHeader { /// omitted at the call site. #[no_mangle] pub extern "C" fn js_set_foreach(set: *const SetHeader, callback: f64, this_arg: f64) { - js_set_foreach_impl( - set, - callback, - this_arg, - f64::from_bits(crate::value::TAG_UNDEFINED), - ); + js_set_foreach_impl(set, callback, this_arg, collection_override(set)); +} + +/// The `Set` twin of `map::collection_override` — see its doc (#7570). +#[inline(always)] +fn collection_override(set: *const SetHeader) -> f64 { + let receiver = set_receiver_identity(set); + let resolved = clean_set_ptr(set); + if resolved.is_null() || std::ptr::eq(resolved, receiver) { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + crate::value::js_nanbox_pointer(receiver as i64) } /// `Set.prototype.forEach` for a `class … extends Set` subclass instance: the diff --git a/test-files/test_gap_7570_map_set_declared_base_type.ts b/test-files/test_gap_7570_map_set_declared_base_type.ts index d19476a7b8..253bb2efe1 100644 --- a/test-files/test_gap_7570_map_set_declared_base_type.ts +++ b/test-files/test_gap_7570_map_set_declared_base_type.ts @@ -109,10 +109,13 @@ console.log("6 entries:", JSON.stringify([...m6.entries()])); console.log("6 spread:", JSON.stringify([...m6])); console.log("6 Array.from:", JSON.stringify(Array.from(m6.keys()))); let acc6 = 0; -m6.forEach((v, k) => { +// The 3rd callback argument is the RECEIVER, not the hidden backing collection. +let sameMap6 = true; +m6.forEach((v, k, self) => { acc6 += v + k.length; + if (self !== m6) sameMap6 = false; }); -console.log("6 forEach:", acc6); +console.log("6 forEach:", acc6, sameMap6); const seen6: string[] = []; for (const [k, v] of m6) seen6.push(`${k}=${v}`); console.log("6 for-of:", seen6.join("|")); @@ -122,10 +125,12 @@ console.log("6 set values:", [...s6.values()].join(",")); console.log("6 set keys:", [...s6.keys()].join(",")); console.log("6 set spread:", [...s6].join(",")); let acc6s = 0; -s6.forEach((v) => { +let sameSet6 = true; +s6.forEach((v, _k, self) => { acc6s += v; + if (self !== s6) sameSet6 = false; }); -console.log("6 set forEach:", acc6s); +console.log("6 set forEach:", acc6s, sameSet6); const seen6s: number[] = []; for (const v of s6) seen6s.push(v); console.log("6 set for-of:", seen6s.join(",")); @@ -182,6 +187,11 @@ const plain: Map = new Map([["p", 1]]); plain.set("p2", 2); console.log("10 plain:", plain.get("p"), plain.size, [...plain.keys()].join(",")); console.log("10 plain identity:", plain.set("p3", 3) === plain, plain.size); +let plainSelf = true; +plain.forEach((_v, _k, self) => { + if (self !== plain) plainSelf = false; +}); +console.log("10 plain forEach self:", plainSelf); const plainSet: Set = new Set([1, 2]); plainSet.add(3); console.log("10 plainSet:", plainSet.size, plainSet.has(3), [...plainSet].join(",")); From 5505db46b6261bb252581f0e6f9e3a097483dd07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 7 Aug 2026 07:37:49 +0200 Subject: [PATCH 5/7] docs(changelog): note the forEach collection-identity arm (#7573) Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- changelog.d/7573-map-set-declared-base-type-receiver.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/changelog.d/7573-map-set-declared-base-type-receiver.md b/changelog.d/7573-map-set-declared-base-type-receiver.md index 25385c7246..d428d70658 100644 --- a/changelog.d/7573-map-set-declared-base-type-receiver.md +++ b/changelog.d/7573-map-set-declared-base-type-receiver.md @@ -71,7 +71,7 @@ subclassable native base", which would have cost the fast path for every `Map`-annotated binding in every program, and over a codegen-emitted guard, which would have had to be repeated at each of the ~20 lowering sites. -Three sites needed more than the funnel: +Four sites needed more than the funnel: * `js_set_add` / `js_set_has` / `js_set_delete` / `js_set_clear` / `js_set_to_array` never called `clean_set_ptr` at all — they went straight to @@ -85,6 +85,13 @@ Three sites needed more than the funnel: false and chaining handed out the backing. The receiver is rooted across the store (`RuntimeHandle::across_mut`), because it is a movable `ObjectHeader` and the store allocates. +* `js_map_foreach` / `js_set_foreach` derive the collection they report as the + callback's 3rd argument (and the `self === m` identity) from the map being + iterated, which after resolution is the backing. They now pass the receiver + through as the collection override — the same contract + `js_map_foreach_with_collection` already serves for the unannotated path — and + only when the resolution actually moved, so a plain Map keeps + `has_override == false` and behaves exactly as before. #### Validation From 3dd86854509d4bbb38ac5770ec265f5e682f5ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 7 Aug 2026 11:31:49 +0200 Subject: [PATCH 6/7] docs(changelog): record the post-rebase collection-family parity A/B (#7573) Claude-Session: https://claude.ai/code/session_019EHcmXKArA7m42SihYCcgH --- .../7573-map-set-declared-base-type-receiver.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/changelog.d/7573-map-set-declared-base-type-receiver.md b/changelog.d/7573-map-set-declared-base-type-receiver.md index d428d70658..b17be3ae7f 100644 --- a/changelog.d/7573-map-set-declared-base-type-receiver.md +++ b/changelog.d/7573-map-set-declared-base-type-receiver.md @@ -115,7 +115,18 @@ Four sites needed more than the funnel: (`diff` on `--trace llvm` output, 8,910 lines, zero differences) — plain `new Map()` still lowers to `js_map_set_string_number` / `js_map_get_string_key` / `js_map_size` exactly as before. -* `cargo test -p perry-runtime`: 1816 passed, 0 failed. +* Collection-family parity A/B, rebased onto `main` at v0.5.1334: the + `map` / `set` / `iter` / `weak` / `collection` / `foreach` / `spread` parity + sweeps (~110 tests) produce the **identical failure set** with the fix and with + `crates/perry-runtime/` reverted in full — `test_effect_pipe_map`, + `test_gap_2514_settracesigint`, `test_phase2v3_3_show_toast_set_text`, + `test_gap_ratelimiter_memory`, `test_issue_4034_object_literal_semantics`, + `test_issue_2656_weakref_finalization_gc`, `test_issue_610_foreach`. All + pre-existing; none references `Map`/`Set`. +* `cargo test -p perry-runtime`: 1842 passed, 0 failed. + `cargo test -p perry-codegen --lib`: 672 passed, 0 failed. +* Lint gates: `raw_handle_debt.py` 998 (baseline 998), `addr_class_inventory.py`, + `class_id_collisions.py`, `check_file_size.sh`, `cargo fmt --check` — all clean. #### Not fixed here From 4d1c6ae4abcdba4429bd96621f22a10cb4f76407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 7 Aug 2026 11:44:55 +0200 Subject: [PATCH 7/7] chore(version): bump to 0.5.1335 --- 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 e9305938a1..bb1a58d2ce 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.1334 +**Current Version:** 0.5.1335 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index c6036c0112..9cad5898d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1334" +version = "0.5.1335" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1334" +version = "0.5.1335" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1334" +version = "0.5.1335" [[package]] name = "perry-ui-tvos" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1334" +version = "0.5.1335" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index db7c3566ee..1d85cb9385 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1334" +version = "0.5.1335" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"