diff --git a/crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs b/crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs index 7211892e12..8968594c52 100644 --- a/crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs +++ b/crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs @@ -438,9 +438,90 @@ pub(super) fn lower_inline_dyn_typed_array_get( ctx.block() .cond_br(&header_ok, &object_brand_label, &object_miss_label); + // An elements-backed Array-subclass instance (`ObjectMeta.elements`, + // perry-runtime `array/subclass_elements.rs`): its indexed elements live + // in a real Array hanging off the meta record, so the read is the plain + // Array read on that inner array — no shape IC, no family token. A miss + // of this probe (no meta, no store) is the shape-carried form and keeps + // the IC below; an out-of-bounds index or a hole goes to the complete + // dispatcher (prototype chain). + let elem_meta_idx = ctx.new_block("arrlike.elem.meta"); + let elem_store_idx = ctx.new_block("arrlike.elem.store"); + let elem_bounds_idx = ctx.new_block("arrlike.elem.bounds"); + let elem_load_idx = ctx.new_block("arrlike.elem.load"); + let elem_value_idx = ctx.new_block("arrlike.elem.value"); + let elem_meta_label = ctx.block_label(elem_meta_idx); + let elem_store_label = ctx.block_label(elem_store_idx); + let elem_bounds_label = ctx.block_label(elem_bounds_idx); + let elem_load_label = ctx.block_label(elem_load_idx); + let elem_value_label = ctx.block_label(elem_value_idx); ctx.current_block = object_brand_idx; ctx.block() - .cond_br(&is_array, &object_array_guard_label, &object_shape_label); + .cond_br(&is_array, &object_array_guard_label, &elem_meta_label); + ctx.current_block = elem_meta_idx; + let elem_meta_addr = ctx.block().add(I64, &object_raw, &meta_offset); + let elem_meta_slot_ptr = ctx.block().inttoptr(I64, &elem_meta_addr); + let elem_meta_loaded = ctx.block().load( + if meta_ptr_size == 4 { I32 } else { I64 }, + &elem_meta_slot_ptr, + ); + let elem_meta_i64 = if meta_ptr_size == 4 { + ctx.block().zext(I32, &elem_meta_loaded, I64) + } else { + elem_meta_loaded + }; + let elem_has_meta = ctx.block().icmp_ne(I64, &elem_meta_i64, "0"); + ctx.block() + .cond_br(&elem_has_meta, &elem_store_label, &object_shape_label); + ctx.current_block = elem_store_idx; + let elem_meta_ptr = ctx.block().inttoptr(I64, &elem_meta_i64); + // `ObjectMeta.elements` is word 12 (offset 96; pinned by a const assert + // in perry-runtime `object/mod.rs`). + let elem_store_slot_ptr = ctx.block().gep(I64, &elem_meta_ptr, &[(I64, "12")]); + let elem_store_i64 = ctx.block().load(I64, &elem_store_slot_ptr); + let elem_has_store = ctx.block().icmp_ne(I64, &elem_store_i64, "0"); + ctx.block() + .cond_br(&elem_has_store, &elem_bounds_label, &object_shape_label); + ctx.current_block = elem_bounds_idx; + let elem_type_addr = ctx.block().sub(I64, &elem_store_i64, "8"); + let elem_type_ptr = ctx.block().inttoptr(I64, &elem_type_addr); + let elem_type = ctx.block().load(I8, &elem_type_ptr); + let elem_is_array = ctx.block().icmp_eq(I8, &elem_type, "1"); + let elem_flags_addr = ctx.block().sub(I64, &elem_store_i64, "7"); + let elem_flags_ptr = ctx.block().inttoptr(I64, &elem_flags_addr); + let elem_flags = ctx.block().load(I8, &elem_flags_ptr); + let elem_fwd = ctx.block().and(I8, &elem_flags, "128"); + let elem_not_fwd = ctx.block().icmp_eq(I8, &elem_fwd, "0"); + let elem_store_ptr = ctx.block().inttoptr(I64, &elem_store_i64); + let elem_length = ctx.block().load(I32, &elem_store_ptr); + let elem_length_i64 = ctx.block().zext(I32, &elem_length, I64); + let elem_in_bounds = ctx.block().icmp_ult(I64, &object_idx_i64, &elem_length_i64); + let elem_ok = ctx.block().and(I1, &elem_is_array, &elem_not_fwd); + let elem_ok = ctx.block().and(I1, &elem_ok, &elem_in_bounds); + ctx.block() + .cond_br(&elem_ok, &elem_load_label, &object_miss_label); + ctx.current_block = elem_load_idx; + let elem_bytes = ctx.block().shl(I64, &object_idx_i64, "3"); + let elem_elements_addr = ctx.block().add(I64, &elem_store_i64, "8"); + let elem_addr = ctx.block().add(I64, &elem_elements_addr, &elem_bytes); + let elem_ptr = ctx.block().inttoptr(I64, &elem_addr); + let elem_raw = ctx.block().load(DOUBLE, &elem_ptr); + let elem_bits = ctx.block().bitcast_double_to_i64(&elem_raw); + let elem_is_hole = ctx + .block() + .icmp_eq(I64, &elem_bits, crate::nanbox::TAG_HOLE_I64); + ctx.block() + .cond_br(&elem_is_hole, &object_miss_label, &elem_value_label); + ctx.current_block = elem_value_idx; + let elem_value = if coerce_slow_to_number { + ctx.block() + .call(DOUBLE, "js_number_coerce", &[(DOUBLE, &elem_raw)]) + } else { + elem_raw + }; + let elem_end_label = ctx.block().label.clone(); + ctx.block().br(&merge_label); + kind_incoming.push((elem_value, elem_end_label)); // Ordinary Array: the receiver tag and forwarding state were checked in // the predecessor. Reject descriptors or any process-wide prototype diff --git a/crates/perry-codegen/src/expr/index_get_claim_tests.rs b/crates/perry-codegen/src/expr/index_get_claim_tests.rs index 09e28734bf..6b0faf81d4 100644 --- a/crates/perry-codegen/src/expr/index_get_claim_tests.rs +++ b/crates/perry-codegen/src/expr/index_get_claim_tests.rs @@ -409,6 +409,18 @@ fn any_typed_dynamic_key_takes_the_numeric_tiers_when_it_is_an_array_index() { ir.contains("tav.get.brand") && ir.contains("arrlike.ic.family_token"), "an integer key must reach the inline typed-array and dense-subclass tiers:\n{ir}" ); + // The elements-backed subclass probe sits ahead of the shape IC: meta + // word → `ObjectMeta.elements` (word 12) → inner-array bounds → slot. + let store = super::class_field_barrier_tests::block_body(&ir, "arrlike.elem.store.") + .expect("the elements-store probe block exists"); + assert!( + store.contains("getelementptr i64, ptr %") && store.contains(", i64 12"), + "the probe must load ObjectMeta.elements at word 12:\n{store}" + ); + assert!( + ir.contains("arrlike.elem.bounds") && ir.contains("arrlike.elem.load"), + "the probe must bounds-check and load from the inner array:\n{ir}" + ); assert!( ir.contains("call double @js_array_get_index_or_string("), "non-index keys must keep the complete key route:\n{ir}" diff --git a/crates/perry-codegen/src/expr/property_get/composed_ics.rs b/crates/perry-codegen/src/expr/property_get/composed_ics.rs index 4ae3f0d7a1..041ab6f050 100644 --- a/crates/perry-codegen/src/expr/property_get/composed_ics.rs +++ b/crates/perry-codegen/src/expr/property_get/composed_ics.rs @@ -223,7 +223,54 @@ pub(super) fn emit_array_subclass_length_ic( let forwarded = ctx.block().and(I8, &gc_flags, "128"); let not_forwarded = ctx.block().icmp_eq(I8, &forwarded, "0"); let header_ok = ctx.block().and(I1, &is_object, ¬_forwarded); - ctx.block().cond_br(&header_ok, &shape_label, &miss_label); + let meta_ptr_size: u64 = if crate::target_layout::target_is_ilp32(ctx.target_triple) { + 4 + } else { + 8 + }; + let meta_offset = (crate::target_layout::object_header_size_bytes(ctx.target_triple) + - meta_ptr_size) + .to_string(); + // An elements-backed Array-subclass instance (`ObjectMeta.elements`): + // `length` is the inner Array's length word — no shape IC. A probe miss + // (no meta, no store) is the shape-carried form and keeps the IC below. + let elem_meta_idx = ctx.new_block("plen.elem.meta"); + let elem_store_idx = ctx.new_block("plen.elem.store"); + let elem_length_idx = ctx.new_block("plen.elem.length"); + let elem_meta_label = ctx.block_label(elem_meta_idx); + let elem_store_label = ctx.block_label(elem_store_idx); + let elem_length_label = ctx.block_label(elem_length_idx); + ctx.block() + .cond_br(&header_ok, &elem_meta_label, &miss_label); + ctx.current_block = elem_meta_idx; + let elem_meta_addr = ctx.block().add(I64, recv_handle, &meta_offset); + let elem_meta_slot_ptr = ctx.block().inttoptr(I64, &elem_meta_addr); + let elem_meta_loaded = ctx.block().load( + if meta_ptr_size == 4 { I32 } else { I64 }, + &elem_meta_slot_ptr, + ); + let elem_meta_i64 = if meta_ptr_size == 4 { + ctx.block().zext(I32, &elem_meta_loaded, I64) + } else { + elem_meta_loaded + }; + let elem_has_meta = ctx.block().icmp_ne(I64, &elem_meta_i64, "0"); + ctx.block() + .cond_br(&elem_has_meta, &elem_store_label, &shape_label); + ctx.current_block = elem_store_idx; + let elem_meta_ptr = ctx.block().inttoptr(I64, &elem_meta_i64); + // `ObjectMeta.elements` is word 12 (offset 96; const-asserted in the runtime). + let elem_store_slot_ptr = ctx.block().gep(I64, &elem_meta_ptr, &[(I64, "12")]); + let elem_store_i64 = ctx.block().load(I64, &elem_store_slot_ptr); + let elem_has_store = ctx.block().icmp_ne(I64, &elem_store_i64, "0"); + ctx.block() + .cond_br(&elem_has_store, &elem_length_label, &shape_label); + ctx.current_block = elem_length_idx; + let elem_store_ptr = ctx.block().inttoptr(I64, &elem_store_i64); + let elem_length_i32 = ctx.block().load(I32, &elem_store_ptr); + let elem_length = ctx.block().uitofp(I32, &elem_length_i32, DOUBLE); + let elem_end = ctx.block().label.clone(); + ctx.block().br(&merge_label); ctx.current_block = shape_idx; let object_ptr = ctx.block().inttoptr(I64, recv_handle); @@ -252,15 +299,6 @@ pub(super) fn emit_array_subclass_length_ic( let exact_match = ctx.block().icmp_eq(I64, &live_key, &cached_key); ctx.block().cond_br(&exact_match, &slot_label, &miss_label); - let meta_ptr_size: u64 = if crate::target_layout::target_is_ilp32(ctx.target_triple) { - 4 - } else { - 8 - }; - let meta_offset = (crate::target_layout::object_header_size_bytes(ctx.target_triple) - - meta_ptr_size) - .to_string(); - ctx.current_block = family_meta_idx; let family_meta_addr = ctx.block().add(I64, recv_handle, &meta_offset); let family_meta_slot_ptr = ctx.block().inttoptr(I64, &family_meta_addr); @@ -358,6 +396,7 @@ pub(super) fn emit_array_subclass_length_ic( let length = ctx.block().phi( DOUBLE, &[ + (&elem_length, &elem_end), (&inline_length, &inline_end), (&spilled_length, &spill_end), (&miss_length, &miss_end), diff --git a/crates/perry-codegen/src/expr/property_get/tests.rs b/crates/perry-codegen/src/expr/property_get/tests.rs index 7c633bc593..03e8e6f41d 100644 --- a/crates/perry-codegen/src/expr/property_get/tests.rs +++ b/crates/perry-codegen/src/expr/property_get/tests.rs @@ -860,3 +860,26 @@ fn generic_non_size_read_has_no_collection_layout_load() { "only `.size` may grow the native collection fast path:\n{ir}" ); } + +/// The object-backed `.length` tier probes the elements-backed subclass store +/// first: meta word → `ObjectMeta.elements` (word 12) → the inner Array's +/// `length` word — and only then the shape/family IC. +#[test] +fn the_length_tier_probes_the_elements_store_before_the_shape_ic() { + let ir = emit_guarded_length_read(); + assert!( + ir.contains("plen.elem.meta") && ir.contains("plen.elem.length"), + "the elements probe must exist:\n{ir}" + ); + let store = super::super::class_field_barrier_tests::block_body(&ir, "plen.elem.store.") + .expect("the elements-store probe block exists"); + assert!( + store.contains("getelementptr i64, ptr %") && store.contains(", i64 12"), + "the probe must load ObjectMeta.elements at word 12:\n{store}" + ); + // A miss of the probe keeps the shape IC. + assert!( + store.contains("plen.ic.shape"), + "a missing store must fall through to the shape IC:\n{store}" + ); +} diff --git a/crates/perry-codegen/src/expr/this_super_call.rs b/crates/perry-codegen/src/expr/this_super_call.rs index 48fdc8bdd9..51be11827a 100644 --- a/crates/perry-codegen/src/expr/this_super_call.rs +++ b/crates/perry-codegen/src/expr/this_super_call.rs @@ -264,6 +264,44 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { .filter(|class| class.extends_expr.is_none() && !class.heritage_lexically_shadowed) .and_then(|class| class.extends_name.clone()) .filter(|parent| !ctx.classes.contains_key(parent.as_str())); + // `class X extends Array { constructor(...a) { super(...a) } }`: + // the Array parent has no registered constructor, so the spread + // form must run the same subclass init the direct `super(n)` + // form does (`lower_array_super_init`), handing it the + // materialized argument array's elements. Without this the + // instance had no `length` and no Array surface at all. + // Resolved the way the direct `super(n)` arm resolves its parent + // (`extends_name`, a lexically shadowed heritage excluded): the + // heritage of `class X extends Array` also carries `extends_expr`, + // which the `async_parent` filter above rejects. + let array_parent = ctx + .classes + .get(¤t_class_name) + .filter(|class| !class.heritage_lexically_shadowed) + .and_then(|class| class.extends_name.as_deref()) + .is_some_and(|parent| parent == "Array" && !ctx.classes.contains_key("Array")); + if array_parent { + let len_i32 = ctx.block().call(I32, "js_array_length", &[(I64, &arr)]); + let len = ctx.block().zext(I32, &len_i32, I64); + let elems_addr = ctx.block().add(I64, &arr, "8"); + let elems_ptr = ctx.block().inttoptr(I64, &elems_addr); + let result = ctx.block().call( + DOUBLE, + "js_array_subclass_init_args", + &[ + (DOUBLE, &this_box), + (crate::types::PTR, &elems_ptr), + (I64, &len), + ], + ); + bind_derived_this_after_super(ctx); + crate::lower_call::apply_field_initializers_recursive( + ctx, + ¤t_class_name, + crate::lower_call::FieldInitMode::SelfOnly, + )?; + return Ok(result); + } if matches!( async_parent.as_deref(), Some("EventEmitterAsyncResource" | "AsyncLocalStorage" | "AsyncResource") diff --git a/crates/perry-runtime/src/array/mod.rs b/crates/perry-runtime/src/array/mod.rs index 2dcc14c09c..4bfada28f3 100644 --- a/crates/perry-runtime/src/array/mod.rs +++ b/crates/perry-runtime/src/array/mod.rs @@ -29,6 +29,7 @@ mod sort; mod species; mod splice_slice; mod subclass; +pub(crate) mod subclass_elements; #[cfg(test)] mod collection_tag_tests; @@ -41,6 +42,8 @@ mod spread_dense_tests; #[cfg(test)] mod strict_store_tests; #[cfg(test)] +mod subclass_elements_tests; +#[cfg(test)] mod subclass_tests; #[cfg(test)] mod tests; @@ -184,11 +187,12 @@ pub(crate) use indexing_support::test_swap_array_index_fast_path_invalidated; // 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, array_subclass_fast_index_get, array_subclass_fast_length, - array_subclass_fast_length_with_ic, array_subclass_named_prefix_token_for_slot, - array_subclass_tail_descriptors_are_plain, clear_array_subclass_named_prefix_token, - clear_packed_subclass_numeric_proof, is_array_subclass_class_id, is_array_subclass_value, - note_array_subclass_index_write, note_packed_subclass_spill_store, + array_object_set_length, array_subclass_fast_index_get, array_subclass_fast_index_set, + array_subclass_fast_length, array_subclass_fast_length_with_ic, + array_subclass_named_prefix_token_for_slot, array_subclass_tail_descriptors_are_plain, + clear_array_subclass_named_prefix_token, clear_packed_subclass_numeric_proof, + is_array_subclass_class_id, is_array_subclass_value, note_array_subclass_index_write, + note_packed_subclass_spill_store, }; // Issue #1572 — flatten helpers reused by `node_stream::ns_iter_flat_map` // so an `async function*` mapper return is driven through the iterator diff --git a/crates/perry-runtime/src/array/subclass.rs b/crates/perry-runtime/src/array/subclass.rs index e10c46ad33..094f174442 100644 --- a/crates/perry-runtime/src/array/subclass.rs +++ b/crates/perry-runtime/src/array/subclass.rs @@ -15,7 +15,7 @@ use crate::object::ObjectHeader; use crate::value::JSValue; #[path = "subclass_loop_guard.rs"] -mod loop_guard; +pub(super) mod loop_guard; // The loop-guard entry points are exported C symbols; only the unit tests // reach them through Rust paths. #[cfg(test)] @@ -96,9 +96,9 @@ struct DenseSubclassLayout { /// Array-subclass mutation reuse that single header read for brand, layout, /// and frozen/sealed/no-extend checks. #[derive(Clone, Copy)] -struct ValidatedObjectReceiver { - object: *const ObjectHeader, - object_flags: u16, +pub(super) struct ValidatedObjectReceiver { + pub(super) object: *const ObjectHeader, + pub(super) object_flags: u16, } /// Read the per-instance prototype-divergence bit after the caller has already @@ -439,23 +439,43 @@ pub(crate) unsafe fn array_subclass_named_prefix_token_for_slot( if declared_keys.is_null() { return 0; } - let shape_id = (*obj).parent_class_id; - let cache_key = dense_cache_key(class_id, shape_id); - let layout = cached_dense_layout(cache_key).or_else(|| { - let layout = build_dense_layout(obj)?; - publish_dense_layout(cache_key, layout); - Some(layout) - }); - let Some(layout) = layout else { - return 0; + // An elements-backed instance (`super::subclass_elements`) has NO numeric + // keys and no `length` property in its shape, so `build_dense_layout` + // (which locates `length` by name) cannot describe it — and without a + // token the descriptor-bearing IC-miss path below never primes the PIC, + // leaving every declared-field read to miss forever (measured: 17.8M + // misses on `change`/`sset`/`mask` in a 2 s wolf-ecs run). Its named + // prefix is simply the WHOLE shape: the strongest form of the same + // proof, validated by the identical declared-prefix comparison below. + let elements_backed = !super::subclass_elements::elements_of(obj).is_null(); + let (element_base, dense_prefix_len, length_slot) = if elements_backed { + (u32::MAX, 0, u32::MAX) + } else { + let shape_id = (*obj).parent_class_id; + let cache_key = dense_cache_key(class_id, shape_id); + let layout = cached_dense_layout(cache_key).or_else(|| { + let layout = build_dense_layout(obj)?; + publish_dense_layout(cache_key, layout); + Some(layout) + }); + let Some(layout) = layout else { + return 0; + }; + ( + layout.element_base, + layout.dense_prefix_len, + layout.length_slot, + ) }; // Descriptor-bearing Array subclasses cannot use the ordinary exact-shape // raw-load PIC even while empty: their unrelated `length` descriptor sends // them through the descriptor arm. Admit the fully validated named prefix // before the first numeric key exists as well. `element_base` is the first // prospective numeric slot and `dense_prefix_len == 0` proves there is no - // tail yet; the complete-prefix equality below remains the authority. - if requested_slot >= layout.element_base as usize { + // tail yet; the complete-prefix equality below remains the authority. An + // elements-backed instance has no numeric slot at all (`u32::MAX`), so + // only the declared-prefix bound below applies to it. + if requested_slot >= element_base as usize { return 0; } let cached = (*meta).array_subclass_named_prefix_token; @@ -475,11 +495,17 @@ pub(crate) unsafe fn array_subclass_named_prefix_token_for_slot( crate::object::keys_array_dense_slots(declared_keys as *const ArrayHeader); let current_count = (shape.logical_key_count as usize).min(current_physical_len); let declared_count = (declared_count as usize).min(declared_physical_len); - if current_slots.is_null() - || declared_slots.is_null() - || declared_count > current_count - || layout.element_base as usize + layout.dense_prefix_len as usize != current_count - { + if current_slots.is_null() || declared_slots.is_null() || declared_count > current_count { + return 0; + } + // Every key is either in the named prefix or in the numeric tail. An + // elements-backed instance has no tail, so the requested slot must simply + // be a declared one; the shape-carried form keeps the exact partition. + if elements_backed { + if requested_slot >= declared_count { + return 0; + } + } else if element_base as usize + dense_prefix_len as usize != current_count { return 0; } @@ -523,25 +549,32 @@ pub(crate) unsafe fn array_subclass_named_prefix_token_for_slot( // existing slot; otherwise the exact missing names must follow the // declared prefix in that order. Anything else is instance-specific. let declared_count = declared_count as u32; - let expected_length_slot = if let Some(slot) = declared_length_slot { - slot - } else { - declared_count - }; - if layout.length_slot != expected_length_slot { - return 0; - } let mut expected_runtime_names: [&[u8]; 2] = [&[]; 2]; let mut expected_runtime_count = 0usize; - if declared_length_slot.is_none() { - expected_runtime_names[expected_runtime_count] = b"length"; - expected_runtime_count += 1; + if !elements_backed { + // The shape-carried form carries `length` as an own property, at the + // declared slot when the class declared that name and appended + // otherwise. + let expected_length_slot = declared_length_slot.unwrap_or(declared_count); + if length_slot != expected_length_slot { + return 0; + } + if declared_length_slot.is_none() { + expected_runtime_names[expected_runtime_count] = b"length"; + expected_runtime_count += 1; + } + } else if declared_length_slot.is_some() { + // A class that declares its own `length` field is not modelled by the + // elements store (the store owns `length`); keep it off this token. + return 0; } if !declared_fill { expected_runtime_names[expected_runtime_count] = b"fill"; expected_runtime_count += 1; } - if layout.element_base != declared_count.saturating_add(expected_runtime_count as u32) { + if !elements_backed + && element_base != declared_count.saturating_add(expected_runtime_count as u32) + { return 0; } for (offset, expected) in expected_runtime_names[..expected_runtime_count] @@ -855,6 +888,11 @@ fn nonnegative_u32_length(value: JSValue) -> Option { /// semantics; descriptor/prototype-divergent shapes decline above. #[inline] pub(crate) fn array_subclass_fast_length(value: f64) -> Option { + if let Some(elements) = validated_object_receiver_for_value(value) + .and_then(|r| super::subclass_elements::elements_for_validated(&r)) + { + return Some(f64::from(unsafe { (*elements).length })); + } let (obj, layout) = dense_layout_for_value(value)?; Some(f64::from_bits(layout_length_value(obj, layout).bits())) } @@ -868,6 +906,13 @@ pub(crate) fn array_subclass_fast_length(value: f64) -> Option { /// into the cache, so moving GC needs neither a root nor a rewrite hook. #[inline] pub(crate) fn array_subclass_fast_length_with_ic(value: f64, cache: *mut u64) -> Option { + if let Some(elements) = validated_object_receiver_for_value(value) + .and_then(|r| super::subclass_elements::elements_for_validated(&r)) + { + // No shape layout to publish: the IC words describe inline slots, + // and an elements-backed receiver has none for `length`. + return Some(f64::from(unsafe { (*elements).length })); + } let (obj, layout) = dense_layout_for_value(value)?; let result = f64::from_bits(layout_length_value(obj, layout).bits()); if !cache.is_null() { @@ -893,6 +938,9 @@ pub(crate) fn array_subclass_fast_length_with_ic(value: f64, cache: *mut u64) -> pub(crate) fn array_subclass_fast_length_raw(arr: *const ArrayHeader) -> Option { let raw = (arr as u64 & crate::value::POINTER_MASK) as usize; let receiver = validated_object_receiver(raw)?; + if let Some(elements) = super::subclass_elements::elements_for_validated(&receiver) { + return Some(f64::from(unsafe { (*elements).length })); + } let layout = dense_layout_for_validated_object(receiver.object)?; Some(f64::from_bits( layout_length_value(receiver.object, layout).bits(), @@ -904,6 +952,11 @@ pub(crate) fn array_subclass_fast_length_raw(arr: *const ArrayHeader) -> Option< /// proof when a length-only grow created holes without changing the shape. #[inline] pub(crate) fn array_subclass_fast_index_get(value: f64, index: u32) -> Option { + if let Some(elements) = validated_object_receiver_for_value(value) + .and_then(|r| super::subclass_elements::elements_for_validated(&r)) + { + return super::subclass_elements::elements_index_get(elements, index); + } let (obj, layout) = dense_layout_for_value(value)?; dense_index_get_with_layout(obj, layout, index) } @@ -915,6 +968,9 @@ pub(crate) fn array_subclass_fast_index_get_raw( ) -> Option { let raw = (arr as u64 & crate::value::POINTER_MASK) as usize; let receiver = validated_object_receiver(raw)?; + if let Some(elements) = super::subclass_elements::elements_for_validated(&receiver) { + return super::subclass_elements::elements_index_get(elements, index); + } let layout = dense_layout_for_validated_object(receiver.object)?; dense_index_get_with_layout(receiver.object, layout, index) } @@ -1108,7 +1164,7 @@ pub(crate) fn array_subclass_tail_descriptors_are_plain( } #[inline(always)] -fn mutation_receiver_allows_plain_tail(object_flags: u16) -> bool { +pub(super) fn mutation_receiver_allows_plain_tail(object_flags: u16) -> bool { object_flags & (crate::gc::OBJ_FLAG_FROZEN | crate::gc::OBJ_FLAG_SEALED | crate::gc::OBJ_FLAG_NO_EXTEND) == 0 @@ -1146,6 +1202,9 @@ fn array_subclass_fast_index_set_validated( index: u32, value: f64, ) -> bool { + if let Some(done) = super::subclass_elements::elements_index_set(&receiver, index, value) { + return done; + } let obj = receiver.object; let Some(layout) = dense_layout_for_validated_object(obj) else { return false; @@ -1226,6 +1285,9 @@ fn array_subclass_fast_push_one_validated( value: f64, proven_u31: Option, ) -> Option { + if super::subclass_elements::elements_for_validated(&receiver).is_some() { + return super::subclass_elements::elements_push(&receiver, value); + } let obj = receiver.object; let layout = dense_layout_for_validated_object(obj)?; let length = nonnegative_u32_length(layout_length_value(obj, layout))?; @@ -1351,6 +1413,9 @@ pub(crate) fn array_subclass_fast_pop_raw(arr: *const ArrayHeader) -> Option Option { + if super::subclass_elements::elements_for_validated(&receiver).is_some() { + return super::subclass_elements::elements_pop(&receiver); + } let obj = receiver.object; let layout = dense_layout_for_validated_object(obj)?; let length = nonnegative_u32_length(layout_length_value(obj, layout))?; @@ -1495,7 +1560,17 @@ pub extern "C" fn js_packed_arraylike_index_get(receiver: f64, index: f64, cache && header.gc_flags & crate::gc::GC_FLAG_FORWARDED == 0 { let obj = raw.cast::(); - if let Some(layout) = dense_layout_for_validated_object(obj) { + // Elements-backed instance: an in-bounds non-hole element + // answers directly; a hole continues to the complete + // dispatcher (prototype chain). + let elements = unsafe { super::subclass_elements::elements_of(obj) }; + if !elements.is_null() { + if let Some(value) = + super::subclass_elements::elements_index_get(elements, index_u32) + { + return value; + } + } else if let Some(layout) = dense_layout_for_validated_object(obj) { // The codegen hit path handles both inline and // object-owned spill slots. In spill mode, publish a // class-wide dense-tail identity when the owner has @@ -1601,6 +1676,11 @@ pub fn is_array_subclass_instance(object: f64) -> bool { /// therefore yields `undefined` rather than a preserved hole — an accepted /// limitation for this rare case. pub fn array_subclass_dense_snapshot(recv: f64) -> f64 { + // An elements-backed instance iterates its live inner array, exactly as + // a plain Array does (no snapshot: a live length, live holes). + if let Some((_, elements)) = crate::array::subclass_elements::backed_value(recv) { + return crate::value::js_nanbox_pointer(elements as i64); + } let len = al_length(recv).max(0); // ArrayCreate throws a RangeError for len ≥ 2^32 (matching `js_arraylike_map`) // — and, critically, this guard prevents the `as u32` truncation below from @@ -1754,6 +1834,21 @@ pub(crate) fn array_object_method(recv: f64, method: &str, args: &[f64]) -> Opti if let Some(value) = array_subclass_fast_pop(recv) { return Some(value); } + } else if method == "fill" { + // `Array.prototype.fill` over the receiver's own `length` + indexed + // properties. An elements-backed instance has no own `fill` method + // (see `js_array_subclass_init`), so this funnel is where the + // inherited one is served. + return Some(crate::array::js_array_fill_generic( + recv, + args.first() + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)), + i32::from(args.len() > 1), + args.get(1).copied().unwrap_or(0.0), + i32::from(args.len() > 2), + args.get(2).copied().unwrap_or(0.0), + )); } let (ptr, len) = (args.as_ptr(), args.len()); if let Some(result) = super::generic::run_object_mutator(recv, method, ptr, len) { @@ -1814,6 +1909,11 @@ pub(crate) fn array_object_index_set(recv: f64, index: u32, value: f64) { /// 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) { + // An elements-backed instance's `length` is the inner array's: the index + // store already maintained it, and no numeric proof lives on the shape. + if crate::array::subclass_elements::backed_value(recv).is_some() { + return; + } // Stringifying a numeric key can allocate and evacuate the object. Keep // both inputs live, then re-read the receiver before retiring its proof. let scope = crate::gc::RuntimeHandleScope::new(); @@ -1848,6 +1948,9 @@ pub(crate) fn note_array_subclass_index_write(recv: f64, key: f64) { /// generic OBJECT index-store funnels can apply it without re-entering the /// store. pub(crate) fn maintain_array_exotic_length(recv: f64, index: u32) { + if crate::array::subclass_elements::backed_value(recv).is_some() { + return; + } let current = al_length(recv); if (index as i64) < current { return; @@ -1870,6 +1973,10 @@ pub(crate) fn maintain_array_exotic_length(recv: f64, index: u32) { #[cold] #[inline(never)] pub(crate) fn array_object_set_length(recv: f64, new_length: f64) { + if let Some((obj, elements)) = crate::array::subclass_elements::backed_value(recv) { + unsafe { crate::array::subclass_elements::set_length(obj, elements, new_length) }; + return; + } if !new_length.is_finite() || new_length < 0.0 || new_length.trunc() != new_length { crate::array::array_length_range_error(); } diff --git a/crates/perry-runtime/src/array/subclass_elements.rs b/crates/perry-runtime/src/array/subclass_elements.rs new file mode 100644 index 0000000000..2b68960cd8 --- /dev/null +++ b/crates/perry-runtime/src/array/subclass_elements.rs @@ -0,0 +1,638 @@ +//! Elements backing store for `class X extends Array` instances. +//! +//! An Array-subclass instance is an ordinary `GC_TYPE_OBJECT` (`super::subclass`); +//! today its indexed elements and `length` are shape-carried properties, so every +//! `push`/`pop`/`obj[i] = v` is a property-shape transition. Under +//! [`array_subclass_elements_enabled`] the instance instead owns a real +//! `GC_TYPE_ARRAY` in `ObjectMeta.elements` (a traced child edge exactly like +//! `spill`) holding its indexed elements and `length`, and the property entry +//! points route canonical array-index keys and `length` to it. +//! +//! This module owns the edge: the gate, the accessor, installation at +//! construction, and the barriered head write-back after a re-allocating append. +use crate::array::ArrayHeader; +use crate::object::ObjectHeader; + +use super::subclass::{mutation_receiver_allows_plain_tail, ValidatedObjectReceiver}; + +/// `PERRY_ARRAY_SUBCLASS_ELEMENTS=1|on|true` — off while the property entry +/// points are being routed; the default flips once the semantics suite is green. +#[inline] +pub(crate) fn array_subclass_elements_enabled() -> bool { + static ON: std::sync::OnceLock = std::sync::OnceLock::new(); + *ON.get_or_init(|| { + matches!( + std::env::var("PERRY_ARRAY_SUBCLASS_ELEMENTS").as_deref(), + Ok("1") | Ok("on") | Ok("true") + ) + }) +} + +/// The elements store of a live `GC_TYPE_OBJECT`, or null when it has none +/// (no meta record, or not an elements-backed Array subclass instance). +/// +/// # Safety +/// `obj` must be a live `GC_TYPE_OBJECT` user pointer. +#[inline] +pub(crate) unsafe fn elements_of(obj: *const ObjectHeader) -> *mut ArrayHeader { + let meta = (*obj).meta; + if meta.is_null() { + return std::ptr::null_mut(); + } + (*meta).elements as *mut ArrayHeader +} + +/// Store `elements` as the instance's backing store (a barriered meta-record +/// slot store; `elements` may be null to detach, e.g. on deopt). +/// +/// # Safety +/// `obj` must be a live `GC_TYPE_OBJECT` with a meta record. +#[inline] +pub(crate) unsafe fn set_elements_head(obj: *mut ObjectHeader, elements: *mut ArrayHeader) { + let meta = (*obj).meta; + debug_assert!(!meta.is_null()); + // GC_STORE_AUDIT(BARRIERED): meta-record child edge, stored exactly as + // `reserve_object_spill` stores `spill`. + (*meta).elements = elements as u64; + crate::gc::runtime_write_barrier_slot( + meta as usize, + &(*meta).elements as *const _ as usize, + elements as u64, + ); +} + +/// Install a fresh elements store of `length` holes on `obj` (the `super(n)` +/// shape: `length = n`, every index absent). Idempotent: an instance that +/// already has a store keeps it. +/// +/// # Safety +/// `obj` must be a live `GC_TYPE_OBJECT` user pointer. +pub(crate) unsafe fn install_elements(obj: *mut ObjectHeader, length: u32) { + let scope = crate::gc::RuntimeHandleScope::new(); + let obj_handle = scope.root_raw_mut_ptr(obj); + let (_, obj) = + obj_handle.across_mut::(|| crate::object::object_meta_ensure(obj)); + if !elements_of(obj).is_null() { + return; + } + let (elements, obj) = obj_handle + .across_mut::(|| crate::array::js_array_alloc_with_length_exact(length)); + if elements_of(obj).is_null() { + set_elements_head(obj, elements); + } +} + +// --------------------------------------------------------------------------- +// The property funnel: every ordinary-object entry point that can see an +// elements-backed instance's indexed properties or `length` asks here first. +// --------------------------------------------------------------------------- + +/// A property key an elements-backed instance answers itself. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum ElementsKey { + Index(u32), + Length, +} + +/// The live `(object, elements)` pair behind a raw or NaN-box-tagged object +/// address, or `None` for anything that is not an elements-backed instance. +/// +/// # Safety +/// `addr` must be a raw user pointer or a POINTER-tagged NaN-box; it is +/// classified before any dereference. +pub(crate) unsafe fn backed(addr: usize) -> Option<(*mut ObjectHeader, *mut ArrayHeader)> { + let bits = addr as u64; + let raw = if (bits >> 48) == 0x7FFD { + (bits & crate::value::POINTER_MASK) as usize + } else { + addr + }; + let header = crate::value::addr_class::try_read_gc_header(raw)?; + if header.obj_type != crate::gc::GC_TYPE_OBJECT + || header.gc_flags & crate::gc::GC_FLAG_FORWARDED != 0 + { + return None; + } + let obj = raw as *mut ObjectHeader; + let elements = elements_of(obj); + (!elements.is_null()).then_some((obj, elements)) +} + +/// [`backed`] for a NaN-boxed value. +pub(crate) fn backed_value(value: f64) -> Option<(*mut ObjectHeader, *mut ArrayHeader)> { + let js = crate::JSValue::from_bits(value.to_bits()); + if !js.is_pointer() { + return None; + } + unsafe { backed(value.to_bits() as usize) } +} + +pub(crate) fn key_of_str(name: &str) -> Option { + if name == "length" { + return Some(ElementsKey::Length); + } + crate::object::canonical_array_index(name).map(ElementsKey::Index) +} + +/// # Safety +/// `key` must be null or a live string header. +pub(crate) unsafe fn key_of_header(key: *const crate::StringHeader) -> Option { + if key.is_null() { + return None; + } + // Cheap pre-filter: only a leading ASCII digit (an index) or `l` + // (`length`) can be an elements key, and no canonical index is longer + // than ten digits. Every ordinary named property — the common case on + // these receivers — is rejected on one byte, before the UTF-8 decode and + // the canonical-index parse below. + let byte_len = (*key).byte_len as usize; + if byte_len == 0 || byte_len > 10 { + return None; + } + let first = *crate::object::string_header_payload(key); + if !first.is_ascii_digit() && first != b'l' { + return None; + } + crate::object::has_own_helpers::str_from_string_header(key).and_then(key_of_str) +} + +/// A NaN-boxed property key: a canonical-index number, or a string naming an +/// index or `length`. Symbols and everything else are never elements keys. +pub(crate) fn key_of_value(key: f64) -> Option { + let js = crate::JSValue::from_bits(key.to_bits()); + if js.is_number() { + let n = js.as_number(); + if n.is_finite() && n >= 0.0 && n < 4_294_967_295.0 && n.fract() == 0.0 { + return Some(ElementsKey::Index(n as u32)); + } + return None; + } + if js.is_any_string() { + let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + return unsafe { crate::string::js_string_key_bytes(js, &mut sso) } + .and_then(|b| std::str::from_utf8(b).ok()) + .and_then(key_of_str); + } + None +} + +#[inline] +unsafe fn slot_bits(elements: *const ArrayHeader, index: u32) -> u64 { + *(elements as *const u8) + .add(std::mem::size_of::()) + .cast::() + .add(index as usize) +} + +/// Own value for `key`: `length`, or an in-bounds non-hole element. `None` +/// means "not an own property" — the caller continues with its ordinary +/// lookup (the shape carries no such key, so that reaches the prototype +/// chain exactly as a hole on a plain Array does). +/// +/// # Safety +/// `elements` must be the live store of a validated instance. +pub(crate) unsafe fn get_by_key(elements: *const ArrayHeader, key: ElementsKey) -> Option { + match key { + ElementsKey::Length => Some(f64::from((*elements).length)), + ElementsKey::Index(index) => { + if index >= (*elements).length { + return None; + } + let bits = slot_bits(elements, index); + (bits != crate::value::TAG_HOLE).then_some(f64::from_bits(bits)) + } + } +} + +/// `[[Set]]` of an elements key: an in-bounds store, an append, a hole-creating +/// extension, or a `length` write (Array semantics: truncate or extend with +/// holes; a non-index `length` is a RangeError). The owner is rooted across +/// the re-allocating cases and the new head is written back. +/// +/// # Safety +/// `obj` must be a validated elements-backed instance and `elements` its store. +pub(crate) unsafe fn set_by_key( + obj: *mut ObjectHeader, + elements: *mut ArrayHeader, + key: ElementsKey, + value: f64, +) { + match key { + ElementsKey::Length => set_length(obj, elements, value), + ElementsKey::Index(index) => { + let length = (*elements).length; + if index < length { + crate::array::js_array_set_f64(elements, index, value); + return; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let obj_handle = scope.root_raw_mut_ptr(obj); + let value_root = scope.root_nanbox_f64(value); + let (head, obj) = obj_handle.across_mut::(|| { + if index == length { + crate::array::js_array_push_f64(elements, value_root.get_nanbox_f64()) + } else { + crate::array::js_array_set_f64_extend( + elements, + index, + value_root.get_nanbox_f64(), + ) + } + }); + if !head.is_null() && head != elements_of(obj) { + set_elements_head(obj, head); + } + } + } +} + +/// `length = n` on an elements-backed instance. +/// +/// # Safety +/// As [`set_by_key`]. +pub(crate) unsafe fn set_length( + obj: *mut ObjectHeader, + elements: *mut ArrayHeader, + new_length: f64, +) { + if !new_length.is_finite() + || new_length < 0.0 + || new_length.fract() != 0.0 + || new_length >= 4_294_967_296.0 + { + crate::array::array_length_range_error(); + } + let target = new_length as u32; + let current = (*elements).length; + if target <= current { + // Truncation never allocates. + crate::array::js_array_set_length_strict(elements, new_length); + return; + } + // Extension: grow through the index store (which returns the head), then + // punch the written slot back out into a hole. + let scope = crate::gc::RuntimeHandleScope::new(); + let obj_handle = scope.root_raw_mut_ptr(obj); + let (head, obj) = obj_handle.across_mut::(|| { + crate::array::js_array_set_f64_extend( + elements, + target - 1, + f64::from_bits(crate::value::TAG_UNDEFINED), + ) + }); + let head = if head.is_null() { elements } else { head }; + if head != elements_of(obj) { + set_elements_head(obj, head); + } + crate::array::js_array_delete(head, target - 1); +} + +/// # Safety +/// `elements` must be a live store. +pub(crate) unsafe fn has_own_key(elements: *const ArrayHeader, key: ElementsKey) -> bool { + match key { + ElementsKey::Length => true, + ElementsKey::Index(index) => { + index < (*elements).length && slot_bits(elements, index) != crate::value::TAG_HOLE + } + } +} + +/// `delete obj[key]`: an index becomes a hole (1); `length` is +/// non-configurable (0). +/// +/// # Safety +/// `elements` must be a live store. +pub(crate) unsafe fn delete_key(elements: *mut ArrayHeader, key: ElementsKey) -> i32 { + match key { + ElementsKey::Length => 0, + ElementsKey::Index(index) => { + if index < (*elements).length { + crate::array::js_array_delete(elements, index) + } else { + 1 + } + } + } +} + +/// The present (non-hole) indices, ascending. +/// +/// # Safety +/// `elements` must be a live store. +pub(crate) unsafe fn own_index_keys(elements: *const ArrayHeader) -> Vec { + let length = (*elements).length; + let mut out = Vec::new(); + for index in 0..length { + if slot_bits(elements, index) != crate::value::TAG_HOLE { + out.push(index); + } + } + out +} + +/// A fresh keys array: the present indices as strings (ascending), then +/// `"length"` when `with_length` (getOwnPropertyNames), then every key of +/// `shape_keys` in order. `shape_keys` and the result are rooted across the +/// string allocations. +/// +/// # Safety +/// `elements` must be a live store; `shape_keys` a live array (or null). +pub(crate) unsafe fn prepend_index_keys( + elements: *const ArrayHeader, + shape_keys: *mut ArrayHeader, + with_length: bool, +) -> *mut ArrayHeader { + let indices = own_index_keys(elements); + let scope = crate::gc::RuntimeHandleScope::new(); + let shape_h = scope.root_raw_mut_ptr(shape_keys); + let out_h = scope.root_raw_mut_ptr(crate::array::js_array_alloc(0)); + let push_key = |bytes: &[u8]| { + let (s, _) = out_h.across_mut::(|| { + crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32) + }); + let (grown, _) = out_h.across_mut::(|| { + let out: *mut ArrayHeader = out_h.with_mut_ptr(|p| p); + crate::array::js_array_push_f64(out, crate::value::js_nanbox_string(s as i64)) + }); + out_h.set_raw_mut_ptr(grown); + }; + for index in indices { + push_key(index.to_string().as_bytes()); + } + if with_length { + push_key(b"length"); + } + let shape_len = shape_h.with_mut_ptr(|p: *mut ArrayHeader| { + if p.is_null() { + 0 + } else { + crate::array::js_array_length(p) + } + }); + for i in 0..shape_len { + let key = shape_h.with_mut_ptr(|p: *mut ArrayHeader| crate::array::js_array_get(p, i)); + let (grown, _) = out_h.across_mut::(|| { + let out: *mut ArrayHeader = out_h.with_mut_ptr(|p| p); + crate::array::js_array_push_f64(out, f64::from_bits(key.bits())) + }); + out_h.set_raw_mut_ptr(grown); + } + out_h.with_mut_ptr(|p| p) +} + +/// The own property descriptor of an elements key, or `None` when the key is +/// not an own property (a hole, an index past `length`). +/// +/// # Safety +/// `obj` must be a validated instance and `elements` its live store. +pub(crate) unsafe fn own_property_descriptor( + obj: *const ObjectHeader, + elements: *const ArrayHeader, + key: ElementsKey, +) -> Option { + let value = get_by_key(elements, key)?; + let frozen = crate::value::addr_class::try_read_gc_header(obj as usize) + .is_some_and(|h| h._reserved & crate::gc::OBJ_FLAG_FROZEN != 0); + Some(match key { + ElementsKey::Index(_) => { + crate::object::descriptors::build_data_descriptor(value, !frozen, true, !frozen) + } + ElementsKey::Length => { + crate::object::descriptors::build_data_descriptor(value, !frozen, false, false) + } + }) +} + +/// Leave the elements representation for good: every present element becomes +/// a shape-carried index property (ascending), then `length`, and the store is +/// detached. Everything after this runs on the shape-carried machinery +/// (`super::subclass`, `object/array_tail_transition.rs`) exactly as before. +/// Called by the exotic operations that machinery already models — +/// `defineProperty` on an index or `length`, accessor installs, freeze/seal/ +/// preventExtensions, `setPrototypeOf` — before they do their work. +/// +/// # Safety +/// `obj` must be a validated elements-backed instance. +pub(crate) unsafe fn deopt_to_shape(obj: *mut ObjectHeader) { + let elements = elements_of(obj); + if elements.is_null() { + return; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let obj_h = scope.root_raw_mut_ptr(obj); + let elements_h = scope.root_raw_mut_ptr(elements); + // Detach first: the index stores below must not be routed back here. + set_elements_head(obj, std::ptr::null_mut()); + let length = (*elements).length; + for index in own_index_keys(elements) { + let value = + elements_h.with_mut_ptr(|e: *mut ArrayHeader| f64::from_bits(slot_bits(e, index))); + let name = index.to_string(); + let (key, obj) = obj_h.across_mut::(|| { + crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32) + }); + crate::object::js_object_set_field_by_name(obj, key, value); + } + let (key, obj) = obj_h.across_mut::(|| { + crate::string::js_string_from_bytes(b"length".as_ptr(), 6) + }); + crate::object::set_field_by_name_object_tail(obj, key, f64::from(length)); +} + +/// [`deopt_to_shape`] for a NaN-boxed receiver that may not be elements-backed. +pub(crate) fn deopt_value(value: f64) { + if let Some((obj, _)) = backed_value(value) { + unsafe { deopt_to_shape(obj) }; + } +} + +/// A fresh values array: present elements (ascending), then every value of +/// `shape_values` in order (`Object.values`). +/// +/// # Safety +/// `elements` must be a live store; `shape_values` a live array (or null). +pub(crate) unsafe fn prepend_index_values( + elements: *const ArrayHeader, + shape_values: *mut ArrayHeader, +) -> *mut ArrayHeader { + let indices = own_index_keys(elements); + let scope = crate::gc::RuntimeHandleScope::new(); + let elements_h = scope.root_raw_const_ptr(elements); + let shape_h = scope.root_raw_mut_ptr(shape_values); + let out_h = scope.root_raw_mut_ptr(crate::array::js_array_alloc(0)); + let push = |value: f64| { + let (grown, _) = out_h.across_mut::(|| { + let out: *mut ArrayHeader = out_h.with_mut_ptr(|p| p); + crate::array::js_array_push_f64(out, value) + }); + out_h.set_raw_mut_ptr(grown); + }; + for index in indices { + let value = + elements_h.with_const_ptr(|e: *const ArrayHeader| f64::from_bits(slot_bits(e, index))); + push(value); + } + let shape_len = shape_h.with_mut_ptr(|p: *mut ArrayHeader| { + if p.is_null() { + 0 + } else { + crate::array::js_array_length(p) + } + }); + for i in 0..shape_len { + let value = shape_h.with_mut_ptr(|p: *mut ArrayHeader| crate::array::js_array_get(p, i)); + push(f64::from_bits(value.bits())); + } + out_h.with_mut_ptr(|p| p) +} + +/// A fresh entries array: `[String(index), element]` pairs for the present +/// elements (ascending), then every pair of `shape_entries` in order +/// (`Object.entries`). +/// +/// # Safety +/// `elements` must be a live store; `shape_entries` a live array (or null). +pub(crate) unsafe fn prepend_index_entries( + elements: *const ArrayHeader, + shape_entries: *mut ArrayHeader, +) -> *mut ArrayHeader { + let indices = own_index_keys(elements); + let scope = crate::gc::RuntimeHandleScope::new(); + let elements_h = scope.root_raw_const_ptr(elements); + let shape_h = scope.root_raw_mut_ptr(shape_entries); + let out_h = scope.root_raw_mut_ptr(crate::array::js_array_alloc(0)); + let push = |value: f64| { + let (grown, _) = out_h.across_mut::(|| { + let out: *mut ArrayHeader = out_h.with_mut_ptr(|p| p); + crate::array::js_array_push_f64(out, value) + }); + out_h.set_raw_mut_ptr(grown); + }; + for index in indices { + let name = index.to_string(); + let (key, _) = out_h.across_mut::(|| { + crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32) + }); + let key_h = scope.root_nanbox_f64(crate::value::js_nanbox_string(key as i64)); + let (pair, _) = out_h.across_mut::(|| crate::array::js_array_alloc(2)); + let pair_h = scope.root_raw_mut_ptr(pair); + let (pair, _) = pair_h.across_mut::(|| { + let pair: *mut ArrayHeader = pair_h.with_mut_ptr(|p| p); + crate::array::js_array_push_f64(pair, key_h.get_nanbox_f64()) + }); + pair_h.set_raw_mut_ptr(pair); + let value = + elements_h.with_const_ptr(|e: *const ArrayHeader| f64::from_bits(slot_bits(e, index))); + let (pair, _) = pair_h.across_mut::(|| { + let pair: *mut ArrayHeader = pair_h.with_mut_ptr(|p| p); + crate::array::js_array_push_f64(pair, value) + }); + pair_h.set_raw_mut_ptr(pair); + let pair_value = + pair_h.with_mut_ptr(|p: *mut ArrayHeader| crate::value::js_nanbox_pointer(p as i64)); + push(pair_value); + } + let shape_len = shape_h.with_mut_ptr(|p: *mut ArrayHeader| { + if p.is_null() { + 0 + } else { + crate::array::js_array_length(p) + } + }); + for i in 0..shape_len { + let value = shape_h.with_mut_ptr(|p: *mut ArrayHeader| crate::array::js_array_get(p, i)); + push(f64::from_bits(value.bits())); + } + out_h.with_mut_ptr(|p| p) +} + +// --------------------------------------------------------------------------- +// Hot-entry helpers used by `super::subclass` (moved here to keep that file +// under the size gate). +// --------------------------------------------------------------------------- + +/// The elements store of an elements-backed instance (`super::subclass_elements`), +/// or `None` for the shape-carried representation. Non-null only when the +/// store was installed at construction, so no gate check is needed here. +#[inline] +pub(super) fn elements_for_validated( + receiver: &ValidatedObjectReceiver, +) -> Option<*mut ArrayHeader> { + let elements = unsafe { elements_of(receiver.object) }; + (!elements.is_null()).then_some(elements) +} + +/// In-bounds, non-hole element of the inner array; `None` sends the caller +/// to the same prototype-chain fallback a hole in the shape-carried form does. +#[inline] +pub(super) fn elements_index_get(elements: *const ArrayHeader, index: u32) -> Option { + unsafe { + if index >= (*elements).length { + return None; + } + let slot = (elements as *const u8) + .add(std::mem::size_of::()) + .cast::() + .add(index as usize); + let bits = *slot; + (bits != crate::value::TAG_HOLE).then_some(f64::from_bits(bits)) + } +} + +/// Elements-backed `receiver[index] = value` for an in-bounds index or the +/// appending index (`== length`); anything else (holes past the end, a +/// frozen/sealed/non-extensible receiver) declines to the generic path. +pub(super) fn elements_index_set( + receiver: &ValidatedObjectReceiver, + index: u32, + value: f64, +) -> Option { + let elements = elements_for_validated(receiver)?; + if !mutation_receiver_allows_plain_tail(receiver.object_flags) { + return Some(false); + } + let length = unsafe { (*elements).length }; + if index < length { + crate::array::js_array_set_f64(elements, index, value); + return Some(true); + } + if index == length { + return elements_push(receiver, value).map(|_| true); + } + Some(false) +} + +/// Elements-backed append: the owner is rooted across the (possibly +/// re-allocating) push and the new head is written back through the +/// barriered meta slot. Returns the new length. +pub(super) fn elements_push(receiver: &ValidatedObjectReceiver, value: f64) -> Option { + let elements = elements_for_validated(receiver)?; + if !mutation_receiver_allows_plain_tail(receiver.object_flags) { + return None; + } + let obj = receiver.object as *mut ObjectHeader; + unsafe { + let scope = crate::gc::RuntimeHandleScope::new(); + let obj_handle = scope.root_raw_mut_ptr(obj); + let value_root = scope.root_nanbox_f64(value); + let (grown, obj) = obj_handle.across_mut::(|| { + crate::array::js_array_push_f64(elements, value_root.get_nanbox_f64()) + }); + let current = elements_of(obj); + if grown != current { + set_elements_head(obj, grown); + } + Some(f64::from((*grown).length)) + } +} + +/// Elements-backed `pop`: the inner array's own pop (no allocation, so no +/// rooting), declining like `elements_push` on a non-plain receiver. +pub(super) fn elements_pop(receiver: &ValidatedObjectReceiver) -> Option { + let elements = elements_for_validated(receiver)?; + if !mutation_receiver_allows_plain_tail(receiver.object_flags) { + return None; + } + Some(crate::array::js_array_pop_f64(elements)) +} diff --git a/crates/perry-runtime/src/array/subclass_elements_tests.rs b/crates/perry-runtime/src/array/subclass_elements_tests.rs new file mode 100644 index 0000000000..954586dba6 --- /dev/null +++ b/crates/perry-runtime/src/array/subclass_elements_tests.rs @@ -0,0 +1,414 @@ +//! The `ObjectMeta.elements` edge of an Array-subclass instance is a traced +//! child exactly like `spill`: it must survive owner and meta evacuation, be +//! rewritten to the moved inner array, and keep the inner array alive. +use super::subclass_elements::{elements_of, install_elements, set_elements_head}; +use crate::object::{js_object_alloc, ObjectHeader}; + +const CLASS_ID_ARRAY: u32 = 0xFFFF_0024; + +fn live_obj(receiver: f64) -> *mut ObjectHeader { + (receiver.to_bits() & 0x0000_FFFF_FFFF_FFFF) as *mut ObjectHeader +} + +#[test] +fn the_elements_edge_survives_moving_gc_and_keeps_the_inner_array_alive() { + let _copying_nursery = crate::gc::CopyingNurseryTestGuard::new(0); + let _triggers = crate::gc::GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let _force_evacuation = crate::gc::knob_overrides::ForcedEvacuationTestGuard::on(); + crate::gc::register_runtime_handle_root_scanner_for_tests(); + crate::gc::gc_register_mutable_root_scanner(crate::object::shapes::scan_shape_table_rekey_mut); + + let class_id = 0x0074_8695; + crate::object::js_register_class_parent(class_id, CLASS_ID_ARRAY); + let obj = js_object_alloc(class_id, 2); + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver_h = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(obj as i64)); + + // `super(3)`: three holes, `length` 3, no shape-carried `length`. + unsafe { install_elements(live_obj(receiver_h.get_nanbox_f64()), 3) }; + let before_elements = unsafe { elements_of(live_obj(receiver_h.get_nanbox_f64())) }; + assert!(!before_elements.is_null()); + assert_eq!(unsafe { (*before_elements).length }, 3); + // Idempotent: a second install keeps the store. + unsafe { install_elements(live_obj(receiver_h.get_nanbox_f64()), 9) }; + assert_eq!( + unsafe { elements_of(live_obj(receiver_h.get_nanbox_f64())) }, + before_elements + ); + + // An append past the exact capacity re-allocates the inner array; the + // head is written back through the barriered meta slot. + let grown = crate::array::js_array_push_f64(before_elements, 44.0); + unsafe { set_elements_head(live_obj(receiver_h.get_nanbox_f64()), grown) }; + crate::array::js_array_set_f64(grown, 0, 11.0); + assert_eq!(unsafe { (*grown).length }, 4); + + let before_cycles = crate::gc::copying_minor_cycles(); + let _ = crate::gc::gc_collect_minor(); + assert!(crate::gc::copying_minor_cycles() > before_cycles); + + let live = live_obj(receiver_h.get_nanbox_f64()); + let elements = unsafe { elements_of(live) }; + assert!( + !elements.is_null(), + "the edge must be rewritten, not dropped" + ); + assert_ne!( + elements, grown, + "forced evacuation must have moved the inner array" + ); + let header = unsafe { crate::value::addr_class::try_read_gc_header(elements as usize) } + .expect("the moved inner array is a live heap object"); + assert_eq!(header.obj_type, crate::gc::GC_TYPE_ARRAY); + assert_eq!(header.gc_flags & crate::gc::GC_FLAG_FORWARDED, 0); + assert_eq!(unsafe { (*elements).length }, 4); + assert_eq!(crate::array::js_array_get_f64(elements, 0), 11.0); + assert_eq!(crate::array::js_array_get_f64(elements, 3), 44.0); + // The untouched indices are still absent (a hole, or `undefined` once the + // read resolves it through the prototype chain), never a stale value. + let hole = crate::array::js_array_get_f64(elements, 1).to_bits(); + assert!( + hole == crate::value::TAG_HOLE || hole == crate::value::TAG_UNDEFINED, + "index 1 must still be absent: {hole:#x}" + ); +} + +/// The hot runtime entries route an elements-backed instance to its inner +/// array: `length`, `[i]` get/set, append (including the re-allocating one, +/// with the owner rooted across it) and pop — through both the value-taking +/// `array_subclass_fast_*` entries and the raw `js_array_*` entries the +/// codegen fallbacks call, and across a forced-evacuation minor GC. +#[test] +fn hot_entries_route_to_the_elements_store() { + use super::subclass::{ + array_subclass_fast_index_get, array_subclass_fast_index_set, array_subclass_fast_length, + array_subclass_fast_pop, array_subclass_fast_push_one, + }; + let _copying_nursery = crate::gc::CopyingNurseryTestGuard::new(0); + let _triggers = crate::gc::GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let _force_evacuation = crate::gc::knob_overrides::ForcedEvacuationTestGuard::on(); + crate::gc::register_runtime_handle_root_scanner_for_tests(); + crate::gc::gc_register_mutable_root_scanner(crate::object::shapes::scan_shape_table_rekey_mut); + + let class_id = 0x0074_8696; + crate::object::js_register_class_parent(class_id, CLASS_ID_ARRAY); + let obj = js_object_alloc(class_id, 2); + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver_h = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(obj as i64)); + unsafe { install_elements(live_obj(receiver_h.get_nanbox_f64()), 0) }; + let recv = || receiver_h.get_nanbox_f64(); + let as_arr = || (recv().to_bits() & 0x0000_FFFF_FFFF_FFFF) as *mut crate::array::ArrayHeader; + + assert_eq!(array_subclass_fast_length(recv()), Some(0.0)); + assert_eq!(array_subclass_fast_index_get(recv(), 0), None); + // 40 appends from an exact-capacity-0 store: several re-allocations. + for i in 0..40u32 { + assert_eq!( + array_subclass_fast_push_one(recv(), f64::from(i)), + Some(f64::from(i + 1)) + ); + if i == 17 { + let _ = crate::gc::gc_collect_minor(); + } + } + assert_eq!(array_subclass_fast_length(recv()), Some(40.0)); + for i in 0..40u32 { + assert_eq!(array_subclass_fast_index_get(recv(), i), Some(f64::from(i))); + } + assert_eq!(array_subclass_fast_index_get(recv(), 40), None); + // In-bounds write, appending write, hole-creating write (declined). + assert!(array_subclass_fast_index_set(recv(), 3, 300.0)); + assert_eq!(array_subclass_fast_index_get(recv(), 3), Some(300.0)); + assert!(array_subclass_fast_index_set(recv(), 40, 400.0)); + assert_eq!(array_subclass_fast_length(recv()), Some(41.0)); + assert!(!array_subclass_fast_index_set(recv(), 50, 500.0)); + assert_eq!(array_subclass_fast_length(recv()), Some(41.0)); + // Pop through the value entry and through the raw `js_array_*` entries + // the codegen fallbacks call with the object address as an ArrayHeader. + assert_eq!(array_subclass_fast_pop(recv()), Some(400.0)); + assert_eq!(crate::array::js_array_pop_f64(as_arr()), 39.0); + assert_eq!(crate::array::js_array_length(as_arr()), 39); + let _ = crate::array::js_array_push_f64(as_arr(), 77.0); + assert_eq!(crate::array::js_array_length(as_arr()), 40); + assert_eq!(crate::array::js_array_get_f64(as_arr(), 39), 77.0); + assert_eq!(array_subclass_fast_index_get(recv(), 39), Some(77.0)); + let _ = crate::gc::gc_collect_minor(); + assert_eq!(array_subclass_fast_length(recv()), Some(40.0)); + assert_eq!(array_subclass_fast_index_get(recv(), 3), Some(300.0)); + assert_eq!(crate::array::js_array_get_f64(as_arr(), 39), 77.0); + // The shape-carried machinery never learned anything for this instance. + assert_eq!( + unsafe { (*(*live_obj(recv())).meta).array_subclass_dense_key }, + 0 + ); +} + +fn key(name: &str) -> *const crate::StringHeader { + crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32) +} +fn key_value(name: &str) -> f64 { + crate::value::js_nanbox_string(key(name) as i64) +} +fn key_strings(arr: *const crate::array::ArrayHeader) -> Vec { + let n = crate::array::js_array_length(arr); + (0..n) + .map(|i| { + let v = crate::array::js_array_get(arr, i); + let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + unsafe { crate::string::js_string_key_bytes(v, &mut sso) } + .map(|b| String::from_utf8_lossy(b).into_owned()) + .unwrap_or_else(|| "".to_string()) + }) + .collect() +} +fn truthy(v: f64) -> bool { + v.to_bits() == 0x7FFC_0000_0000_0004 +} + +/// The property funnel: through the ordinary object entry points, an +/// elements-backed instance's indices and `length` are own properties backed +/// by the inner array — reads, writes (in-bounds, append, hole-creating +/// extension, `length` truncation/extension), `hasOwnProperty`/`in`, +/// `delete`, key order for `Object.keys`/`getOwnPropertyNames`, and own +/// property descriptors — and no index key ever lands in the shape. +#[test] +fn the_property_funnel_answers_indices_and_length_from_the_store() { + let _triggers = crate::gc::GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + crate::gc::register_runtime_handle_root_scanner_for_tests(); + let class_id = 0x0074_8697; + crate::object::js_register_class_parent(class_id, CLASS_ID_ARRAY); + let obj = js_object_alloc(class_id, 2); + let scope = crate::gc::RuntimeHandleScope::new(); + let recv_h = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(obj as i64)); + unsafe { install_elements(live_obj(recv_h.get_nanbox_f64()), 0) }; + let recv = || recv_h.get_nanbox_f64(); + let obj = || live_obj(recv()); + let get = |name: &str| crate::object::js_object_get_field_by_name(obj(), key(name)); + let set = |name: &str, v: f64| crate::object::js_object_set_field_by_name(obj(), key(name), v); + + // A named field stays a shape property; `length` and indices do not. + set("tag", 7.0); + set("0", 10.0); + set("1", 11.0); + set("2", 12.0); + assert_eq!(get("length").as_number(), 3.0); + assert_eq!(get("1").as_number(), 11.0); + assert_eq!(get("tag").as_number(), 7.0); + assert!(get("5").is_undefined()); + // Hole-creating extension, then `length` truncation and extension. + set("5", 15.0); + assert_eq!(get("length").as_number(), 6.0); + assert!(get("3").is_undefined()); + assert_eq!(get("5").as_number(), 15.0); + set("length", 2.0); + assert_eq!(get("length").as_number(), 2.0); + assert!(get("2").is_undefined()); + set("length", 4.0); + assert_eq!(get("length").as_number(), 4.0); + assert!(get("3").is_undefined()); + set("3", 13.0); + // hasOwn / in. + assert!(truthy(crate::object::js_object_has_own( + recv(), + key_value("0") + ))); + assert!(truthy(crate::object::js_object_has_own( + recv(), + key_value("length") + ))); + assert!(!truthy(crate::object::js_object_has_own( + recv(), + key_value("2") + ))); + assert!(!truthy(crate::object::js_object_has_own( + recv(), + key_value("9") + ))); + assert!(truthy(crate::object::js_object_has_property(recv(), 3.0))); + assert!(!truthy(crate::object::js_object_has_property(recv(), 2.0))); + assert!(truthy(crate::object::js_object_has_property( + recv(), + key_value("tag") + ))); + // delete: an index becomes a hole, `length` is untouched and undeletable. + assert_eq!(crate::object::js_object_delete_dynamic(obj(), 0.0), 1); + assert!(get("0").is_undefined()); + assert_eq!(get("length").as_number(), 4.0); + assert_eq!( + crate::object::js_object_delete_dynamic(obj(), key_value("length")), + 0 + ); + // Key order: present indices ascending, then shape keys; `length` only + // in getOwnPropertyNames, between them. + assert_eq!( + key_strings(crate::object::js_object_keys(obj())), + vec!["1", "3", "tag"] + ); + let names = crate::object::js_object_get_own_property_names(recv()); + assert_eq!( + key_strings(crate::value::js_nanbox_get_pointer(names) as *const crate::array::ArrayHeader), + vec!["1", "3", "length", "tag"] + ); + // values / entries: present elements first, then the shape's `tag`. + let values = crate::object::js_object_values(obj()); + assert_eq!(crate::array::js_array_length(values), 3); + assert_eq!(crate::array::js_array_get(values, 0).as_number(), 11.0); + assert_eq!(crate::array::js_array_get(values, 1).as_number(), 13.0); + assert_eq!(crate::array::js_array_get(values, 2).as_number(), 7.0); + let entries = crate::object::js_object_entries(obj()); + assert_eq!(crate::array::js_array_length(entries), 3); + let first = crate::value::js_nanbox_get_pointer(f64::from_bits( + crate::array::js_array_get(entries, 0).bits(), + )) as *const crate::array::ArrayHeader; + assert_eq!(key_strings(first)[0], "1"); + assert_eq!(crate::array::js_array_get(first, 1).as_number(), 11.0); + // Descriptors. + let d = crate::object::js_object_get_own_property_descriptor(recv(), key_value("1")); + let dobj = crate::value::js_nanbox_get_pointer(d) as *const ObjectHeader; + assert_eq!( + crate::object::js_object_get_field_by_name(dobj, key("value")).as_number(), + 11.0 + ); + assert!(truthy(f64::from_bits( + crate::object::js_object_get_field_by_name(dobj, key("enumerable")).bits() + ))); + let d = crate::object::js_object_get_own_property_descriptor(recv(), key_value("length")); + let dobj = crate::value::js_nanbox_get_pointer(d) as *const ObjectHeader; + assert_eq!( + crate::object::js_object_get_field_by_name(dobj, key("value")).as_number(), + 4.0 + ); + assert!(!truthy(f64::from_bits( + crate::object::js_object_get_field_by_name(dobj, key("enumerable")).bits() + ))); + assert!(crate::JSValue::from_bits( + crate::object::js_object_get_own_property_descriptor(recv(), key_value("0")).to_bits() + ) + .is_undefined()); + // The shape never learned an index key. + assert!(!key_strings(crate::object::js_object_keys(obj())) + .iter() + .any(|k| k == "0" || k == "1" && false)); + assert!(!unsafe { elements_of(obj()) }.is_null()); +} + +/// `Object.freeze` leaves the elements representation for good: every present +/// element and `length` become shape-carried properties, the store is +/// detached, and the frozen instance reads back exactly the same. +#[test] +fn freeze_deopts_to_the_shape_carried_form() { + let _triggers = crate::gc::GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + crate::gc::register_runtime_handle_root_scanner_for_tests(); + let class_id = 0x0074_8698; + crate::object::js_register_class_parent(class_id, CLASS_ID_ARRAY); + let obj = js_object_alloc(class_id, 2); + let scope = crate::gc::RuntimeHandleScope::new(); + let recv_h = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(obj as i64)); + unsafe { install_elements(live_obj(recv_h.get_nanbox_f64()), 0) }; + let recv = || recv_h.get_nanbox_f64(); + let obj = || live_obj(recv()); + for i in 0..3u32 { + crate::object::js_object_set_field_by_name(obj(), key(&i.to_string()), f64::from(i * 10)); + } + crate::object::js_object_delete_dynamic(obj(), 1.0); + crate::object::js_object_set_field_by_name(obj(), key("tag"), 7.0); + let _ = crate::object::js_object_freeze(recv()); + assert!( + unsafe { elements_of(obj()) }.is_null(), + "the store is detached on freeze" + ); + let get = |name: &str| crate::object::js_object_get_field_by_name(obj(), key(name)); + assert_eq!(get("length").as_number(), 3.0); + assert_eq!(get("0").as_number(), 0.0); + assert!(get("1").is_undefined()); + assert_eq!(get("2").as_number(), 20.0); + assert_eq!(get("tag").as_number(), 7.0); + assert!(truthy(crate::object::js_object_has_own( + recv(), + key_value("2") + ))); + assert!(!truthy(crate::object::js_object_has_own( + recv(), + key_value("1") + ))); + // Frozen: the shape-carried machinery refuses the append. + assert_eq!( + super::subclass::array_subclass_fast_push_one(recv(), 99.0), + None + ); +} + +/// The counted-loop guard admits an elements-backed instance as a PLAIN-ARRAY +/// loop over its inner array: kind 1, the inner array's live address, and a +/// revalidation that re-resolves the (possibly re-allocated) store from the +/// receiver — this is what keeps versioned loops off the string-keyed +/// property path. +#[test] +fn the_counted_loop_guard_admits_an_elements_backed_receiver_as_its_inner_array() { + let _triggers = crate::gc::GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + crate::gc::register_runtime_handle_root_scanner_for_tests(); + let class_id = 0x0074_8699; + crate::object::js_register_class_parent(class_id, CLASS_ID_ARRAY); + let obj = js_object_alloc(class_id, 2); + let scope = crate::gc::RuntimeHandleScope::new(); + let recv_h = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(obj as i64)); + unsafe { install_elements(live_obj(recv_h.get_nanbox_f64()), 0) }; + let recv = || recv_h.get_nanbox_f64(); + for i in 0..8u32 { + assert!(super::subclass::array_subclass_fast_push_one(recv(), f64::from(i)).is_some()); + } + + let mut facts = [0u64; 7]; + let live = super::subclass::loop_guard::js_packed_arraylike_loop_guard_live( + recv(), + -1.0, + 0, + facts.as_mut_ptr(), + ); + assert_ne!(live, 0, "an elements-backed receiver must be admitted"); + assert_eq!(facts[0], 1, "admitted as a plain-array loop"); + assert_eq!( + live as usize, + unsafe { elements_of(live_obj(recv())) } as usize, + "the live address is the inner array" + ); + assert_eq!(facts[6], 8, "the live-length bound is the inner length"); + let revalidated = super::subclass::loop_guard::js_packed_arraylike_loop_revalidate_live( + recv(), + -1.0, + 0, + facts.as_ptr(), + ); + assert_eq!(revalidated, live, "revalidation resolves the same store"); + + // An append inside the loop body re-allocates the store. The recorded + // facts describe the OLD array, so revalidation takes the side exit (0), + // exactly as it does for a grown plain Array — and a fresh guard call + // then admits the current store. + for i in 8..64u32 { + assert!(super::subclass::array_subclass_fast_push_one(recv(), f64::from(i)).is_some()); + } + let grown = unsafe { elements_of(live_obj(recv())) }; + assert_ne!(grown as usize, live as usize, "the appends re-allocated"); + assert_eq!( + super::subclass::loop_guard::js_packed_arraylike_loop_revalidate_live( + recv(), + -1.0, + 0, + facts.as_ptr(), + ), + 0, + "stale facts must side-exit" + ); + let live2 = super::subclass::loop_guard::js_packed_arraylike_loop_guard_live( + recv(), + -1.0, + 0, + facts.as_mut_ptr(), + ); + assert_eq!( + live2 as usize, grown as usize, + "re-admission sees the new store" + ); + assert_eq!(facts[6], 64); +} diff --git a/crates/perry-runtime/src/array/subclass_loop_guard.rs b/crates/perry-runtime/src/array/subclass_loop_guard.rs index c170048f04..99c720ea5b 100644 --- a/crates/perry-runtime/src/array/subclass_loop_guard.rs +++ b/crates/perry-runtime/src/array/subclass_loop_guard.rs @@ -15,6 +15,32 @@ use super::*; /// dense_prefix|inline_bound<<32, bound)`. Kind 1 is an ArrayHeader and kind 2 /// is an ObjectHeader Array subclass. A zero return leaves every semantic case /// to the unchanged generic loop. +/// Resolve an elements-backed Array-subclass receiver to its inner array +/// (`None` for every other receiver, including the shape-carried subclass +/// form, which keeps the kind-2 admission below). +#[inline] +fn elements_loop_source( + raw: *const u8, + header: &'static crate::gc::GcHeader, +) -> Option<(*const u8, &'static crate::gc::GcHeader)> { + if header.obj_type != crate::gc::GC_TYPE_OBJECT + || header.gc_flags & crate::gc::GC_FLAG_FORWARDED != 0 + { + return None; + } + let elements = + unsafe { crate::array::subclass_elements::elements_of(raw.cast::()) } + as *const u8; + if elements.is_null() { + return None; + } + let elements_header = + unsafe { crate::value::addr_class::try_read_gc_header(elements as usize) }?; + (elements_header.obj_type == crate::gc::GC_TYPE_ARRAY + && elements_header.gc_flags & crate::gc::GC_FLAG_FORWARDED == 0) + .then_some((elements, elements_header)) +} + fn packed_arraylike_loop_guard( receiver: f64, bound: f64, @@ -64,6 +90,14 @@ fn packed_arraylike_loop_guard( source }; let header = unsafe { crate::value::addr_class::try_read_gc_header(raw as usize) }?; + // An elements-backed Array-subclass instance (`super::subclass_elements`) + // keeps its elements in a real Array hanging off the meta record, so the + // loop is a PLAIN-ARRAY loop over that inner array: resolve to it and let + // the ordinary kind-1 admission below prove length/capacity/descriptors + // and (for a numeric mode) the raw-f64 bit. The live address the caller + // reads through is the inner array's, and the revalidation entry + // re-resolves it from the receiver on every iteration. + let (raw, header) = elements_loop_source(raw, header).unwrap_or((raw, header)); if header.obj_type == crate::gc::GC_TYPE_ARRAY { if header._reserved & crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS != 0 @@ -292,6 +326,11 @@ pub extern "C" fn js_packed_arraylike_loop_revalidate_live( else { return 0; }; + // Re-resolve an elements-backed receiver to its CURRENT inner array: an + // append inside the loop body may have re-allocated it, and the meta slot + // is the authority. + let (source, source_header) = + elements_loop_source(source, source_header).unwrap_or((source, source_header)); let raw = if source_header.gc_flags & crate::gc::GC_FLAG_FORWARDED != 0 { if source_header.obj_type != crate::gc::GC_TYPE_ARRAY { return 0; diff --git a/crates/perry-runtime/src/gc/layout.rs b/crates/perry-runtime/src/gc/layout.rs index fe69b71707..12b8ae396c 100644 --- a/crates/perry-runtime/src/gc/layout.rs +++ b/crates/perry-runtime/src/gc/layout.rs @@ -1437,6 +1437,9 @@ pub(crate) struct HeapChildSlotIterator { /// #6812: second prefix — the object's `meta` header edge. Kept /// separate from `prefix_slot` so payload indices stay mask-aligned. pub(super) meta_slot: Option<*mut u64>, + /// A second explicit meta edge — the Array-subclass `elements` store at + /// the end of the `ObjectMeta` record — read exactly like `meta_slot`. + pub(super) meta_slot2: Option<*mut u64>, pub(super) payload: HeapSlotRange, pub(super) selection: HeapPayloadSlotSelection, /// #8122: the receiver's `ShapeDescriptor`, resolved ONCE by @@ -1452,6 +1455,7 @@ impl HeapChildSlotIterator { Self { prefix_slot: None, meta_slot: None, + meta_slot2: None, payload: HeapSlotRange::new(std::ptr::null_mut(), 0), selection: HeapPayloadSlotSelection::Empty, object_shape: None, @@ -1467,6 +1471,7 @@ impl HeapChildSlotIterator { Self { prefix_slot, meta_slot: None, + meta_slot2: None, payload, selection, object_shape: None, @@ -1487,6 +1492,7 @@ impl HeapChildSlotIterator { Self { prefix_slot, meta_slot: None, + meta_slot2: None, payload, selection, object_shape, @@ -1498,10 +1504,19 @@ impl HeapChildSlotIterator { self } + pub(super) fn with_meta_slot2(mut self, slot: Option<*mut u64>) -> Self { + self.meta_slot2 = slot; + self + } + pub(super) fn take_meta_child_slot(&mut self) -> Option<*mut u64> { self.meta_slot.take() } + pub(super) fn take_meta_child_slot2(&mut self) -> Option<*mut u64> { + self.meta_slot2.take() + } + pub(super) fn take_prefix_child_slot(&mut self) -> Option<*mut u64> { self.prefix_slot.take() } @@ -1549,6 +1564,9 @@ impl Iterator for HeapChildSlotIterator { if let Some(slot) = self.meta_slot.take() { return Some(HeapChildSlot::Child(slot, HeapChildSlotReadKind::Prefix)); } + if let Some(slot) = self.meta_slot2.take() { + return Some(HeapChildSlot::Child(slot, HeapChildSlotReadKind::Prefix)); + } match &mut self.selection { HeapPayloadSlotSelection::Empty => None, HeapPayloadSlotSelection::PointerFree { @@ -1746,7 +1764,13 @@ pub(super) unsafe fn gc_child_slots(header: *mut GcHeader) -> HeapChildSlotItera let proto_slot = Some(&mut (*meta).prototype as *mut u64); let brand_slot = Some(&mut (*meta).private_evaluation_brand as *mut u64); let range = HeapSlotRange::new(&mut (*meta).spill as *mut u64, 1); - HeapChildSlotIterator::new(header, proto_slot, range).with_meta_slot(brand_slot) + // The Array-subclass elements store is a raw-pointer child edge + // (0 = none) exactly like `spill`; it sits at the end of the + // record, so it is enumerated as a second explicit meta edge. + let elements_slot = Some(&mut (*meta).elements as *mut u64); + HeapChildSlotIterator::new(header, proto_slot, range) + .with_meta_slot(brand_slot) + .with_meta_slot2(elements_slot) } GcLayoutSlotKind::ClosureCaptures => { let closure = user_ptr as *mut crate::closure::ClosureHeader; diff --git a/crates/perry-runtime/src/gc/layout_slot_visit.rs b/crates/perry-runtime/src/gc/layout_slot_visit.rs index c2a7f9c05a..abe78b396d 100644 --- a/crates/perry-runtime/src/gc/layout_slot_visit.rs +++ b/crates/perry-runtime/src/gc/layout_slot_visit.rs @@ -75,6 +75,9 @@ pub(super) unsafe fn visit_gc_layout_slot_descriptors( if let Some(slot) = child_slots.take_meta_child_slot() { visit(fixed_slot(slot).with_layout(HeapChildSlotReadKind::Prefix)); } + if let Some(slot) = child_slots.take_meta_child_slot2() { + visit(fixed_slot(slot).with_layout(HeapChildSlotReadKind::Prefix)); + } match child_slots.payload_scan() { HeapPayloadSlotScan::Empty => {} @@ -326,6 +329,9 @@ pub(super) unsafe fn visit_gc_rewrite_slot_descriptors( // so an unvisited edge here collects a live object's own // properties — the same shape as the spill hazard above (#6812). visit(fixed_slot(&mut (*meta).expando as *mut u64)); + // The Array-subclass elements store (0 = none): a raw-pointer child + // edge traced and rewritten exactly like `spill`. + visit(fixed_slot(&mut (*meta).elements as *mut u64)); // A fresh class object stored as an instance's private evaluation // brand is a NaN-boxed child edge and moves with the meta record. visit(fixed_slot( diff --git a/crates/perry-runtime/src/json/stringify.rs b/crates/perry-runtime/src/json/stringify.rs index 6718792ea6..5979836d49 100644 --- a/crates/perry-runtime/src/json/stringify.rs +++ b/crates/perry-runtime/src/json/stringify.rs @@ -551,6 +551,11 @@ pub(crate) unsafe fn stringify_value(value: f64, type_hint: u32, buf: &mut Strin // capacity heuristic (`cap < 10000`) misidentified legitimate // arrays that had grown past 10k as strings, panicking on // `JSON.stringify(arr)` where `arr.length >= 10000` (issue #43). + // An elements-backed Array-subclass instance IS an Array to + // `JSON.stringify` (IsArray is true): serialize its elements. + if let Some((_, elements)) = crate::array::subclass_elements::backed(ptr as usize) { + return stringify_array(elements as *const u8, buf); + } match gc_obj_type(ptr) { crate::gc::GC_TYPE_ARRAY => stringify_array(ptr, buf), // A function has no ordinary object/array/string/error/map/set @@ -814,6 +819,9 @@ pub(crate) unsafe fn stringify_value_depth( buf.push_str("null"); return; } + if let Some((_, elements)) = crate::array::subclass_elements::backed(ptr as usize) { + return stringify_array_depth(elements as *const u8, buf, depth); + } match gc_obj_type(ptr) { crate::gc::GC_TYPE_OBJECT => stringify_object_inner(ptr, buf, depth), crate::gc::GC_TYPE_ARRAY => stringify_array_depth(ptr, buf, depth), diff --git a/crates/perry-runtime/src/node_stream_constructors/builders.rs b/crates/perry-runtime/src/node_stream_constructors/builders.rs index aee7a3b187..de5a937947 100644 --- a/crates/perry-runtime/src/node_stream_constructors/builders.rs +++ b/crates/perry-runtime/src/node_stream_constructors/builders.rs @@ -194,6 +194,28 @@ pub extern "C" fn js_array_subclass_init(this: f64, n: f64) -> f64 { n.floor().min(MAX_SAFE_INTEGER) } }; + if crate::array::subclass_elements::array_subclass_elements_enabled() { + // Elements-backed instance: `length` and the indices live in the + // store, never as shape-carried properties. + let scope = crate::gc::RuntimeHandleScope::new(); + let this_root = scope.root_nanbox_f64(this); + unsafe { + crate::array::subclass_elements::install_elements(obj, len.min(u32::MAX as f64) as u32) + }; + // The Array surface the instance relies on, installed exactly as in + // the shape-carried form. It must NOT be hidden behind a property + // descriptor: that sets `OBJ_FLAG_HAS_DESCRIPTORS` on every instance, + // which the codegen class-field inline guard rejects — every field + // read then takes the IC miss (measured: 6x on the wolf-ecs twins). + // `fill` showing up in `getOwnPropertyNames` is the pre-existing + // divergence tracked in #8953, unchanged by the elements store. + let this = this_root.get_nanbox_f64(); + let obj = raw_ptr_from_value(this) as *mut ObjectHeader; + crate::closure::js_register_closure_arity(ns_array_fill as *const u8, 1); + let methods: [(&str, StubFn); 1] = [("fill", super::cast1(ns_array_fill))]; + install_methods_on_existing_object(obj, this, &methods, &[]); + return this_root.get_nanbox_f64(); + } let length_key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); js_object_set_field_by_name(obj, length_key, len); crate::closure::js_register_closure_arity(ns_array_fill as *const u8, 1); diff --git a/crates/perry-runtime/src/object/delete_rest.rs b/crates/perry-runtime/src/object/delete_rest.rs index 037fd19985..00d84accba 100644 --- a/crates/perry-runtime/src/object/delete_rest.rs +++ b/crates/perry-runtime/src/object/delete_rest.rs @@ -514,6 +514,11 @@ pub extern "C" fn js_object_delete_dynamic_value(obj_value: f64, key: f64) -> i3 /// Returns 1 if successful, 0 otherwise #[no_mangle] pub extern "C" fn js_object_delete_dynamic(obj: *mut ObjectHeader, key: f64) -> i32 { + if let Some((_, elements)) = unsafe { crate::array::subclass_elements::backed(obj as usize) } { + if let Some(elements_key) = crate::array::subclass_elements::key_of_value(key) { + return unsafe { crate::array::subclass_elements::delete_key(elements, elements_key) }; + } + } // Proxy receiver (small registered id) — route through the proxy // `deleteProperty` trap before any key coercion that would deref the fake // pointer. Handles symbol keys too (the string path also funnels into diff --git a/crates/perry-runtime/src/object/descriptors.rs b/crates/perry-runtime/src/object/descriptors.rs index a0d0689350..5c65969c00 100644 --- a/crates/perry-runtime/src/object/descriptors.rs +++ b/crates/perry-runtime/src/object/descriptors.rs @@ -121,6 +121,16 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu if crate::proxy::js_proxy_is_proxy(obj_value) != 0 { return crate::proxy::js_reflect_get_own_property_descriptor(obj_value, key_value); } + if let Some((obj, elements)) = crate::array::subclass_elements::backed_value(obj_value) { + if let Some(elements_key) = crate::array::subclass_elements::key_of_value(key_value) { + return crate::array::subclass_elements::own_property_descriptor( + obj, + elements, + elements_key, + ) + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)); + } + } // #6363: a native HANDLE receiver (zlib stream, fetch Headers/Request/ // Response/Blob, crypto hash, …) is a pointer-tagged registry id, not a @@ -1223,6 +1233,42 @@ unsafe fn string_primitive_descriptor(str_value: f64, key_value: f64) -> f64 { /// Takes a NaN-boxed f64 object pointer, returns a NaN-boxed f64 array pointer. #[no_mangle] pub extern "C" fn js_object_get_own_property_names(obj_value: f64) -> f64 { + // An elements-backed Array-subclass instance: present indices, then + // `length`, then the shape's own string keys. + if crate::array::subclass_elements::backed_value(obj_value).is_some() { + let scope = crate::gc::RuntimeHandleScope::new(); + let obj_h = scope.root_nanbox_f64(obj_value); + let (names, obj_value) = + obj_h.across_nanbox(|| js_object_get_own_property_names_shape(obj_value)); + if let Some((_, elements)) = crate::array::subclass_elements::backed_value(obj_value) { + let names_ptr = crate::value::js_nanbox_get_pointer(names) as *mut ArrayHeader; + let combined = unsafe { + crate::array::subclass_elements::prepend_index_keys(elements, names_ptr, true) + }; + return crate::value::js_nanbox_pointer(combined as i64); + } + return names; + } + js_object_get_own_property_names_shape(obj_value) +} + +/// [`js_object_get_own_property_names`] over the shape alone. +/// A receiver whose heap cell IS a `GC_TYPE_ARRAY`. `Array.isArray` is also +/// true for a `class X extends Array` instance, but that is an `ObjectHeader` +/// (#8953: reading it through the array key helpers dereferenced a null +/// cleaned pointer); its own keys come from the ordinary object walk — plus +/// the elements store, for the elements-backed form. +fn real_array_receiver(value: f64) -> bool { + let jv = JSValue::from_bits(value.to_bits()); + if !jv.is_pointer() { + return false; + } + let raw = (value.to_bits() & crate::value::POINTER_MASK) as usize; + unsafe { crate::value::addr_class::try_read_gc_header(raw) } + .is_some_and(|h| h.obj_type == crate::gc::GC_TYPE_ARRAY) +} + +fn js_object_get_own_property_names_shape(obj_value: f64) -> f64 { unsafe { // #2818: ToObject(null/undefined) throws TypeError, matching Node. let obj_jv = crate::JSValue::from_bits(obj_value.to_bits()); @@ -1417,7 +1463,6 @@ pub extern "C" fn js_object_get_own_property_names(obj_value: f64) -> f64 { // property names are the index names `"0".."len-1"` plus `"length"`. // Reading a bogus `keys_array` off their header segfaulted (#800). { - const TAG_TRUE_BITS: u64 = 0x7FFC_0000_0000_0004; let jv = JSValue::from_bits(obj_value.to_bits()); let n: Option = if jv.is_any_string() { let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; @@ -1427,7 +1472,7 @@ pub extern "C" fn js_object_get_own_property_names(obj_value: f64) -> f64 { } _ => Some(0), } - } else if crate::array::js_array_is_array(obj_value).to_bits() == TAG_TRUE_BITS { + } else if real_array_receiver(obj_value) { let ap = extract_obj_ptr(obj_value) as *const crate::array::ArrayHeader; Some(crate::array::js_array_length(ap)) } else { @@ -1435,7 +1480,7 @@ pub extern "C" fn js_object_get_own_property_names(obj_value: f64) -> f64 { }; if let Some(n) = n { let result = crate::array::js_array_alloc(n + 1); - if crate::array::js_array_is_array(obj_value).to_bits() == TAG_TRUE_BITS { + if real_array_receiver(obj_value) { let ap = extract_obj_ptr(obj_value) as *const crate::array::ArrayHeader; for i in 0..n { if super::has_own_helpers::array_own_key_present(ap, { diff --git a/crates/perry-runtime/src/object/field_get_set/enumeration.rs b/crates/perry-runtime/src/object/field_get_set/enumeration.rs index 79c19b8b15..b86c326272 100644 --- a/crates/perry-runtime/src/object/field_get_set/enumeration.rs +++ b/crates/perry-runtime/src/object/field_get_set/enumeration.rs @@ -1043,6 +1043,27 @@ fn registered_buffer_enum(addr: usize, what: MapSetEnum) -> Option<*mut ArrayHea /// Otherwise (the common case), this returns the stored keys array directly. #[no_mangle] pub extern "C" fn js_object_keys(obj: *const ObjectHeader) -> *mut ArrayHeader { + // An elements-backed Array-subclass instance: its present indices come + // first (ascending, as strings), then the shape's own enumerable keys. + // `length` is non-enumerable and not in the shape, so it never appears. + if unsafe { crate::array::subclass_elements::backed(obj as usize) }.is_some() { + let scope = crate::gc::RuntimeHandleScope::new(); + let obj_h = scope.root_raw_const_ptr(obj); + let (shape_keys, obj) = obj_h.across_const::(|| js_object_keys_shape(obj)); + if let Some((_, elements)) = + unsafe { crate::array::subclass_elements::backed(obj as usize) } + { + return unsafe { + crate::array::subclass_elements::prepend_index_keys(elements, shape_keys, false) + }; + } + return shape_keys; + } + js_object_keys_shape(obj) +} + +/// [`js_object_keys`] over the shape alone. +fn js_object_keys_shape(obj: *const ObjectHeader) -> *mut ArrayHeader { // #8149: a registered BUFFER receiver — node `Buffer`, `Uint8Array`, // `ArrayBuffer`, `SharedArrayBuffer` or `DataView`. Asked FIRST, above the // `is_valid_obj_ptr` guard: a `BufferHeader` is not an `ObjectHeader`, and @@ -1405,6 +1426,27 @@ pub(crate) unsafe fn descriptor_marks_non_enumerable( /// Returns an array of the object's field values #[no_mangle] pub extern "C" fn js_object_values(obj: *const ObjectHeader) -> *mut ArrayHeader { + // An elements-backed Array-subclass instance: its present elements come + // first (ascending), then the shape's own enumerable properties. + if unsafe { crate::array::subclass_elements::backed(obj as usize) }.is_some() { + let scope = crate::gc::RuntimeHandleScope::new(); + let obj_h = scope.root_raw_const_ptr(obj); + let (shape_part, obj) = + obj_h.across_const::(|| js_object_values_shape(obj)); + if let Some((_, elements)) = + unsafe { crate::array::subclass_elements::backed(obj as usize) } + { + return unsafe { + crate::array::subclass_elements::prepend_index_values(elements, shape_part) + }; + } + return shape_part; + } + js_object_values_shape(obj) +} + +/// [`js_object_values`] over the shape alone. +fn js_object_values_shape(obj: *const ObjectHeader) -> *mut ArrayHeader { // #8149: a registered BUFFER receiver — node `Buffer`, `Uint8Array`, // `ArrayBuffer`, `SharedArrayBuffer` or `DataView`. Asked FIRST, above the // `is_valid_obj_ptr` guard: a `BufferHeader` is not an `ObjectHeader`, and @@ -1568,6 +1610,27 @@ pub extern "C" fn js_object_values(obj: *const ObjectHeader) -> *mut ArrayHeader /// Returns an array where each element is a 2-element array [key, value] #[no_mangle] pub extern "C" fn js_object_entries(obj: *const ObjectHeader) -> *mut ArrayHeader { + // An elements-backed Array-subclass instance: its present elements come + // first (ascending), then the shape's own enumerable properties. + if unsafe { crate::array::subclass_elements::backed(obj as usize) }.is_some() { + let scope = crate::gc::RuntimeHandleScope::new(); + let obj_h = scope.root_raw_const_ptr(obj); + let (shape_part, obj) = + obj_h.across_const::(|| js_object_entries_shape(obj)); + if let Some((_, elements)) = + unsafe { crate::array::subclass_elements::backed(obj as usize) } + { + return unsafe { + crate::array::subclass_elements::prepend_index_entries(elements, shape_part) + }; + } + return shape_part; + } + js_object_entries_shape(obj) +} + +/// [`js_object_entries`] over the shape alone. +fn js_object_entries_shape(obj: *const ObjectHeader) -> *mut ArrayHeader { // #8149: a registered BUFFER receiver — node `Buffer`, `Uint8Array`, // `ArrayBuffer`, `SharedArrayBuffer` or `DataView`. Asked FIRST, above the // `is_valid_obj_ptr` guard: a `BufferHeader` is not an `ObjectHeader`, and diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs index 1c10f88b50..bde9b72270 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs @@ -30,6 +30,18 @@ pub extern "C" fn js_object_get_field_by_name( if let Some(value) = super::private_member_get_by_name(obj, key) { return JSValue::from_bits(value.to_bits()); } + // An elements-backed Array-subclass instance answers its indices and + // `length` from its store; an absent index falls through to the ordinary + // lookup, which reaches the prototype chain (the shape has no index keys). + if let Some((_, elements)) = unsafe { crate::array::subclass_elements::backed(obj as usize) } { + if let Some(elements_key) = unsafe { crate::array::subclass_elements::key_of_header(key) } { + if let Some(value) = + unsafe { crate::array::subclass_elements::get_by_key(elements, elements_key) } + { + return JSValue::from_bits(value.to_bits()); + } + } + } // #7341: the `.size` arm below calls two helpers that allocate, and every // arm AFTER it dereferences `obj` again. Shadow the parameter so that arm // can republish the post-collection address instead of leaving from-space diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs index 2e1cb30b9d..6e90a4a9ab 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs @@ -11,6 +11,18 @@ pub(crate) fn get_field_by_name_object_tail( obj: *const ObjectHeader, key: *const crate::StringHeader, ) -> JSValue { + // An elements-backed Array-subclass instance answers its indices and + // `length` from its store; an absent index falls through to the ordinary + // lookup, which reaches the prototype chain (the shape has no index keys). + if let Some((_, elements)) = unsafe { crate::array::subclass_elements::backed(obj as usize) } { + if let Some(elements_key) = unsafe { crate::array::subclass_elements::key_of_header(key) } { + if let Some(value) = + unsafe { crate::array::subclass_elements::get_by_key(elements, elements_key) } + { + return JSValue::from_bits(value.to_bits()); + } + } + } // Strip NaN-boxing tags if present (defensive: handle POINTER_TAG, UNDEFINED, NULL, etc.) let obj = { let bits = obj as u64; diff --git a/crates/perry-runtime/src/object/field_get_set/has_property.rs b/crates/perry-runtime/src/object/field_get_set/has_property.rs index 8ebdfc10f1..8da38582cd 100644 --- a/crates/perry-runtime/src/object/field_get_set/has_property.rs +++ b/crates/perry-runtime/src/object/field_get_set/has_property.rs @@ -221,6 +221,15 @@ pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 { } }; let key_val = JSValue::from_bits(key.to_bits()); + if let Some((_, elements)) = crate::array::subclass_elements::backed_value(obj) { + if let Some(elements_key) = crate::array::subclass_elements::key_of_value(key) { + if unsafe { crate::array::subclass_elements::has_own_key(elements, elements_key) } { + return nanbox_true; + } + // Absent index: not own — the ordinary walk continues to the + // prototype chain (the shape carries no index keys). + } + } // ── #6748 fast path: ordinary heap object + string key ──────────────── // One GC-header read classifies the receiver. A `GC_TYPE_OBJECT` cannot diff --git a/crates/perry-runtime/src/object/field_get_set/ic_miss.rs b/crates/perry-runtime/src/object/field_get_set/ic_miss.rs index 1347d7a232..dd0ead0dbd 100644 --- a/crates/perry-runtime/src/object/field_get_set/ic_miss.rs +++ b/crates/perry-runtime/src/object/field_get_set/ic_miss.rs @@ -514,6 +514,22 @@ pub extern "C" fn js_object_get_field_ic_miss( // slot without repeating generic object dispatch. Both arms retain // their established helpers, making this a dispatch short-circuit // rather than a second implementation of either representation. + // An elements-backed Array-subclass instance answers its indices and + // `length` from its store; an absent index falls through to the ordinary + // lookup, which reaches the prototype chain (the shape has no index keys). + if let Some((_, elements)) = + unsafe { crate::array::subclass_elements::backed(obj as usize) } + { + if let Some(elements_key) = + unsafe { crate::array::subclass_elements::key_of_header(key) } + { + if let Some(value) = + unsafe { crate::array::subclass_elements::get_by_key(elements, elements_key) } + { + return value; + } + } + } if unsafe { key_bytes_are(key, b"length") } { match unsafe { gc_type_of(obj) } { Some(crate::gc::GC_TYPE_ARRAY) => { diff --git a/crates/perry-runtime/src/object/field_set_by_name.rs b/crates/perry-runtime/src/object/field_set_by_name.rs index 3b01a5f35c..4447c63345 100644 --- a/crates/perry-runtime/src/object/field_set_by_name.rs +++ b/crates/perry-runtime/src/object/field_set_by_name.rs @@ -51,6 +51,23 @@ pub extern "C" fn js_object_set_field_by_name( // `prototype` property is non-writable, so both ordinary assignment and // a computed static field whose PropertyKey resolves to "prototype" must // fail instead of appending an ordinary shape slot. + // An elements-backed Array-subclass instance stores its indices and + // `length` in its store: no index key ever becomes a shape property. + if let Some((backed_obj, elements)) = + unsafe { crate::array::subclass_elements::backed(obj as usize) } + { + if let Some(elements_key) = unsafe { crate::array::subclass_elements::key_of_header(key) } { + unsafe { + crate::array::subclass_elements::set_by_key( + backed_obj, + elements, + elements_key, + value, + ) + }; + return; + } + } let obj_bits = obj as u64; let normalized_obj = if (obj_bits >> 48) == 0x7FFD { (obj_bits & crate::value::POINTER_MASK) as *mut ObjectHeader diff --git a/crates/perry-runtime/src/object/field_set_by_name/tail.rs b/crates/perry-runtime/src/object/field_set_by_name/tail.rs index 4dbacea616..7c5b1fee44 100644 --- a/crates/perry-runtime/src/object/field_set_by_name/tail.rs +++ b/crates/perry-runtime/src/object/field_set_by_name/tail.rs @@ -40,6 +40,23 @@ pub(crate) fn set_field_by_name_object_tail( key: *const crate::StringHeader, value: f64, ) { + // An elements-backed Array-subclass instance stores its indices and + // `length` in its store: no index key ever becomes a shape property. + if let Some((backed_obj, elements)) = + unsafe { crate::array::subclass_elements::backed(obj as usize) } + { + if let Some(elements_key) = unsafe { crate::array::subclass_elements::key_of_header(key) } { + unsafe { + crate::array::subclass_elements::set_by_key( + backed_obj, + elements, + elements_key, + value, + ) + }; + return; + } + } let scope = crate::gc::RuntimeHandleScope::new(); let obj_handle = scope.root_raw_mut_ptr(obj); let key_handle = scope.root_string_ptr(key); diff --git a/crates/perry-runtime/src/object/meta_accessors.rs b/crates/perry-runtime/src/object/meta_accessors.rs index 1f8d1742d7..bef1298e42 100644 --- a/crates/perry-runtime/src/object/meta_accessors.rs +++ b/crates/perry-runtime/src/object/meta_accessors.rs @@ -34,6 +34,7 @@ pub(crate) unsafe fn object_meta_ensure_for_cell(user_ptr: usize) -> Option<*mut (*meta).array_subclass_dense_slots = 0; (*meta).array_subclass_dense_bounds = 0; (*meta).expando = 0; + (*meta).elements = 0; // GC_STORE_AUDIT(BARRIERED): header-slot store followed by an object-slot // barrier, exactly as `object_meta_ensure` does for an `ObjectHeader`. *slot = meta; @@ -75,6 +76,7 @@ pub(crate) unsafe fn object_meta_ensure(obj: *mut ObjectHeader) -> *mut ObjectMe (*meta).array_subclass_dense_slots = 0; (*meta).array_subclass_dense_bounds = 0; (*meta).expando = 0; + (*meta).elements = 0; // GC_STORE_AUDIT(BARRIERED): meta-record edge is a header-slot store // followed by an object-slot barrier, mirroring `set_object_keys_array`. (*obj).meta = meta; diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index c229a94b5f..c0228ad520 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -75,7 +75,7 @@ mod data_view_registry; mod dataview_proto_thunks; mod date_proto_thunks; mod delete_rest; -mod descriptors; +pub(crate) mod descriptors; mod disposable_proto_thunks; pub(crate) mod exotic_expando; pub(crate) mod field_get_set; @@ -1611,6 +1611,15 @@ pub struct ObjectMeta { /// child edge: it moves with its owner, dies with its owner, and needs no /// address bookkeeping at all. pub expando: u64, + /// Elements backing store of a `class X extends Array` instance: a + /// `GC_TYPE_ARRAY` (`*mut ArrayHeader` bits, 0 = none) holding the + /// instance's indexed elements and `length`, exactly as a plain Array + /// does — so `push`/`pop`/`obj[i]` are element operations instead of + /// property-shape transitions (`array/subclass_elements.rs`). A traced + /// child edge exactly like `spill`: lives and moves with this record. + /// Installed by `js_array_subclass_init` under + /// `array_subclass_elements_enabled()`; never present otherwise. + pub elements: u64, } pub(crate) const OBJECT_META_FLAG_PROTO_OVERRIDE: u64 = 1; @@ -1654,6 +1663,11 @@ pub(crate) unsafe fn object_is_shaped(obj: *const ObjectHeader) -> bool { const _: () = assert!(std::mem::offset_of!(ObjectMeta, spill) == 32); const _: () = assert!(std::mem::offset_of!(ObjectMeta, array_subclass_named_prefix_token) == 48); const _: () = assert!(std::mem::offset_of!(ObjectMeta, array_tail_object_hot) == 56); +// The Array-subclass elements store: codegen's inline `elem.*` tiers load +// `ObjectHeader.meta` then this word (perry-codegen `expr/index_get` and +// `property_get/composed_ics.rs`). Keep in lock-step. +const _: () = assert!(std::mem::offset_of!(ObjectMeta, elements) == 96); +const _: () = assert!(std::mem::offset_of!(ObjectHeader, meta) == 8); const _: () = assert!(std::mem::size_of::() == 8); /// Fetch-or-allocate the per-object meta record. Caller must have already diff --git a/crates/perry-runtime/src/object/object_ops/define_properties.rs b/crates/perry-runtime/src/object/object_ops/define_properties.rs index 8fb6bb9846..5b0d6afe8d 100644 --- a/crates/perry-runtime/src/object/object_ops/define_properties.rs +++ b/crates/perry-runtime/src/object/object_ops/define_properties.rs @@ -12,6 +12,7 @@ use super::*; /// on that so `const x = Object.defineProperties(...)` still binds `x`. #[no_mangle] pub extern "C" fn js_object_define_properties(target: f64, descriptors: f64) -> f64 { + crate::array::subclass_elements::deopt_value(target); // #2817: target must be an object (or class-ref). Node throws // `Object.defineProperties called on non-object` for primitives. // @@ -210,6 +211,7 @@ pub extern "C" fn js_object_define_properties(target: f64, descriptors: f64) -> /// + inherited property dispatch can consult it. #[no_mangle] pub extern "C" fn js_object_set_prototype_of(obj_value: f64, proto: f64) -> f64 { + crate::array::subclass_elements::deopt_value(obj_value); const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; let obj_bits = obj_value.to_bits(); diff --git a/crates/perry-runtime/src/object/object_ops/define_property.rs b/crates/perry-runtime/src/object/object_ops/define_property.rs index 616a30f17e..7f18aaa72e 100644 --- a/crates/perry-runtime/src/object/object_ops/define_property.rs +++ b/crates/perry-runtime/src/object/object_ops/define_property.rs @@ -312,6 +312,14 @@ pub extern "C" fn js_object_define_property( key_value: f64, descriptor_value: f64, ) -> f64 { + // An index or `length` descriptor on an elements-backed Array-subclass + // instance leaves the elements representation for good (the shape-carried + // machinery models descriptors); other keys keep the store. + if crate::array::subclass_elements::backed_value(obj_value).is_some() + && crate::array::subclass_elements::key_of_value(key_value).is_some() + { + crate::array::subclass_elements::deopt_value(obj_value); + } unsafe { // #6748 follow-up: classify the receiver ONCE. A `GC_TYPE_OBJECT` that // is not an exotic cell (RegExp is the one OBJECT-typed exotic) cannot diff --git a/crates/perry-runtime/src/object/object_ops/has_own.rs b/crates/perry-runtime/src/object/object_ops/has_own.rs index ab40384314..61eb98ec69 100644 --- a/crates/perry-runtime/src/object/object_ops/has_own.rs +++ b/crates/perry-runtime/src/object/object_ops/has_own.rs @@ -71,6 +71,17 @@ pub extern "C" fn js_object_has_own(obj_value: f64, key_value: f64) -> f64 { if obj_js.is_undefined() || obj_js.is_null() { super::super::has_own_helpers::throw_to_object_nullish_type_error(); } + if let Some((_, elements)) = crate::array::subclass_elements::backed_value(obj_value) { + if let Some(elements_key) = crate::array::subclass_elements::key_of_value(key_value) { + return f64::from_bits( + if crate::array::subclass_elements::has_own_key(elements, elements_key) { + TAG_TRUE + } else { + TAG_FALSE + }, + ); + } + } // A POINTER_TAG registry handle (zlib stream, fetch Request/Response/ // Headers/Blob, …) is not an address and must never be dereferenced. Its diff --git a/crates/perry-runtime/src/object/object_ops_frozen.rs b/crates/perry-runtime/src/object/object_ops_frozen.rs index 700c1b646c..248510d3b8 100644 --- a/crates/perry-runtime/src/object/object_ops_frozen.rs +++ b/crates/perry-runtime/src/object/object_ops_frozen.rs @@ -128,6 +128,7 @@ unsafe fn test_integrity_level_proxy(obj_value: f64, frozen: bool) -> bool { #[no_mangle] pub extern "C" fn js_object_freeze(obj_value: f64) -> f64 { + crate::array::subclass_elements::deopt_value(obj_value); if crate::proxy::js_proxy_is_proxy(obj_value) != 0 { return unsafe { set_integrity_level_proxy(obj_value, /*frozen=*/ true) @@ -236,6 +237,7 @@ pub extern "C" fn js_object_freeze(obj_value: f64) -> f64 { /// existing key. Writable is preserved (sealed ≠ frozen). Returns the object. #[no_mangle] pub extern "C" fn js_object_seal(obj_value: f64) -> f64 { + crate::array::subclass_elements::deopt_value(obj_value); if crate::proxy::js_proxy_is_proxy(obj_value) != 0 { return unsafe { set_integrity_level_proxy(obj_value, /*frozen=*/ false) @@ -339,6 +341,7 @@ pub extern "C" fn js_object_seal(obj_value: f64) -> f64 { /// Object.preventExtensions(obj) — sets the no-extend flag. Returns the object. #[no_mangle] pub extern "C" fn js_object_prevent_extensions(obj_value: f64) -> f64 { + crate::array::subclass_elements::deopt_value(obj_value); // A Proxy is a small registered id, not a heap object — `extract_obj_ptr` // yields the fake pointer and `gc_header_for` would deref unmapped memory. // Route through the `[[PreventExtensions]]` trap; per spec throw a TypeError diff --git a/crates/perry-runtime/src/object/polymorphic_index.rs b/crates/perry-runtime/src/object/polymorphic_index.rs index 1248757dfa..accf224d09 100644 --- a/crates/perry-runtime/src/object/polymorphic_index.rs +++ b/crates/perry-runtime/src/object/polymorphic_index.rs @@ -485,6 +485,15 @@ 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. // Keep the receiver/key alive because the setter can allocate and the + // An Array-subclass instance with an in-bounds or appending index: + // the dense/elements store takes it without minting a key string + // (the keyed path below allocates one per store — on the wolf-ecs + // `packed[sparse[x]] = last` that was an allocation per swap). + if let Some(index) = numeric_key_u32_index(idx) { + if crate::array::array_subclass_fast_index_set(boxed, index, value) { + return; + } + } // Array-subclass post-step below must observe their evacuated values. let scope = crate::gc::RuntimeHandleScope::new(); let recv_h = scope.root_nanbox_f64(boxed);