From b1e4c8932b17605ea52210015974bb73c982344a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 7 Aug 2026 23:57:47 +0200 Subject: [PATCH 1/2] fix(runtime): an Array subclass in a base-typed binding was read as a raw header (#7574) --- .../7603-array-subclass-declared-base-type.md | 53 +++++ crates/perry-codegen/src/expr/array_push.rs | 29 ++- crates/perry-codegen/src/expr/index_get.rs | 20 +- crates/perry-runtime/src/array/from_concat.rs | 19 ++ crates/perry-runtime/src/array/header.rs | 40 +++- crates/perry-runtime/src/array/indexing.rs | 34 ++- .../perry-runtime/src/array/iter_methods.rs | 23 +- crates/perry-runtime/src/array/mod.rs | 9 + crates/perry-runtime/src/array/push_pop.rs | 29 ++- crates/perry-runtime/src/array/subclass.rs | 221 ++++++++++++++++++ .../perry-runtime/src/array/subclass_tests.rs | 151 ++++++++++++ .../src/object/polymorphic_index.rs | 15 ++ crates/perry-runtime/src/proxy/put_value.rs | 28 ++- crates/perry-runtime/src/typed_feedback.rs | 13 ++ ..._7574_array_subclass_declared_base_type.ts | 184 +++++++++++++++ 15 files changed, 851 insertions(+), 17 deletions(-) create mode 100644 changelog.d/7603-array-subclass-declared-base-type.md create mode 100644 crates/perry-runtime/src/array/subclass_tests.rs create mode 100644 test-files/test_gap_7574_array_subclass_declared_base_type.ts diff --git a/changelog.d/7603-array-subclass-declared-base-type.md b/changelog.d/7603-array-subclass-declared-base-type.md new file mode 100644 index 0000000000..2ec8116b74 --- /dev/null +++ b/changelog.d/7603-array-subclass-declared-base-type.md @@ -0,0 +1,53 @@ +### fix(runtime): an Array subclass in a base-typed binding was read as a raw header (#7574) + +`const a: number[] = new MyArr()` (where `class MyArr extends Array {}`) +took the raw `ArrayHeader` fast paths and **SIGSEGVed on the second `.push()`** +(exit 139). Sibling of #7570/#7573 on a different family, and the same premise: +a declared TypeScript type is a hint, never a layout fact. All five binding +forms were affected — `const`, parameter, class field, return type, `as` cast. + +An Array-subclass instance is a plain `ObjectHeader` (perry has no exotic +array-object representation, and `js_array_subclass_init` keeps the elements as +ordinary indexed object properties). `ObjectHeader` overlays `ArrayHeader` field +for field: `length` reads `object_type` (= 1), `capacity` reads `class_id`, and +the element slots at +8/+16/+24 are `parent_class_id ‖ field_count`, +`keys_array` and `meta`. `1 <= class_id` passes `clean_arr_ptr`'s +length/capacity sanity check, so the forged header was accepted and the first +push stored `1.0` over `keys_array` — a live GC child edge — while `length + 1` +overwrote `object_type`. The second push dereferenced it and faulted at +`0x3ff0000000000000` (the bit pattern of the double it had just written). + +**Fix.** `clean_arr_ptr` now refuses a `GC_TYPE_OBJECT`/`GC_TYPE_CLOSURE` +allocation, making all ~190 of its call sites fail-closed at once (one extra +compare on a byte the surrounding block already loads; the registry probes that +rule out header-less buffers/typed arrays are cold-arm only). On top of that, +the entry points the declared-type tiers actually reach re-enter through their +existing null branch and run the operation on the spec-generic array-like +engine — the same engine the *unannotated* form has always used. Unlike Map/Set +there is nothing to redirect *to*: an Array subclass has no hidden backing, and +minting one would split element storage away from `Object.keys` / `for…in` / +`JSON.stringify` / the generic engine. + +Three sites needed more than the funnel: `forEach` must report the **receiver** +(not the dense snapshot `normalize_array_receiver` builds) as the callback's +3rd argument; `sub[i] = v` must run the Array-exotic `length` step, which was +missing on the unannotated path too (`sub[0] = 10; sub.length` read back `0`, +so the next `push` overwrote index 0); and `concat`'s all-dense bulk path had to +stop reading a refused pointer as an empty array, which would have silently +dropped `[1,2].concat(sub)`'s subclass elements. + +Two codegen guards are unavoidable — the inline `Expr::ArrayPush` store and +`lower_bounded_array_index_get` emit no runtime call at all, so no runtime +funnel can reach them. Both now test `obj_type == GC_TYPE_ARRAY` and route a +miss to the slow call they already had. Both are strictly more restrictive than +what they replaced, and the bounded-index one is a net instruction *cheaper*. + +Validated by `test-files/test_gap_7574_array_subclass_declared_base_type.ts` +(byte-identical to node, exit 0), six sabotage-shaped unit tests, a full revert +of both crates reproducing exit 139, a normalized-IR diff on plain-array +programs showing the only delta is the guard predicate, and a 141-test +array-family A/B with an identical failure set. + +Known gap left in place: `ArraySpeciesCreate` on a subclass (node's +`sub.map(f)` returns a `MyArr`, perry a plain `Array`) — pre-existing and +identical on the unannotated path. diff --git a/crates/perry-codegen/src/expr/array_push.rs b/crates/perry-codegen/src/expr/array_push.rs index 998798b378..be5eafe3c5 100644 --- a/crates/perry-codegen/src/expr/array_push.rs +++ b/crates/perry-codegen/src/expr/array_push.rs @@ -324,11 +324,38 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr, value_discarded: bool) -> // head — the inline path's offset-0 length read would // otherwise pick up the lower 32 bits of the // forwarding pointer (garbage). + // + // #7574: the same load also has to prove the receiver IS an + // array. `Expr::ArrayPush` is folded from the receiver's + // DECLARED type, and a declared type is a hint, never a layout + // fact (CLAUDE.md, *Known Limitations*), so + // `const a: number[] = new MyArr()` — a `class X extends Array` + // instance, which perry models as a plain `ObjectHeader` — + // reached the inline store below. `ObjectHeader` overlays + // `ArrayHeader` field for field, so `length` read + // `object_type` (= 1) and `capacity` read `class_id` (large): + // `1 < class_id` passed the in-bounds test and the value was + // stored at `handle + 8 + 1*8` — i.e. over `ObjectHeader + // .keys_array`, a live GC child edge — while `length + 1` + // overwrote `object_type`. The SECOND push then SIGSEGVed + // (exit 139) dereferencing `keys_array`, whose bytes were now + // the double `1.0` (fault address `0x3ff0000000000000`). + // + // Route any non-`GC_TYPE_ARRAY` receiver to `js_array_push_f64` + // — the same slow arm forwarding already uses — which resolves + // an array-like object receiver onto the spec-generic engine. + // Strictly more restrictive than the old test: nothing that + // used to take the slow arm now takes the inline store. + let gc_type_addr = blk.sub(I64, &arr_handle, "8"); + let gc_type_ptr = blk.inttoptr(I64, &gc_type_addr); + let gc_type = blk.load(I8, &gc_type_ptr); + let not_array = blk.icmp_ne(I8, &gc_type, "1"); // != GC_TYPE_ARRAY let gc_flags_addr = blk.sub(I64, &arr_handle, "7"); let gc_flags_ptr = blk.inttoptr(I64, &gc_flags_addr); let gc_flags = blk.load(I8, &gc_flags_ptr); let fwd_bits = blk.and(I8, &gc_flags, "128"); - let is_fwd = blk.icmp_ne(I8, &fwd_bits, "0"); + let fwd_set = blk.icmp_ne(I8, &fwd_bits, "0"); + let is_fwd = blk.or(I1, ¬_array, &fwd_set); let fwd_idx = ctx.new_block("apush.fwd"); let nofwd_idx = ctx.new_block("apush.nofwd"); diff --git a/crates/perry-codegen/src/expr/index_get.rs b/crates/perry-codegen/src/expr/index_get.rs index d478d9028a..136d294b07 100644 --- a/crates/perry-codegen/src/expr/index_get.rs +++ b/crates/perry-codegen/src/expr/index_get.rs @@ -542,16 +542,32 @@ fn lower_bounded_array_index_get( // bytes at `arr + 8 + idx*8`, so route through the slow path only when // the receiver is lazy. Issue #233: also detect FORWARDED arrays; the // slow path's `clean_arr_ptr` follows the chain. + // + // #7574: the test is now POSITIVE — `obj_type == GC_TYPE_ARRAY` — instead + // of "not lazy". `is_array_expr` is satisfied by a DECLARED `Type::Array`, + // and a declared type is a hint, never a layout fact (CLAUDE.md, *Known + // Limitations*), so `const a: number[] = new MyArr()` (a `class X extends + // Array` instance — a plain `ObjectHeader`) reached the raw + // `gep + load double` at `handle + 8 + idx*8`, i.e. straight into + // `parent_class_id ‖ field_count`, then the `keys_array` and `meta` + // POINTERS — reading two live GC child edges out as user doubles. The + // sibling tier in `index_get/guarded_array.rs` has always tested + // `GC_TYPE_ARRAY` here; this one only excluded lazy arrays. + // + // Strictly more restrictive than the old test (`GC_TYPE_LAZY_ARRAY` is 9, + // so `!= GC_TYPE_ARRAY` subsumes `== GC_TYPE_LAZY_ARRAY`): no receiver that + // used to take the slow path now takes the fast one. It is also one + // instruction CHEAPER — a single `icmp ne` replaces `icmp eq` + `or`. let gc_type_addr = blk.sub(I64, &arr_handle, "8"); let gc_type_ptr = blk.inttoptr(I64, &gc_type_addr); let gc_type = blk.load(I8, &gc_type_ptr); - let is_lazy = blk.icmp_eq(I8, &gc_type, "9"); // GC_TYPE_LAZY_ARRAY + let not_array = blk.icmp_ne(I8, &gc_type, "1"); // != GC_TYPE_ARRAY let gc_flags_addr = blk.sub(I64, &arr_handle, "7"); let gc_flags_ptr = blk.inttoptr(I64, &gc_flags_addr); let gc_flags = blk.load(I8, &gc_flags_ptr); let fwd_bits = blk.and(I8, &gc_flags, "128"); // GC_FLAG_FORWARDED let is_fwd = blk.icmp_ne(I8, &fwd_bits, "0"); - let needs_slow = blk.or(I1, &is_lazy, &is_fwd); + let needs_slow = blk.or(I1, ¬_array, &is_fwd); // Index accessors / custom attribute descriptors (`Object.defineProperty // (arr, i, { get })`) divert element reads through the descriptor tables — // the raw slot load below would bypass them (test262 sort/precise-*). diff --git a/crates/perry-runtime/src/array/from_concat.rs b/crates/perry-runtime/src/array/from_concat.rs index d33f9dd720..8655091549 100644 --- a/crates/perry-runtime/src/array/from_concat.rs +++ b/crates/perry-runtime/src/array/from_concat.rs @@ -781,6 +781,15 @@ unsafe fn peek_plain_array_len(arr: *const ArrayHeader) -> Option { if crate::array::array_ptr_as_proxy(arr).is_some() { return None; } + // #7574: `clean_arr_ptr` now REFUSES a `GC_TYPE_OBJECT` allocation, so an + // array-like object — a `class X extends Array` instance among them — would + // fall into the null arm below and be mis-sized as an EMPTY array. It used + // to reach the `obj_type != GC_TYPE_ARRAY` test and answer `None` + // ("un-peekable, size it as 1 and take the spec-shaped per-source flow"). + // Keep that answer: classify BEFORE the null shortcut. + if crate::array::subclass::raw_receiver_is_heap_object(arr) { + return None; + } let arr = clean_arr_ptr(arr); if arr.is_null() { return Some(0); @@ -825,6 +834,16 @@ unsafe fn dense_concat_array_source(src: *const ArrayHeader) -> Option<(*const A if crate::array::array_ptr_as_proxy(src).is_some() { return None; } + // #7574: same hazard as `peek_plain_array_len` — but here mis-classifying a + // `class X extends Array` argument as an empty dense source would SILENTLY + // DROP its elements, because this bulk path returns `Some(out)` and the + // spec-shaped `append_concat_arg` flow (which has the subclass snapshot + // arm) never runs. `[1, 2].concat(sub)` yielded `1,2`. Reject it here so + // the caller falls through, exactly as it did before `clean_arr_ptr` + // started refusing object receivers. + if crate::array::subclass::raw_receiver_is_heap_object(src) { + return None; + } let src = clean_arr_ptr(src); if src.is_null() { return Some((src, 0)); diff --git a/crates/perry-runtime/src/array/header.rs b/crates/perry-runtime/src/array/header.rs index 5805869eac..69acc2f815 100644 --- a/crates/perry-runtime/src/array/header.rs +++ b/crates/perry-runtime/src/array/header.rs @@ -620,13 +620,51 @@ pub(crate) fn clean_arr_ptr(arr: *const ArrayHeader) -> *const ArrayHeader { if (cleaned as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 { let gc_header = (cleaned as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc_header).obj_type == crate::gc::GC_TYPE_LAZY_ARRAY { + let obj_type = (*gc_header).obj_type; + if obj_type == crate::gc::GC_TYPE_LAZY_ARRAY { let lazy = cleaned as *mut crate::json_tape::LazyArrayHeader; if (*lazy).magic == crate::json_tape::LAZY_ARRAY_MAGIC { let materialized = crate::json_tape::force_materialize_lazy(lazy); return materialized as *const ArrayHeader; } } + // #7574: a `GC_TYPE_OBJECT` / `GC_TYPE_CLOSURE` allocation is NOT + // an `ArrayHeader`, and the two layouts overlay field for field — + // `ArrayHeader.length` reads `ObjectHeader.object_type` (= 1), + // `.capacity` reads `class_id`, and the element slots at +8/+16/+24 + // are `parent_class_id ‖ field_count`, `keys_array` and `meta`. The + // sanity check below waves that through (1 <= class_id <= 100M), so + // an element WRITE overwrites two live GC child edges with + // arbitrary doubles and the collector then traces them: `class X + // extends Array` in a `T[]`-annotated binding SIGSEGVs on its + // second `.push()`. + // + // A declared TypeScript type is a hint, never a layout fact + // (CLAUDE.md, *Known Limitations*), so this is reachable from every + // binding form. Refusing it here makes ALL ~190 `clean_arr_ptr` + // call sites fail-closed at once — each degrades through its + // existing null branch instead of dereferencing a forged header — + // and it is the same "resolve at the shared runtime funnel, not at + // one codegen predicate at a time" shape #7573 used for Map/Set. + // + // Correctness (rather than mere safety) for the entry points the + // declared-type tiers actually reach is layered on top: those null + // branches re-enter through `array::subclass::array_object_*`, + // which runs the operation on the spec-generic array-like engine. + // + // Costs one compare on a byte this block already loaded. Buffers + // and typed arrays are `std::alloc`-backed with no `GcHeader`, so + // their preceding bytes are allocator bookkeeping that can read as + // any value — confirm against the registries before nulling, in the + // cold arm only. + if obj_type == crate::gc::GC_TYPE_OBJECT || obj_type == crate::gc::GC_TYPE_CLOSURE { + let addr = cleaned as usize; + if !crate::buffer::is_registered_buffer(addr) + && crate::typedarray::lookup_typed_array_kind(addr).is_none() + { + return std::ptr::null(); + } + } } } // Length/capacity sanity: dense arrays have length <= capacity and diff --git a/crates/perry-runtime/src/array/indexing.rs b/crates/perry-runtime/src/array/indexing.rs index e8d2ba4c1f..3885be789c 100644 --- a/crates/perry-runtime/src/array/indexing.rs +++ b/crates/perry-runtime/src/array/indexing.rs @@ -660,10 +660,15 @@ pub extern "C" fn js_array_get_element_f64(arr: i64, index: i64) -> f64 { /// Use when the codegen KNOWS the pointer is a plain Array (not Map/Set/Buffer). #[no_mangle] pub extern "C" fn js_array_get_f64_unchecked(arr: *const ArrayHeader, index: u32) -> f64 { - let arr = clean_arr_ptr(arr); - if arr.is_null() { + let cleaned = clean_arr_ptr(arr); + if cleaned.is_null() { + // #7574: array-like OBJECT receiver — see `js_array_get_f64`. + if crate::array::subclass::array_object_receiver(arr).is_some() { + return js_array_get_f64(arr, index); + } return f64::NAN; } + let arr = cleaned; // Index accessors / custom attrs installed via `Object.defineProperty` // need the descriptor-aware getter. if array_object_flags(arr) & crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS != 0 { @@ -763,10 +768,17 @@ pub extern "C" fn js_array_get_f64(arr: *const ArrayHeader, index: u32) -> f64 { } } } - let arr = clean_arr_ptr(arr); - if arr.is_null() { + let cleaned = clean_arr_ptr(arr); + if cleaned.is_null() { + // #7574: `a[i]` on a `class X extends Array` instance held in a + // `T[]`-annotated binding. Read the object's indexed property through + // the spec-generic `Get`, not the `ObjectHeader` words. + if let Some(recv) = crate::array::subclass::array_object_receiver(arr) { + return crate::array::subclass::array_object_index_get(recv, index); + } return f64::NAN; } + let arr = cleaned; // Check if this is actually a TypedArray — dispatch through typed array helper if crate::typedarray::lookup_typed_array_kind(arr as usize).is_some() { return crate::typedarray::js_typed_array_get( @@ -1074,10 +1086,20 @@ pub extern "C" fn js_array_set_f64_extend( ) -> *mut ArrayHeader { // Demote a uniquely-owned string source — see `js_array_set_f64`. crate::string::js_string_addref_if_heap_string(value); - let arr = clean_arr_ptr_mut(arr); - if arr.is_null() { + let cleaned = clean_arr_ptr_mut(arr); + if cleaned.is_null() { + // #7574: `a[i] = v` on a `class X extends Array` instance held in a + // `T[]`-annotated binding. Pre-fix this stored the value into + // `ObjectHeader.keys_array` / `.meta`. Run the object `[[Set]]` plus + // the Array-exotic `length` maintenance, and return the ORIGINAL + // receiver so the caller's realloc write-back keeps the binding. + if let Some(recv) = crate::array::subclass::array_object_receiver(arr) { + crate::array::subclass::array_object_index_set(recv, index, value); + return arr; + } return js_array_alloc(0); } + let arr = cleaned; // If this write targets `Array.prototype`, mark the prototype as carrying an // indexed property so out-of-bounds element reads on ordinary arrays consult // it (ECMA-262 OrdinaryGet → prototype chain). Cheap no-op otherwise. diff --git a/crates/perry-runtime/src/array/iter_methods.rs b/crates/perry-runtime/src/array/iter_methods.rs index e719e19f85..06993d7a25 100644 --- a/crates/perry-runtime/src/array/iter_methods.rs +++ b/crates/perry-runtime/src/array/iter_methods.rs @@ -107,6 +107,18 @@ impl Drop for DenseThisGuard { /// Returns nothing (void) #[no_mangle] pub extern "C" fn js_array_forEach(arr: *const ArrayHeader, callback: *const ClosureHeader) { + // #7574: `normalize_array_receiver` materializes an array-like OBJECT + // receiver — a `class X extends Array` instance among them — into a fresh + // dense snapshot. The spec passes the RECEIVER as the callback's 3rd + // argument, so without this the callback saw the snapshot and + // `self === sub` was false (the same "forEach's 3rd argument" obligation + // #7573 hit for Map/Set). Gated on a one-load `GC_TYPE_OBJECT` header test, + // so a genuine array pays a compare and never enters the registry probes. + let self_override = if crate::array::subclass::raw_receiver_is_heap_object(arr) { + crate::array::subclass::array_object_receiver(arr) + } else { + None + }; let arr = normalize_array_receiver(arr); if arr.is_null() { return; @@ -142,6 +154,13 @@ pub extern "C" fn js_array_forEach(arr: *const ArrayHeader, callback: *const Clo let length = (*arr).length; let scope = crate::gc::RuntimeHandleScope::new(); let rooted = RootedIterArray::new(&scope, arr); + // The override is a movable `ObjectHeader` held across user callbacks + // that allocate — root it for the duration of the loop. + let self_handle = self_override.map(|recv| scope.root_nanbox_f64(recv)); + let self_value = |rooted: &RootedIterArray| match &self_handle { + Some(h) => h.get_nanbox_f64(), + None => rooted.receiver(), + }; let _tg = DenseThisGuard::bind_undefined(); if crate::array::array_iteration_is_exotic(arr) { for i in 0..length as usize { @@ -150,7 +169,7 @@ pub extern "C" fn js_array_forEach(arr: *const ArrayHeader, callback: *const Clo continue; } let element = crate::array::array_spec_get(arr, i as u32); - js_closure_call3(callback, element, i as f64, rooted.receiver()); + js_closure_call3(callback, element, i as f64, self_value(&rooted)); } return; } @@ -162,7 +181,7 @@ pub extern "C" fn js_array_forEach(arr: *const ArrayHeader, callback: *const Clo // dispatch path supports call3 safely, so bound native // methods like `array.forEach(console.log)` can observe the // source array just like Node. - js_closure_call3(callback, element, i as f64, rooted.receiver()); + js_closure_call3(callback, element, i as f64, self_value(&rooted)); } } } diff --git a/crates/perry-runtime/src/array/mod.rs b/crates/perry-runtime/src/array/mod.rs index d616e27783..a9e5086188 100644 --- a/crates/perry-runtime/src/array/mod.rs +++ b/crates/perry-runtime/src/array/mod.rs @@ -27,6 +27,8 @@ mod subclass; #[cfg(test)] mod spread_dense_tests; #[cfg(test)] +mod subclass_tests; +#[cfg(test)] mod tests; pub(crate) use self::alloc::{array_length_range_error, js_array_alloc_pointer_elements}; @@ -141,6 +143,13 @@ pub(crate) use self::sort::object_prototype_index_get as sort_object_prototype_i pub use self::subclass::{ array_subclass_dense_snapshot, array_subclass_has_iterator_override, is_array_subclass_instance, }; +// #7574 — array-like OBJECT receiver resolution for the raw `js_array_*` entry +// points, plus the Array-exotic `length` maintenance the generic OBJECT index +// store needs for a `class X extends Array` receiver. +pub(crate) use self::subclass::{ + array_object_set_length, is_array_subclass_class_id, is_array_subclass_value, + maintain_array_exotic_length, note_array_subclass_index_write, +}; // Issue #1572 — flatten helpers reused by `node_stream::ns_iter_flat_map` // so an `async function*` mapper return is driven through the iterator // protocol instead of being appended as a single chunk. diff --git a/crates/perry-runtime/src/array/push_pop.rs b/crates/perry-runtime/src/array/push_pop.rs index a7b4a9649f..79416c9d67 100644 --- a/crates/perry-runtime/src/array/push_pop.rs +++ b/crates/perry-runtime/src/array/push_pop.rs @@ -589,10 +589,23 @@ pub extern "C" fn js_array_push_f64(arr: *mut ArrayHeader, value: f64) -> *mut A } return arr; } - let arr = clean_arr_ptr_mut(arr); - if arr.is_null() { + let cleaned = clean_arr_ptr_mut(arr); + if cleaned.is_null() { + // #7574: a `class X extends Array` instance (or any array-like object) + // in a `T[]`-annotated binding. Pre-fix `clean_arr_ptr` waved its + // `ObjectHeader` through and the store below overwrote `keys_array` / + // `meta` — the SECOND push SIGSEGVed (exit 139). Run the spec-generic + // `Array.prototype.push` on the object instead, and return the ORIGINAL + // receiver so codegen's realloc write-back leaves the binding pointing + // at the instance (returning a fresh empty array here is what made the + // push look silently dropped). + if let Some(recv) = crate::array::subclass::array_object_receiver(arr) { + crate::array::subclass::array_object_method(recv, "push", &[value]); + return arr; + } return js_array_alloc(0); } + let arr = cleaned; if array_is_frozen(arr) { throw_frozen_array_mutation(); } @@ -786,10 +799,18 @@ pub extern "C" fn js_array_pop_f64(arr: *mut ArrayHeader) -> f64 { /// here. test262 built-ins/Array length-write-on-frozen. #[no_mangle] pub extern "C" fn js_array_set_length_strict(arr: *mut ArrayHeader, new_length: f64) { - let arr = clean_arr_ptr_mut(arr); - if arr.is_null() { + let cleaned = clean_arr_ptr_mut(arr); + if cleaned.is_null() { + // #7574: `a.length = n` on a `class X extends Array` instance reached + // here through the `is_array_expr`-keyed `property_set` lowering and + // wrote `ObjectHeader.object_type`. Perform the Array-exotic + // `Set(O, "length", n, true)` on the object instead. + if let Some(recv) = crate::array::subclass::array_object_receiver(arr) { + crate::array::subclass::array_object_set_length(recv, new_length); + } return; } + let arr = cleaned; if array_object_flags(arr) & crate::gc::OBJ_FLAG_FROZEN != 0 { throw_non_writable_length(); } diff --git a/crates/perry-runtime/src/array/subclass.rs b/crates/perry-runtime/src/array/subclass.rs index 8ed0ca7502..60105bd952 100644 --- a/crates/perry-runtime/src/array/subclass.rs +++ b/crates/perry-runtime/src/array/subclass.rs @@ -117,3 +117,224 @@ pub fn array_subclass_has_iterator_override(value: f64) -> bool { let class_id = crate::object::js_object_get_class_id(raw as *const ObjectHeader); class_id != 0 && crate::object::method_owner_class_id(class_id, "@@iterator").is_some() } + +// --------------------------------------------------------------------------- +// #7574 — raw `js_array_*` receiver resolution for an array-like OBJECT. +// +// Codegen decides "this receiver is an Array" from the DECLARED TypeScript type +// of the binding (`is_array_expr` / `Type::Array(_)` / `Generic { base: "Array" }`), +// then emits a raw `js_array_*` call whose first act is to dereference the +// receiver as an `ArrayHeader`. A declared type is a hint, never a layout fact +// (CLAUDE.md, *Known Limitations*: annotations are erased and nothing validates +// them at runtime), so any binding annotated with the BASE type — `const a: +// number[] = new MyArr()`, a parameter, a class field, a return type, an +// `as number[]` cast — can be holding a `class X extends Array` instance, which +// perry models as a plain `ObjectHeader`. The two headers overlay field for +// field: +// +// ArrayHeader.length (u32 @0) <- ObjectHeader.object_type (= 1) +// ArrayHeader.capacity (u32 @4) <- ObjectHeader.class_id +// elements[0] (@8) <- parent_class_id || field_count +// elements[1] (@16) <- keys_array (a *mut ArrayHeader) +// elements[2] (@24) <- meta (a *mut ObjectMeta) +// +// so element WRITES overwrite two live GC child edges with arbitrary doubles — +// the collector then traces whatever the mutator stored. `a.push(1); a.push(2)` +// SIGSEGVs (exit 139) on the second push. +// +// `clean_arr_ptr` now refuses a `GC_TYPE_OBJECT` allocation outright, which +// makes every one of its ~190 call sites fail-CLOSED. That is the memory-safety +// half. The correctness half is these helpers: the entry points reachable from +// the declared-type codegen tiers re-enter through their EXISTING null branch +// and run the operation on the spec-generic array-like engine +// (`super::generic` / `super::generic_object`), which already models an Array +// subclass correctly — it is the same engine the UNANNOTATED path has always +// used via `js_native_call_method`. +// +// Unlike #7573's Map/Set fix there is nothing to *redirect* to: an Array +// subclass instance has no hidden backing collection (`js_array_subclass_init` +// installs a `length` own property and the elements are ordinary indexed object +// properties — see `node_stream_constructors/builders.rs`). Minting one would +// split element storage in two, since `Object.keys` / `for…in` / +// `JSON.stringify` / the generic engine all read the object's own properties; +// the answer here is therefore "run the generic engine", not "redirect". +// --------------------------------------------------------------------------- + +/// The array-like OBJECT receiver a raw `js_array_*` entry point must actually +/// run on, or `None` when the pointer is not one. +/// +/// Admits exactly what [`super::generic::plain_object_value`] admits — an +/// object literal, an anonymous shape, or a `class X extends Array` instance — +/// so ordinary user-class instances, real arrays, typed arrays, buffers, and +/// proxies all answer `None` and keep their existing behaviour. +/// +/// Marked `#[cold]`/`#[inline(never)]`: every caller reaches it only from a +/// branch `clean_arr_ptr` already refused, so a genuine `ArrayHeader` never +/// executes a byte of this. +/// One-load brand pre-filter: true only when `arr` has a readable `GcHeader` +/// saying `GC_TYPE_OBJECT`. A genuine `ArrayHeader` answers false without +/// touching a side table, so callers can gate the (registry-probing) +/// [`array_object_receiver`] behind it on a hot path. Uses +/// `addr_class::try_read_gc_header`, which magnitude-classifies the address +/// before any dereference. +#[inline] +pub(crate) fn raw_receiver_is_heap_object(arr: *const ArrayHeader) -> bool { + let raw = ((arr as u64) & 0x0000_FFFF_FFFF_FFFF) as usize; + if raw == 0 { + return false; + } + match unsafe { crate::value::addr_class::try_read_gc_header(raw) } { + Some(header) => header.obj_type == crate::gc::GC_TYPE_OBJECT, + None => false, + } +} + +#[cold] +#[inline(never)] +pub(crate) fn array_object_receiver(arr: *const ArrayHeader) -> Option { + let raw = (arr as u64) & 0x0000_FFFF_FFFF_FFFF; + if raw == 0 { + return None; + } + super::generic::plain_object_value(raw as *const ArrayHeader) +} + +/// True when `value` is a live `class X extends Array` INSTANCE — the +/// annotation-independent brand test the generic `[[Set]]` funnels use to +/// decide whether the Array-exotic `length` steps apply. +pub(crate) fn is_array_subclass_value(value: f64) -> bool { + if !JSValue::from_bits(value.to_bits()).is_pointer() { + return false; + } + let raw = (value.to_bits() & 0x0000_FFFF_FFFF_FFFF) as *const ObjectHeader; + // `raw_receiver_is_heap_object` magnitude-classifies through + // `addr_class::try_read_gc_header` and proves `GC_TYPE_OBJECT` before the + // class-id read below dereferences the header. + if !raw_receiver_is_heap_object(raw as *const ArrayHeader) { + return false; + } + let class_id = crate::object::js_object_get_class_id(raw); + class_id != 0 && is_array_subclass_class_id(class_id) +} + +/// Run an `Array.prototype` method generically on the array-like object +/// `recv`, covering both the mutating family (`push` / `pop` / `shift` / +/// `unshift` / `reverse` / `splice` / `sort` / `concat`) and the read family +/// (`map` / `filter` / `forEach` / `join` / `slice` / `indexOf` / …). +/// +/// Returns `None` only for a method name neither engine implements. +#[cold] +#[inline(never)] +pub(crate) fn array_object_method(recv: f64, method: &str, args: &[f64]) -> Option { + let (ptr, len) = (args.as_ptr(), args.len()); + if let Some(result) = super::generic::run_object_mutator(recv, method, ptr, len) { + return Some(result); + } + super::generic::dispatch_arraylike_read_method(recv, method, ptr, len) +} + +/// `Get(recv, ToString(index))` for an array-like object receiver. +#[cold] +#[inline(never)] +pub(crate) fn array_object_index_get(recv: f64, index: u32) -> f64 { + al_get(recv, index as i64) +} + +/// `Set(recv, ToString(index), value, …)` PLUS the Array-exotic `length` +/// maintenance the receiver's class inherits from `Array`. +/// +/// A `class X extends Array` instance is a real Array in JavaScript, so +/// `sub[3] = v` sets `length` to 4 (ECMA-262 §10.4.2.1 +/// `ArraySetLength`/`ArrayDefineOwnProperty`). Perry models the instance as a +/// plain object, whose `[[DefineOwnProperty]]` has no such step — pre-fix +/// `sub[0] = 10; sub.length` read back `0`, on the ANNOTATED and unannotated +/// paths alike. Emulate the exotic step here so both agree with node. +#[cold] +#[inline(never)] +pub(crate) fn array_object_index_set(recv: f64, index: u32, value: f64) { + // The store interns a key string and can allocate, so root the receiver + // across it — it is a movable `ObjectHeader` and is read again below. + let scope = crate::gc::RuntimeHandleScope::new(); + let handle = scope.root_nanbox_f64(recv); + crate::object::js_object_set_index_polymorphic( + (handle.get_nanbox_f64().to_bits() & 0x0000_FFFF_FFFF_FFFF) as i64, + index as f64, + value, + ); + maintain_array_exotic_length(handle.get_nanbox_f64(), index); +} + +/// The Array-exotic `length` step for an indexed own-property write, applied by +/// the two generic OBJECT store funnels (`js_put_value_set` and +/// `js_object_set_index_polymorphic`) AFTER the store has landed. +/// +/// `sub[3] = v` on a `class X extends Array` instance must leave `length == 4`. +/// Perry models the instance as a plain object, so nothing in its +/// `[[DefineOwnProperty]]` does that — pre-fix `sub[0] = 10; sub.length` read +/// back `0` on the annotated AND unannotated paths alike, which then made the +/// next `sub.push(v)` append at index 0 and overwrite the element. +/// +/// Gated on the receiver's class chain reaching `Array`, so an object literal +/// (`class_id == 0`) short-circuits on one load and an ordinary class instance +/// on a bounded parent walk. `key` is a property-key VALUE; a non-canonical +/// array index (`"length"`, `"foo"`, `"01"`, a symbol) is a no-op. +pub(crate) fn note_array_subclass_index_write(recv: f64, key: f64) { + if !is_array_subclass_value(recv) { + return; + } + let key_ptr = crate::value::js_jsvalue_to_string(key) as *const crate::string::StringHeader; + // The `&str` borrows the heap `StringHeader`'s bytes. `canonical_array_index` + // only parses digits — it allocates nothing, so the borrow cannot straddle a + // collection point (the `&[u8]`-into-a-StringHeader hazard in CLAUDE.md). + let index = unsafe { + match crate::object::has_own_helpers::str_from_string_header(key_ptr) + .and_then(crate::object::canonical_array_index) + { + Some(i) => i, + None => return, + } + }; + maintain_array_exotic_length(recv, index); +} + +/// The `length`-bumping half of `array_object_index_set`, split out so the +/// generic OBJECT index-store funnels can apply it without re-entering the +/// store. +pub(crate) fn maintain_array_exotic_length(recv: f64, index: u32) { + let current = al_length(recv); + if (index as i64) < current { + return; + } + // `js_string_from_bytes` ALLOCATES, so it is a collection point: root the + // receiver and re-read it afterwards rather than deriving the raw pointer + // first (the #7192 store-after-an-allocating-call shape — a movable + // `ObjectHeader` written through a pre-allocation address lands on a + // forwarding stub). + let scope = crate::gc::RuntimeHandleScope::new(); + let handle = scope.root_nanbox_f64(recv); + let key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); + let raw = (handle.get_nanbox_f64().to_bits() & 0x0000_FFFF_FFFF_FFFF) as *mut ObjectHeader; + crate::object::js_object_set_field_by_name(raw, key, (index as f64) + 1.0); +} + +/// `Set(recv, "length", new_length, true)` for an array-like object receiver: +/// truncating deletes the indices at or above the new length, exactly as the +/// Array-exotic `[[DefineOwnProperty]]` would. +#[cold] +#[inline(never)] +pub(crate) fn array_object_set_length(recv: f64, new_length: f64) { + if !new_length.is_finite() || new_length < 0.0 || new_length.trunc() != new_length { + crate::array::array_length_range_error(); + } + let scope = crate::gc::RuntimeHandleScope::new(); + let handle = scope.root_nanbox_f64(recv); + let target = new_length as i64; + let current = al_length(handle.get_nanbox_f64()); + for k in target..current { + let raw = (handle.get_nanbox_f64().to_bits() & 0x0000_FFFF_FFFF_FFFF) as *mut ObjectHeader; + crate::object::js_object_delete_dynamic(raw, k as f64); + } + let raw = (handle.get_nanbox_f64().to_bits() & 0x0000_FFFF_FFFF_FFFF) as *mut ObjectHeader; + let key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); + crate::object::js_object_set_field_by_name(raw, key, new_length); +} diff --git a/crates/perry-runtime/src/array/subclass_tests.rs b/crates/perry-runtime/src/array/subclass_tests.rs new file mode 100644 index 0000000000..ec2c4b8037 --- /dev/null +++ b/crates/perry-runtime/src/array/subclass_tests.rs @@ -0,0 +1,151 @@ +//! #7574 — `class X extends Array` in a `T[]`-annotated binding took the raw +//! `ArrayHeader` fast paths. +//! +//! Every test here is **sabotage-shaped**: it first asserts that the bytes the +//! pre-fix code misread are *still sitting there* — an `ObjectHeader` read as +//! an `ArrayHeader` yields `length == object_type == 1` and +//! `capacity == class_id`, both of which sail through `clean_arr_ptr`'s +//! length/capacity sanity check — and only then that the entry point refuses +//! or resolves it. A green run therefore proves the brand check FIRED, not that +//! the receiver happened to look invalid for some unrelated reason. + +use super::subclass::{ + array_object_receiver, is_array_subclass_class_id, raw_receiver_is_heap_object, +}; +use crate::array::{clean_arr_ptr, js_array_alloc, ArrayHeader}; +use crate::object::{js_object_alloc, ObjectHeader}; + +/// The reserved parent class id `class X extends Array` records. +const CLASS_ID_ARRAY: u32 = 0xFFFF_0024; + +fn as_array_header(obj: *mut ObjectHeader) -> *const ArrayHeader { + obj as *const ArrayHeader +} + +/// The overlay that makes this bug possible, pinned. If `ObjectHeader` ever +/// stops starting with `object_type: u32, class_id: u32`, the misread this +/// whole family defends against changes shape and these tests must be revisited. +#[test] +fn object_header_still_overlays_array_header_length_and_capacity() { + let class_id = 0x7574_0001; + let obj = js_object_alloc(class_id, 2); + assert!(!obj.is_null()); + let hdr = as_array_header(obj); + unsafe { + assert_eq!( + (*hdr).length, + 1, + "ArrayHeader.length must still alias ObjectHeader.object_type (= 1)" + ); + assert_eq!( + (*hdr).capacity, + class_id, + "ArrayHeader.capacity must still alias ObjectHeader.class_id" + ); + // The sanity check `clean_arr_ptr` applied BEFORE the fix: `length <= + // capacity && length <= 100M`. Both hold, which is precisely why the + // forged header was waved through and `push` stored over `keys_array`. + assert!((*hdr).length <= (*hdr).capacity); + assert!((*hdr).length <= 100_000_000); + } +} + +#[test] +fn clean_arr_ptr_refuses_a_plain_object_receiver() { + let obj = js_object_alloc(0x7574_0002, 2); + let hdr = as_array_header(obj); + unsafe { + // Sabotage precondition: the forged (length, capacity) pair is still + // acceptable to the pre-fix sanity check. + assert!((*hdr).length <= (*hdr).capacity); + } + assert!( + clean_arr_ptr(hdr).is_null(), + "an ObjectHeader must not resolve to an ArrayHeader" + ); +} + +#[test] +fn a_genuine_array_takes_the_fast_path_and_is_never_redirected() { + let arr = js_array_alloc(4); + assert!(!arr.is_null()); + assert_eq!( + clean_arr_ptr(arr as *const ArrayHeader), + arr as *const ArrayHeader, + "a real ArrayHeader must pass clean_arr_ptr unchanged" + ); + // The #7573 lesson: prove the fast path is not merely agreeing with a + // redirect that happened to return the same thing. + assert!( + !raw_receiver_is_heap_object(arr as *const ArrayHeader), + "the one-load brand pre-filter must answer false for GC_TYPE_ARRAY" + ); + assert!( + array_object_receiver(arr as *const ArrayHeader).is_none(), + "a real ArrayHeader must never resolve to an array-like OBJECT receiver" + ); +} + +#[test] +fn array_object_receiver_admits_an_array_subclass_instance() { + let class_id = 0x7574_0003; + crate::object::js_register_class_parent(class_id, CLASS_ID_ARRAY); + assert!( + is_array_subclass_class_id(class_id), + "the class chain must reach the reserved Array parent id" + ); + let obj = js_object_alloc(class_id, 2); + let hdr = as_array_header(obj); + unsafe { + // Sabotage precondition: the misread is still available. + assert_eq!((*hdr).length, 1); + assert_eq!((*hdr).capacity, class_id); + } + assert!( + raw_receiver_is_heap_object(hdr), + "the pre-filter must admit a GC_TYPE_OBJECT allocation" + ); + let recv = array_object_receiver(hdr).expect("subclass instance must resolve to a receiver"); + assert_eq!( + (recv.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize, + obj as usize, + "the resolved receiver must be the INSTANCE, not a copy" + ); + // And `clean_arr_ptr` still refuses it, so every entry point that does not + // resolve explicitly degrades instead of dereferencing the forged header. + assert!(clean_arr_ptr(hdr).is_null()); +} + +#[test] +fn array_object_receiver_rejects_an_ordinary_class_instance() { + let class_id = 0x7574_0004; + crate::object::js_register_class_parent(class_id, 0x7574_0005); + assert!(!is_array_subclass_class_id(class_id)); + let obj = js_object_alloc(class_id, 2); + assert!( + array_object_receiver(as_array_header(obj)).is_none(), + "a non-Array class instance must keep its ordinary dispatch" + ); +} + +#[test] +fn array_object_receiver_is_safe_for_non_pointers_and_handle_band_ids() { + // Handle-band registry ids (fetch/zlib/proxy) carry no GcHeader; reading + // `id - 8` would fault. They must classify as "not an object receiver". + // Addresses are derived from the `addr_class` band map rather than + // re-typed as literals (the addr-class ratchet's contract). + use crate::value::addr_class; + for id in [ + 0usize, + 1, + addr_class::COMMON_HANDLE_BAND_END, + addr_class::FETCH_HANDLE_BAND_START, + addr_class::ZLIB_HANDLE_BAND_START, + addr_class::PROXY_ID_BAND_START, + addr_class::HANDLE_BAND_MAX - 1, + ] { + let hdr = id as *const ArrayHeader; + assert!(!raw_receiver_is_heap_object(hdr), "id {id:#x}"); + assert!(array_object_receiver(hdr).is_none(), "id {id:#x}"); + } +} diff --git a/crates/perry-runtime/src/object/polymorphic_index.rs b/crates/perry-runtime/src/object/polymorphic_index.rs index 4536b52e37..c2bd178054 100644 --- a/crates/perry-runtime/src/object/polymorphic_index.rs +++ b/crates/perry-runtime/src/object/polymorphic_index.rs @@ -394,6 +394,21 @@ pub extern "C" fn js_object_set_index_polymorphic(obj_handle: i64, idx: f64, val // which handles shape transitions, frozen/sealed/extensible checks, // overflow into out-of-line storage, and accessor descriptors. unsafe { rooted_property_key_set(raw, idx, value) }; + // #7574: a `class X extends Array` instance IS an Array in JavaScript, + // so `sub[3] = v` runs the Array-exotic `[[DefineOwnProperty]]` and + // bumps `length` to 4 (ECMA-262 §10.4.2.1). Perry models the instance + // as a plain object, whose `[[DefineOwnProperty]]` has no such step — + // pre-fix `sub[0] = 10; sub.length` read back `0` (node: `1`), which + // then made the next `sub.push(v)` append at index 0 and overwrite it. + // Ordinary objects and object literals never reach the chain walk: the + // `class_id == 0` test short-circuits first. + let class_id = crate::object::js_object_get_class_id(raw as *const ObjectHeader); + if class_id != 0 && crate::array::is_array_subclass_class_id(class_id) { + if let Some(index) = numeric_key_u32_index(idx) { + let recv = f64::from_bits(crate::value::POINTER_TAG | raw); + crate::array::maintain_array_exotic_length(recv, index); + } + } return; } // Buffer / typed-array were handled above. Map / Set are collection diff --git a/crates/perry-runtime/src/proxy/put_value.rs b/crates/perry-runtime/src/proxy/put_value.rs index 235ee2c98f..ad107aaf38 100644 --- a/crates/perry-runtime/src/proxy/put_value.rs +++ b/crates/perry-runtime/src/proxy/put_value.rs @@ -207,6 +207,20 @@ pub extern "C" fn js_put_value_set( } } if target.to_bits() == receiver.to_bits() && key_is_length(property_key) { + // #7574: `sub.length = n` on a `class X extends Array` instance. + // `array_ptr_from_value` rightly answers `None` (the instance is an + // `ObjectHeader`, not an `ArrayHeader`), so this fell through to the + // ordinary set, which recorded the new `length` but left every + // element at or above it in place — `sub[1]` still read its old + // value after `sub.length = 1`. Run the Array-exotic + // `Set(O, "length", n, true)`, which deletes the truncated indices. + if crate::array::is_array_subclass_value(target) { + crate::array::array_object_set_length( + target, + crate::builtins::js_number_coerce(value), + ); + return value; + } if let Some(arr) = array_ptr_from_value(target) { // PutValue(`arr.length = v`) is `Set(O, "length", v, Throw)`. In // strict mode a frozen array's non-writable `length` makes the @@ -229,7 +243,19 @@ pub extern "C" fn js_put_value_set( let ok = if lookup(target).is_some() { js_proxy_set(target, property_key, value).to_bits() == TAG_TRUE } else { - ordinary_set_with_receiver(target, property_key, value, receiver) + let stored = ordinary_set_with_receiver(target, property_key, value, receiver); + // #7574: `sub[3] = v` on a `class X extends Array` instance is an + // Array-exotic `[[DefineOwnProperty]]` and must leave `length == 4`. + // Perry models the instance as a plain object, so the ordinary set + // above records the index but never touches `length` — pre-fix + // `sub[0] = 10; sub.length` read back `0` (node: `1`), on the + // `T[]`-annotated and the unannotated path alike (both land here + // through `js_put_value_set_ic_miss`). Cheap no-op for every other + // receiver: an object literal short-circuits on `class_id == 0`. + if stored { + crate::array::note_array_subclass_index_write(receiver, property_key); + } + stored }; if !ok && strict != 0 { let key_name = key_to_rust_string(property_key).unwrap_or_else(|| "property".to_string()); diff --git a/crates/perry-runtime/src/typed_feedback.rs b/crates/perry-runtime/src/typed_feedback.rs index 03fec4b0ca..0027df2588 100644 --- a/crates/perry-runtime/src/typed_feedback.rs +++ b/crates/perry-runtime/src/typed_feedback.rs @@ -2478,6 +2478,19 @@ pub extern "C" fn js_typed_feedback_array_index_set_fallback_boxed( key_ptr, value_handle.get_nanbox_f64(), ); + // #7574: this is the arm a `T[]`-annotated binding holding a + // `class X extends Array` instance reaches — the inline and + // out-of-line index-set guards both reject a non- + // `GC_TYPE_ARRAY` receiver and land here. The instance is a + // real Array in JavaScript, so `sub[3] = v` must leave + // `length == 4`; the plain-object store above never touches + // it. Cheap no-op for every other object receiver. + let key_value = + f64::from_bits(crate::value::js_nanbox_string(key_ptr as i64).to_bits()); + crate::array::note_array_subclass_index_write( + f64::from_bits(receiver_handle.get_heap_word_u64()), + key_value, + ); } f64::from_bits(receiver_handle.get_heap_word_u64()) } diff --git a/test-files/test_gap_7574_array_subclass_declared_base_type.ts b/test-files/test_gap_7574_array_subclass_declared_base_type.ts new file mode 100644 index 0000000000..f57f09d5b0 --- /dev/null +++ b/test-files/test_gap_7574_array_subclass_declared_base_type.ts @@ -0,0 +1,184 @@ +// #7574 — `class X extends Array` held in a `T[]`-annotated binding took the +// raw `ArrayHeader` fast paths. An Array-subclass instance is a plain +// `ObjectHeader`, and the two headers overlay field for field, so the element +// slots at +8/+16/+24 are `parent_class_id ‖ field_count`, `keys_array` and +// `meta`. Element writes overwrote two live GC child edges: `a.push(1); +// a.push(2)` SIGSEGVed (exit 139) on the SECOND push, with zero output. +// +// A declared TypeScript type is a hint, never a layout fact, so every binding +// form is affected: `const`, parameter, class field, return type, `as` cast. +// Sibling of #7570 (Map/Set, fixed by #7573). +// +// KNOWN GAP, deliberately not asserted here: `ArraySpeciesCreate` on a subclass +// — node's `sub.map(f)` returns a `MyArr`, perry returns a plain `Array`. That +// is pre-existing and identical on the UNANNOTATED path, so this file compares +// element CONTENT (via `join`) rather than the container's console formatting. + +class MyArr extends Array {} + +class Indirect extends MyArr {} + +class WithCtorAndFields extends Array { + tag = "wcf"; + constructor() { + super(); + } +} + +function useParam(p: number[]): string { + p.push(1); + p.push(2); + p[0] = 9; + return `param len=${p.length} p0=${p[0]} p1=${p[1]}`; +} + +function makeArr(): number[] { + const r: number[] = new MyArr(); + r.push(7); + return r; +} + +class Holder { + items: number[] = new MyArr(); +} + +function seed(n: number): number[] { + const a: number[] = new MyArr(); + for (let i = 0; i < n; i++) { + a.push((i + 1) * 10); + } + return a; +} + +// --------------------------------------------------------------------------- +// 1. The crash repro: two pushes through a base-typed const binding. +// --------------------------------------------------------------------------- +const crashRepro: number[] = new MyArr(); +crashRepro.push(1); +console.log("push1", crashRepro.length); +crashRepro.push(2); +console.log("push2", crashRepro.length, crashRepro[0], crashRepro[1]); + +// --------------------------------------------------------------------------- +// 2. Every binding form. +// --------------------------------------------------------------------------- +const asConst: number[] = new MyArr(); +asConst.push(1); +asConst.push(2); +console.log("const", asConst.length, asConst[0], asConst[1]); + +console.log(useParam(new MyArr())); + +const fromReturn = makeArr(); +fromReturn.push(8); +console.log("return", fromReturn.length, fromReturn[0], fromReturn[1]); + +const holder = new Holder(); +holder.items.push(5); +holder.items.push(6); +console.log("field", holder.items.length, holder.items[0], holder.items[1]); + +const asCast = new MyArr() as number[]; +asCast.push(3); +console.log("cast", asCast.length, asCast[0]); + +const indirect: number[] = new Indirect(); +indirect.push(4); +console.log("indirect", indirect.length, indirect[0]); + +const withFields: number[] = new WithCtorAndFields(); +withFields.push(11); +console.log("ctor+fields", withFields.length, withFields[0]); + +// --------------------------------------------------------------------------- +// 3. Element get / set through the annotated binding. +// --------------------------------------------------------------------------- +const idx: number[] = new MyArr(); +idx[0] = 10; +console.log("set0", idx.length, idx[0]); +idx[1] = 20; +console.log("set1", idx.length, idx[1]); +idx[0] = 99; +console.log("overwrite", idx.length, idx[0], idx[1]); +console.log("oob", idx[7]); + +// --------------------------------------------------------------------------- +// 4. `.length` READ and WRITE. +// --------------------------------------------------------------------------- +const lenRW: number[] = seed(3); +console.log("len read", lenRW.length); +lenRW.length = 1; +console.log("len write", lenRW.length, lenRW[0], lenRW[1], lenRW[2]); +lenRW.length = 0; +console.log("len zero", lenRW.length, lenRW[0]); + +// --------------------------------------------------------------------------- +// 5. push / pop / shift. +// --------------------------------------------------------------------------- +const mut: number[] = seed(3); +console.log("pop", mut.pop(), mut.length); +console.log("shift", mut.shift(), mut.length); +mut.push(77); +console.log("push back", mut.length, mut[0], mut[1]); + +// --------------------------------------------------------------------------- +// 6. The bounded-index loop tier (hoisted `arr.length` + raw slot load). +// --------------------------------------------------------------------------- +const looped: number[] = seed(4); +let boundedSum = 0; +for (let i = 0; i < looped.length; i++) { + boundedSum += looped[i]; +} +console.log("bounded sum", boundedSum); + +// --------------------------------------------------------------------------- +// 7. Iteration + spread. +// --------------------------------------------------------------------------- +const iterated: number[] = seed(3); +const forOf: number[] = []; +for (const v of iterated) { + forOf.push(v); +} +console.log("for-of", forOf.join(",")); +console.log("spread", [...iterated].join(",")); +console.log("Array.from", Array.from(iterated).join(",")); +const [d0, d1] = iterated; +console.log("destructure", d0, d1); + +// --------------------------------------------------------------------------- +// 8. map / filter / forEach receiver identity / join / slice / indexOf. +// --------------------------------------------------------------------------- +const funcs: number[] = seed(3); +console.log("map", funcs.map((v) => v * 2).join(",")); +console.log("filter", funcs.filter((v) => v > 10).join(",")); +console.log("slice", funcs.slice(1).join(",")); +console.log("join", funcs.join("-")); +console.log("indexOf", funcs.indexOf(20), funcs.indexOf(999)); +console.log("includes", funcs.includes(30), funcs.includes(999)); +console.log("reduce", funcs.reduce((a, b) => a + b, 0)); +funcs.forEach(function (v, i, self) { + console.log("forEach", i, v, self === funcs, self.length); +}); + +// --------------------------------------------------------------------------- +// 9. Controls — a REAL array in the same binding forms must be untouched. +// --------------------------------------------------------------------------- +const realArr: number[] = []; +realArr.push(1); +realArr.push(2); +realArr[0] = 5; +realArr.length = 1; +console.log("real", realArr.length, realArr[0], Array.isArray(realArr)); +const realLoop: number[] = [1, 2, 3, 4]; +let realSum = 0; +for (let i = 0; i < realLoop.length; i++) { + realSum += realLoop[i]; +} +console.log("real bounded", realSum, realLoop.map((v) => v * 2).join(",")); + +// A plain object merely ANNOTATED as an array must degrade, never crash. +const lying = { length: 0 } as unknown as number[]; +console.log("lying", lying.length, lying[0]); + +console.log("isArray", Array.isArray(asConst), Array.isArray(realArr)); +console.log("done"); From 008e65d2e5e205b51be3dc740796d91396c9fdd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 8 Aug 2026 00:23:40 +0200 Subject: [PATCH 2/2] chore(version): bump to 0.5.1343 --- 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 413ad68af3..a423873ca4 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.1342 +**Current Version:** 0.5.1343 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index aaf2ae27db..608605655d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1342" +version = "0.5.1343" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1342" +version = "0.5.1343" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1342" +version = "0.5.1343" [[package]] name = "perry-ui-tvos" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1342" +version = "0.5.1343" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index d285e5ca38..b1bf381a5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1342" +version = "0.5.1343" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"