diff --git a/crates/perry-codegen/src/expr/array_push.rs b/crates/perry-codegen/src/expr/array_push.rs index 6fd8f1a0bc..cb97ddb3c0 100644 --- a/crates/perry-codegen/src/expr/array_push.rs +++ b/crates/perry-codegen/src/expr/array_push.rs @@ -256,7 +256,7 @@ fn emit_array_handle_length( } let blk = ctx.block(); let len_i32 = blk.call(I32, "js_array_length", &[(I64, array_handle)]); - blk.sitofp(I32, &len_i32, DOUBLE) + blk.uitofp(I32, &len_i32, DOUBLE) } fn emit_array_box_length(ctx: &mut FnCtx<'_>, array_box: &str, value_discarded: bool) -> String { @@ -489,7 +489,7 @@ fn lower_array_push_spec_order( let recv_handle = unbox_to_i64(blk, &recv_box); let new_handle = blk.call( I64, - "js_array_push_f64", + "js_array_push_f64_spec", &[(I64, &recv_handle), (DOUBLE, &v)], ); let new_box = nanbox_pointer_inline(blk, &new_handle); @@ -725,7 +725,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr, value_discarded: bool) -> let arr_handle = unbox_to_i64(blk, &arr_box); let new_handle = blk.call( I64, - "js_array_push_f64", + "js_array_push_f64_spec", &[(I64, &arr_handle), (DOUBLE, &v)], ); let new_box = nanbox_pointer_inline(blk, &new_handle); @@ -741,7 +741,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr, value_discarded: bool) -> ctx.record_lowered_value_with_access_mode_and_facts( "NumericArrayPush", Some(*array_id), - "js_array_push_f64", + "js_array_push_f64_spec", &fallback, Some(BoundsState::Unknown), None, @@ -872,7 +872,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr, value_discarded: bool) -> let blk = ctx.block(); let new_handle = blk.call( I64, - "js_array_push_f64", + "js_array_push_f64_spec", &[(I64, &arr_handle), (DOUBLE, &v)], ); let new_box = nanbox_pointer_inline(blk, &new_handle); @@ -960,6 +960,15 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr, value_discarded: bool) -> let integrity_bits = blk.and(I16, &obj_flags, "1031"); blk.icmp_eq(I16, &integrity_bits, "0") }; + // A sticky runtime byte records indexed properties on + // Array/Object.prototype (and custom Array prototypes). + // Such a property can intercept push with an inherited + // setter, so the raw append is valid only while the default + // prototype chain remains pristine. + let invalidated = + blk.load_volatile(I8, "@PERRY_ARRAY_INDEX_FAST_PATH_INVALIDATED"); + let prototype_clean = blk.icmp_eq(I8, &invalidated, "0"); + let clean = blk.and(I1, &clean, &prototype_clean); let length = blk.safe_load_i32_from_ptr(&arr_handle); let cap_addr = blk.add(I64, &arr_handle, "4"); let cap_ptr = blk.inttoptr(I64, &cap_addr); @@ -1093,7 +1102,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr, value_discarded: bool) -> let blk = ctx.block(); let new_handle = blk.call( I64, - "js_array_push_f64", + "js_array_push_f64_spec", &[(I64, &arr_handle), (DOUBLE, &v)], ); let new_box = nanbox_pointer_inline(blk, &new_handle); @@ -1114,7 +1123,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr, value_discarded: bool) -> let arr_handle = unbox_to_i64(blk, &arr_box); let new_handle = blk.call( I64, - "js_array_push_f64", + "js_array_push_f64_spec", &[(I64, &arr_handle), (DOUBLE, &v)], ); let new_box = nanbox_pointer_inline(blk, &new_handle); diff --git a/crates/perry-codegen/src/expr/array_push_guard_tests.rs b/crates/perry-codegen/src/expr/array_push_guard_tests.rs index a20faba1ee..bc742e9bb5 100644 --- a/crates/perry-codegen/src/expr/array_push_guard_tests.rs +++ b/crates/perry-codegen/src/expr/array_push_guard_tests.rs @@ -447,7 +447,7 @@ fn a_metadata_selected_add_keeps_the_runtime_number_guard() { assert!( generic.contains("call i32 @js_typed_feedback_numeric_array_push_guard") && generic.contains("call i64 @js_array_numeric_push_f64_unboxed") - && generic.contains("call i64 @js_array_push_f64"), + && generic.contains("call i64 @js_array_push_f64_spec"), "a declared-number addition must validate the live value and retain the generic push fallback:\n{generic}" ); assert!( diff --git a/crates/perry-codegen/src/expr/arrays_finds.rs b/crates/perry-codegen/src/expr/arrays_finds.rs index 1e3723419c..ee3c5d27bd 100644 --- a/crates/perry-codegen/src/expr/arrays_finds.rs +++ b/crates/perry-codegen/src/expr/arrays_finds.rs @@ -1251,7 +1251,7 @@ pub(crate) fn lower( crate::lower_array_method::emit_grow_mutator_writeback(ctx, *array_id, &new_box)?; let blk = ctx.block(); let len_i32 = blk.call(I32, "js_array_length", &[(I64, &new_handle)]); - let len_f64 = blk.sitofp(I32, &len_i32, DOUBLE); + let len_f64 = blk.uitofp(I32, &len_i32, DOUBLE); Ok(len_f64) } diff --git a/crates/perry-codegen/src/expr/logical_collections.rs b/crates/perry-codegen/src/expr/logical_collections.rs index 1ba49da27a..590252b274 100644 --- a/crates/perry-codegen/src/expr/logical_collections.rs +++ b/crates/perry-codegen/src/expr/logical_collections.rs @@ -469,6 +469,15 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { &[(DOUBLE, &recv_box), (DOUBLE, &sep)], ) } + // flat(depth?): undefined selects the default depth 1. + "flat" => { + let depth = nth(0).unwrap_or_else(undef); + blk.call( + DOUBLE, + "js_arraylike_flat", + &[(DOUBLE, &recv_box), (DOUBLE, &depth)], + ) + } // slice(start?, end?): has-flags distinguish omitted from undefined. "slice" => { let (has_start, start) = match nth(0) { diff --git a/crates/perry-codegen/src/lower_array_method.rs b/crates/perry-codegen/src/lower_array_method.rs index 4440152585..dbd693eb52 100644 --- a/crates/perry-codegen/src/lower_array_method.rs +++ b/crates/perry-codegen/src/lower_array_method.rs @@ -933,7 +933,7 @@ pub(crate) fn lower_array_method( let len_i32 = ctx .block() .call(I32, "js_array_length", &[(I64, &new_handle)]); - Ok(ctx.block().sitofp(I32, &len_i32, DOUBLE)) + Ok(ctx.block().uitofp(I32, &len_i32, DOUBLE)) } // Issue #655 (chained-receiver path): without this arm, a // chained `obj.field.splice(...)` resolved through `is_array_expr` diff --git a/crates/perry-codegen/src/lower_call/native/native_instance_branch.rs b/crates/perry-codegen/src/lower_call/native/native_instance_branch.rs index c6b44be456..20f0a670d3 100644 --- a/crates/perry-codegen/src/lower_call/native/native_instance_branch.rs +++ b/crates/perry-codegen/src/lower_call/native/native_instance_branch.rs @@ -290,7 +290,7 @@ } let blk = ctx.block(); let len_i32 = blk.call(I32, "js_array_length", &[(I64, &new_handle)]); - return Ok(blk.sitofp(I32, &len_i32, DOUBLE)); + return Ok(blk.uitofp(I32, &len_i32, DOUBLE)); } if module == "array" && (method == "push_single" || method == "push") { @@ -376,7 +376,7 @@ } let blk = ctx.block(); let len_i32 = blk.call(I32, "js_array_length", &[(I64, &new_handle)]); - return Ok(blk.sitofp(I32, &len_i32, DOUBLE)); + return Ok(blk.uitofp(I32, &len_i32, DOUBLE)); } if module == "array" && (method == "pop_back" || method == "pop") { diff --git a/crates/perry-codegen/src/native_value/verify/raw_f64.rs b/crates/perry-codegen/src/native_value/verify/raw_f64.rs index 5746ff8f91..51d2644550 100644 --- a/crates/perry-codegen/src/native_value/verify/raw_f64.rs +++ b/crates/perry-codegen/src/native_value/verify/raw_f64.rs @@ -72,7 +72,14 @@ pub(crate) fn validate_js_value_bits_record(record: &NativeRepRecord, errors: &m pub(crate) fn raw_f64_dynamic_fallback_record(record: &NativeRepRecord) -> bool { matches!( (record.expr_kind.as_str(), record.consumer.as_str()), + // #8858 added the spec-compliant observable push helper alongside the + // original own-property one; BOTH are still emitted (the original at + // expr/static_method.rs, expr/static_field_meta.rs and + // rooting/temp_root.rs), so both must keep the raw_f64_layout + // requirement. Replacing the entry rather than adding to it silently + // dropped it for `js_array_push_f64`. ("NumericArrayPush", "js_array_push_f64") + | ("NumericArrayPush", "js_array_push_f64_spec") | ( "NumericArrayIndexGet", "js_typed_feedback_array_index_get_fallback_boxed" diff --git a/crates/perry-codegen/src/runtime_decls/arrays.rs b/crates/perry-codegen/src/runtime_decls/arrays.rs index 5449b0d3c0..6d55c3d482 100644 --- a/crates/perry-codegen/src/runtime_decls/arrays.rs +++ b/crates/perry-codegen/src/runtime_decls/arrays.rs @@ -13,7 +13,7 @@ use super::*; /// - `js_array_push_f64(arr, value) -> arr*` — push element, may realloc /// and return a NEW pointer that the caller must use going forward /// - `js_array_get_f64(arr, index) -> f64` — read typed-number element -/// - `js_array_length(arr) -> u32` — length (u32, sitofp'd to double for +/// - `js_array_length(arr) -> u32` — length (u32, uitofp'd to double for /// our number ABI) pub fn declare_phase_b_arrays(module: &mut LlModule) { module.declare_function("js_array_alloc", I64, &[I32]); @@ -42,6 +42,7 @@ pub fn declare_phase_b_arrays(module: &mut LlModule) { // blob_len). Returns the nanboxed JS value (a fresh, mutable array). module.declare_function("js_value_from_const_descriptor", DOUBLE, &[PTR, I32]); module.declare_function("js_array_push_f64", I64, &[I64, DOUBLE]); + module.declare_function("js_array_push_f64_spec", I64, &[I64, DOUBLE]); module.declare_function("js_array_push_guard", VOID, &[I64]); module.declare_function("js_array_push_hole", I64, &[I64]); module.declare_function("js_array_numeric_push_f64_unboxed", I64, &[I64, DOUBLE]); @@ -279,6 +280,7 @@ pub fn declare_phase_b_arrays(module: &mut LlModule) { ); module.declare_function("js_arraylike_at", DOUBLE, &[DOUBLE, DOUBLE]); module.declare_function("js_arraylike_join", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_arraylike_flat", DOUBLE, &[DOUBLE, DOUBLE]); module.declare_function( "js_arraylike_slice", DOUBLE, diff --git a/crates/perry-hir/src/analysis/value_types.rs b/crates/perry-hir/src/analysis/value_types.rs index 5db7691b74..f0296646b5 100644 --- a/crates/perry-hir/src/analysis/value_types.rs +++ b/crates/perry-hir/src/analysis/value_types.rs @@ -1590,7 +1590,7 @@ fn infer_arraylike_method_type( .map(|callback| function_return_type_from_expr(callback, env)) .unwrap_or(Type::Any), )), - "filter" | "slice" | "splice" => { + "filter" | "slice" | "splice" | "flat" => { Type::Array(Box::new(array_element_type_from_expr(receiver, env))) } "concat" => Type::Array(Box::new(Type::Any)), diff --git a/crates/perry-hir/src/lower/expr_assign.rs b/crates/perry-hir/src/lower/expr_assign.rs index f5f1f74f8f..a34f3055f1 100644 --- a/crates/perry-hir/src/lower/expr_assign.rs +++ b/crates/perry-hir/src/lower/expr_assign.rs @@ -457,6 +457,22 @@ pub(crate) fn lower_ident_assignment( throw_type_error_const_assignment(&name), ])); } + // A JavaScript `var` inferred from an array literal can later hold an + // ordinary object. Lowering is source-ordered, so revoke the local's + // Array specialization as soon as a certainly non-array object is + // assigned; subsequent `x.unshift(...)` / indexed reads must use + // dynamic dispatch rather than ArrayHeader intrinsics. The module-wide + // type-widening pass mirrors this decision onto the already-emitted + // `Stmt::Let` declaration for codegen. + let invalidate_array_type = matches!( + ctx.lookup_local_type(&name), + Some(Type::Array(_)) | Some(Type::Tuple(_)) + ) && assignment_is_certainly_non_array_object(&value); + if invalidate_array_type { + if let Some(ty) = ctx.locals.lookup_type_mut(&name) { + *ty = Type::Any; + } + } let local_set = Expr::LocalSet(id, value); let mirrors_script_var = super::lower_expr::global_script_this_enabled() && ctx.script_var_decl_names.contains(&name) @@ -540,6 +556,19 @@ pub(crate) fn lower_ident_assignment( } } +fn assignment_is_certainly_non_array_object(value: &Expr) -> bool { + matches!( + value, + Expr::This + | Expr::Object(_) + | Expr::ObjectSpread { .. } + | Expr::ObjectAssign { .. } + | Expr::Closure { .. } + | Expr::Null + | Expr::Undefined + ) || matches!(value, Expr::New { class_name, .. } if class_name != "Array") +} + fn lower_assignment_target( ctx: &mut LoweringContext, target: &ast::AssignTarget, diff --git a/crates/perry-hir/src/lower/expr_call/intrinsics/apply_call.rs b/crates/perry-hir/src/lower/expr_call/intrinsics/apply_call.rs index f8c1952fba..afae5a19f3 100644 --- a/crates/perry-hir/src/lower/expr_call/intrinsics/apply_call.rs +++ b/crates/perry-hir/src/lower/expr_call/intrinsics/apply_call.rs @@ -685,6 +685,7 @@ fn try_arraylike_receiver_method( | "lastIndexOf" | "includes" | "slice" + | "flat" | "at" | "join" // Generic mutators with dedicated runtime engines (#4597 @@ -724,7 +725,7 @@ fn try_arraylike_receiver_method( // materialize-then-call behavior. `Expr::ArrayFromArrayLikeHoley` keeps // absent indexed keys as holes (vs `Array.from({ length })` creating // present undefined slots), so the flatMap callback doesn't visit holes. - // Everything else (mutators, flat, etc.) bails BEFORE lowering the receiver + // Everything else bails BEFORE lowering the receiver // so unrelated shapes keep the existing member-call behavior. if method != "flatMap" { return Ok(None); diff --git a/crates/perry-hir/src/lower/expr_member/member_tail.rs b/crates/perry-hir/src/lower/expr_member/member_tail.rs index 74e8d1370e..2b7f0733c0 100644 --- a/crates/perry-hir/src/lower/expr_member/member_tail.rs +++ b/crates/perry-hir/src/lower/expr_member/member_tail.rs @@ -409,11 +409,24 @@ pub(crate) fn lower_member_tail( // `function_prototype_fallback_target`). let receiver_is_regexp_ctor = property == "RegExp"; let receiver_is_function_ctor = property == "Function"; + // #5898: unknown Array statics are ordinary property reads + // on the constructor object. Preserve that receiver so the + // runtime can walk Array -> Function.prototype (for example + // after `Function.prototype.myproperty = 1`). Known Array + // statics keep the collapsed expression shape used by their + // intrinsic call paths. + let receiver_is_array_ctor_unknown_static = property == "Array" + && !outer_static_member + .map(|member| { + crate::analysis::is_builtin_static_function_member(property, member) + }) + .unwrap_or(false); if !outer_is_prototype_or_proto && !outer_is_constructor_property && !receiver_is_namespace_value && !receiver_is_regexp_ctor && !receiver_is_function_ctor + && !receiver_is_array_ctor_unknown_static && !outer_is_websocket_static && !outer_is_reified_object_static_value && !outer_is_reified_builtin_static_value diff --git a/crates/perry-hir/src/lower/tests.rs b/crates/perry-hir/src/lower/tests.rs index fef57b543d..3ac5e84935 100644 --- a/crates/perry-hir/src/lower/tests.rs +++ b/crates/perry-hir/src/lower/tests.rs @@ -25,6 +25,32 @@ fn test_lower_define_and_lookup_local() { assert_eq!(ctx.lookup_local_type("x"), Some(&Type::Number)); } +#[test] +fn array_inference_is_revoked_after_plain_object_assignment() { + let source = r#" + var value = [1]; + value.unshift(0); + value = { 0: 1 }; + value.unshift(0); + "#; + let module = + perry_parser::parse_typescript(source, "array-reassign.js").expect("source parses"); + let hir = + super::lower_module(&module, "array-reassign", "array-reassign.js").expect("source lowers"); + let value_type = hir.init.iter().find_map(|stmt| match stmt { + Stmt::Let { name, ty, .. } if name == "value" => Some(ty), + _ => None, + }); + assert_eq!(value_type, Some(&Type::Any)); + + let dump = format!("{hir:?}"); + assert_eq!( + dump.matches("ArrayUnshift").count(), + 1, + "only the call before the object reassignment may stay specialized: {dump}" + ); +} + #[test] fn local_declaration_span_survives_ast_to_hir_lowering() { let source = "function build() {\n const boxed = makeValue();\n return boxed;\n}\n"; diff --git a/crates/perry-hir/src/lower/type_widening.rs b/crates/perry-hir/src/lower/type_widening.rs index 3a0ecaf167..2bfa986419 100644 --- a/crates/perry-hir/src/lower/type_widening.rs +++ b/crates/perry-hir/src/lower/type_widening.rs @@ -58,10 +58,28 @@ struct WidenSets { object_like: HashSet, /// Assigned a primitive that is not a JS number → widen a numeric declared type. non_number_primitive: HashSet, + /// Assigned a certainly non-array object -> revoke Array/Tuple intrinsics. + non_array_object: HashSet, +} + +fn rhs_certainly_non_array_object(expr: &Expr) -> bool { + matches!( + expr, + Expr::This + | Expr::Object(_) + | Expr::ObjectSpread { .. } + | Expr::ObjectAssign { .. } + | Expr::Closure { .. } + | Expr::Null + | Expr::Undefined + ) || matches!(expr, Expr::New { class_name, .. } if class_name != "Array") } fn visit_expr(expr: &Expr, out: &mut WidenSets, env: &HirTypeEnv) { if let Expr::LocalSet(id, rhs) = expr { + if rhs_certainly_non_array_object(rhs) { + out.non_array_object.insert(*id); + } if rhs_certainly_object_like(rhs, env) { out.object_like.insert(*id); } else if rhs_certainly_non_number_primitive(rhs, env) { @@ -174,6 +192,7 @@ fn widen_lets_stmt(stmt: &mut Stmt, sets: &WidenSets) { sets.object_like.contains(id) || sets.non_number_primitive.contains(id) } Type::String | Type::Boolean => sets.object_like.contains(id), + Type::Array(_) | Type::Tuple(_) => sets.non_array_object.contains(id), _ => false, }; if widen { @@ -276,7 +295,10 @@ impl TypeWidening { } pub(crate) fn apply(&self, stmts: &mut [Stmt]) { - if self.sets.object_like.is_empty() && self.sets.non_number_primitive.is_empty() { + if self.sets.object_like.is_empty() + && self.sets.non_number_primitive.is_empty() + && self.sets.non_array_object.is_empty() + { return; } for s in stmts { @@ -464,6 +486,36 @@ mod tests { assert_eq!(let_ty(&module.init, 1), &Type::Any); } + #[test] + fn widens_array_local_assigned_plain_object() { + let mut module = Module::new("type-widening-test"); + module.init = vec![ + Stmt::Let { + id: 1, + name: "value".to_string(), + ty: Type::Array(Box::new(Type::Number)), + mutable: true, + init: Some(Expr::Array(vec![Expr::Number(1.0)])), + }, + Stmt::Expr(Expr::LocalSet( + 1, + Box::new(Expr::New { + class_name: "__AnonShape_test".to_string(), + args: vec![], + type_args: vec![], + byte_offset: 0, + cap_args_appended: 0, + }), + )), + ]; + + let mut widening = TypeWidening::from_module(&module); + widening.collect(&module.init); + widening.apply(&mut module.init); + + assert_eq!(let_ty(&module.init, 1), &Type::Any); + } + #[test] fn widens_primitive_local_assigned_object_null_union() { let mut module = Module::new("type-widening-test"); diff --git a/crates/perry-runtime/src/array/alloc.rs b/crates/perry-runtime/src/array/alloc.rs index 058da970ba..0d01c231f7 100644 --- a/crates/perry-runtime/src/array/alloc.rs +++ b/crates/perry-runtime/src/array/alloc.rs @@ -176,7 +176,23 @@ pub(crate) fn js_array_alloc_with_length_exact(capacity: u32) -> *mut ArrayHeade pub extern "C" fn js_array_constructor_single(value: f64) -> *mut ArrayHeader { if let Some(number) = value_bits_to_number(value.to_bits()) { let length = array_length_from_number_or_throw(number); - let arr = js_array_alloc_with_length(length); + // ArrayCreate records the requested uint32 length; it does not require + // a dense backing store for every hole. Allocating `new Array(2^32-1)` + // as one contiguous element buffer tried to reserve roughly 32 GiB and + // either hung or exhausted the process. Keep ordinary-sized arrays + // dense, but represent a large fresh holey array exactly like a later + // sparse length extension: a small backing store plus the full logical + // length. Indexed reads already treat `index >= capacity` as a hole. + const MAX_FRESH_DENSE_ARRAY_LENGTH: u32 = 1_000_000; + let arr = if length > MAX_FRESH_DENSE_ARRAY_LENGTH { + let arr = js_array_alloc(0); + unsafe { + (*arr).length = length; + } + arr + } else { + js_array_alloc_with_length(length) + }; if length > 0 { // #6011: user-facing `new Array(n)` — every slot is TAG_HOLE, so // the raw-f64-or-holes invariant holds by construction and the diff --git a/crates/perry-runtime/src/array/flat_clone.rs b/crates/perry-runtime/src/array/flat_clone.rs index 3fd492ecbb..3d83e99c5f 100644 --- a/crates/perry-runtime/src/array/flat_clone.rs +++ b/crates/perry-runtime/src/array/flat_clone.rs @@ -280,11 +280,29 @@ pub extern "C" fn js_array_flat_depth(arr: *const ArrayHeader, depth: f64) -> *m } else { depth as u32 }; - unsafe { - let mut result = js_array_alloc(0); - result = js_array_flat_into(result, arr, levels); - result - } + let scope = crate::gc::RuntimeHandleScope::new(); + let arr_handle = scope.root_raw_mut_ptr(arr as *mut ArrayHeader); + let result = js_array_alloc(0); + unsafe { js_array_flat_into(result, arr_handle.get_raw_mut_ptr::(), levels) } +} + +/// Generic `Array.prototype.flat.call(receiver, depth?)` entry. The receiver is +/// first converted with ToObject/LengthOfArrayLike while preserving holes, then +/// flattened as an Array. `undefined` (whether omitted or explicitly supplied) +/// selects the specification default depth of one. +#[no_mangle] +pub extern "C" fn js_arraylike_flat(receiver: f64, depth: f64) -> f64 { + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver_handle = scope.root_nanbox_f64(receiver); + let source = js_array_from_arraylike_holey_value(receiver_handle.get_nanbox_f64()); + let source_handle = scope.root_raw_mut_ptr(source); + let depth = if depth.to_bits() == crate::value::TAG_UNDEFINED { + 1.0 + } else { + crate::builtins::js_number_coerce(depth) + }; + let result = js_array_flat_depth(source_handle.get_raw_mut_ptr::(), depth); + f64::from_bits(crate::value::JSValue::pointer(result as *const u8).bits()) } /// Recursive worker for `js_array_flat_depth`. Returns the (possibly @@ -295,24 +313,35 @@ unsafe fn js_array_flat_into( src: *const ArrayHeader, depth_left: u32, ) -> *mut ArrayHeader { - let len = (*src).length as usize; - let elements = (src as *const u8).add(std::mem::size_of::()) as *const f64; + // A push into `result` can allocate and move `src`; keep the source rooted + // and derive its live address for every observable indexed operation. + let scope = crate::gc::RuntimeHandleScope::new(); + let src_handle = scope.root_raw_mut_ptr(src as *mut ArrayHeader); + let len = (*src_handle.get_raw_mut_ptr::()).length as usize; + let exotic = crate::array::array_iteration_is_exotic(src); for i in 0..len { - let element = *elements.add(i); - // Per ECMAScript FlattenIntoArray, holes are absent (HasProperty is - // false) and are skipped, not copied as `null`/`undefined`. - if element.to_bits() == crate::value::TAG_HOLE { - continue; - } + let live_src = src_handle.get_raw_mut_ptr::(); + let element = if exotic { + if !crate::array::array_spec_has_index(live_src, i as u32) { + continue; + } + crate::array::array_spec_get(live_src, i as u32) + } else { + let elements = + (live_src as *const u8).add(std::mem::size_of::()) as *const f64; + let element = *elements.add(i); + // Per FlattenIntoArray, holes are absent and skipped. + if element.to_bits() == crate::value::TAG_HOLE { + continue; + } + element + }; let mut pushed = false; if depth_left > 0 { let sub_arr = flattenable_array_ptr(element); if !sub_arr.is_null() { - let sub_len = (*sub_arr).length as usize; - if sub_len <= 1_000_000 { - result = js_array_flat_into(result, sub_arr, depth_left - 1); - pushed = true; - } + result = js_array_flat_into(result, sub_arr, depth_left - 1); + pushed = true; } } if !pushed { diff --git a/crates/perry-runtime/src/array/forwarding_tests.rs b/crates/perry-runtime/src/array/forwarding_tests.rs index d0e14222ec..05623d23fc 100644 --- a/crates/perry-runtime/src/array/forwarding_tests.rs +++ b/crates/perry-runtime/src/array/forwarding_tests.rs @@ -46,6 +46,35 @@ fn growth_of_old_array_keeps_forwarding_target_out_of_copying_nursery() { ); } +#[test] +fn growth_transfers_array_descriptor_side_tables() { + let _triggers = crate::gc::GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let initial = js_array_alloc_literal(1); + let first = js_array_grow(initial, 2); + crate::object::set_accessor_descriptor( + first as usize, + "1".to_string(), + crate::object::AccessorDescriptor { get: 42, set: 84 }, + ); + crate::object::set_property_attrs( + first as usize, + "1".to_string(), + crate::object::PropertyAttrs::new(false, false, true), + ); + + let second = js_array_grow(first, 3); + + let accessor = crate::object::get_accessor_descriptor(second as usize, "1") + .expect("accessor descriptor must follow array growth"); + assert_eq!(accessor.get, 42); + assert_eq!(accessor.set, 84); + let attrs = crate::object::get_property_attrs(second as usize, "1") + .expect("property attributes must follow array growth"); + assert!(!attrs.writable()); + assert!(!attrs.enumerable()); + assert!(attrs.configurable()); +} + #[test] fn install_array_growth_forwarding_with_installs_stub_for_injected_header() { // Actual low-address classification is covered by diff --git a/crates/perry-runtime/src/array/from_concat.rs b/crates/perry-runtime/src/array/from_concat.rs index f646c45898..ef18dc8ff5 100644 --- a/crates/perry-runtime/src/array/from_concat.rs +++ b/crates/perry-runtime/src/array/from_concat.rs @@ -411,13 +411,7 @@ fn jsv_is_array(value: f64) -> bool { /// user closures, so a plain `function` declaration reads as a constructor, /// matching how Perry models user functions). fn is_constructor_value(value: f64) -> bool { - // A user `class` is an INT32-tagged class reference, not a closure pointer, - // so `is_callable` misses it; recognize it directly. - if crate::object::class_ref_id(value).is_some() { - return true; - } - crate::collection_iter::is_callable(value) - && !crate::object::builtin_closure_is_non_constructable_value(value) + crate::object::js_value_is_constructor(value) } /// Build a fresh `{value, writable, enumerable, configurable}` (all true) diff --git a/crates/perry-runtime/src/array/generic.rs b/crates/perry-runtime/src/array/generic.rs index 4d987c5652..0865d5ea47 100644 --- a/crates/perry-runtime/src/array/generic.rs +++ b/crates/perry-runtime/src/array/generic.rs @@ -309,7 +309,12 @@ pub(super) fn al_length(recv: f64) -> i64 { // `Object.prototype.length = 2` (test262 sort/S15.4.4.11_A6_T2, // splice/S15.4.4.12_A4_T1) resolves only when there is no own // property at all. - if len_val.to_bits() == TAG_UNDEFINED && !own_present { + // The fast own-field getter uses a numeric zero miss sentinel + // for some empty anonymous shapes. Existence, not that returned + // payload, decides whether Get must continue up the prototype + // chain; otherwise `{}` incorrectly hides + // `Object.prototype.length` with a fabricated own zero. + if !own_present { len_val = object_get_named_property_chain(raw_addr, "length"); // The recorded/default proto tables may resolve a DIFFERENT // cell than the user-visible `Object.prototype` (read off @@ -361,6 +366,11 @@ fn canonical_object_prototype_named_get(name: &str) -> f64 { return undef(); } let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let proto_bits = f64::from_bits(JSValue::pointer(proto_v.as_pointer::()).bits()); + let key_bits = f64::from_bits(JSValue::string_ptr(key).bits()); + if crate::object::js_object_has_own(proto_bits, key_bits).to_bits() != TAG_TRUE { + return undef(); + } crate::object::js_object_get_field_by_name_f64( proto_v.as_pointer::(), key, @@ -1535,7 +1545,7 @@ pub(super) unsafe fn real_array_mutator( crate::array::guard_writable_length(arr); let mut a = arr; for i in 0..args_len { - a = crate::array::js_array_push_f64(a, arg_or_undef(args_ptr, args_len, i)); + a = crate::array::js_array_push_f64_spec(a, arg_or_undef(args_ptr, args_len, i)); } crate::array::js_array_length(a) as f64 } diff --git a/crates/perry-runtime/src/array/generic_object.rs b/crates/perry-runtime/src/array/generic_object.rs index e1a77cbc01..defb073f74 100644 --- a/crates/perry-runtime/src/array/generic_object.rs +++ b/crates/perry-runtime/src/array/generic_object.rs @@ -205,6 +205,12 @@ pub(crate) fn object_shift(recv: f64) -> f64 { /// length. pub(super) fn object_push(recv: f64, args_ptr: *const f64, args_len: usize) -> f64 { let len = al_length(recv); + const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991; + if len > MAX_SAFE_INTEGER - args_len.min(MAX_SAFE_INTEGER as usize) as i64 { + crate::collection_iter::throw_type_error( + "Pushing elements would exceed the maximum safe integer length", + ); + } for i in 0..args_len { al_set(recv, len + i as i64, arg_at(args_ptr, args_len, i)); } @@ -218,6 +224,12 @@ pub(super) fn object_push(recv: f64, args_ptr: *const f64, args_len: usize) -> f pub(super) fn object_unshift(recv: f64, args_ptr: *const f64, args_len: usize) -> f64 { let len = al_length(recv); let count = args_len as i64; + const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991; + if len > MAX_SAFE_INTEGER - count { + crate::collection_iter::throw_type_error( + "Unshifting elements would exceed the maximum safe integer length", + ); + } if count > 0 { // Move existing elements up by `count`, high index first so we don't // clobber not-yet-moved slots. diff --git a/crates/perry-runtime/src/array/indexing.rs b/crates/perry-runtime/src/array/indexing.rs index efde64e781..04ae28e988 100644 --- a/crates/perry-runtime/src/array/indexing.rs +++ b/crates/perry-runtime/src/array/indexing.rs @@ -385,6 +385,82 @@ pub(crate) fn array_spec_get(arr: *const ArrayHeader, index: u32) -> f64 { } } +/// Spec `Set(O, ToString(index), value, true)` for an Array receiver. Unlike +/// the internal dense setter, this observes an inherited indexed accessor +/// before creating an own element. Array mutators use it on their exotic path +/// because a prototype setter may mutate the receiver (including freezing it +/// or making `length` non-writable) before the mutator's final length Set. +pub(crate) fn array_spec_set(arr: *mut ArrayHeader, index: u32, value: f64) -> *mut ArrayHeader { + let arr = clean_arr_ptr_mut(arr); + if arr.is_null() { + return arr; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let arr_handle = scope.root_raw_mut_ptr(arr); + let value_handle = scope.root_nanbox_f64(value); + let receiver = + || crate::value::js_nanbox_pointer(arr_handle.get_raw_mut_ptr::() as i64); + let key = index.to_string(); + + unsafe { + if array_has_own_index(arr_handle.get_raw_mut_ptr::(), index) { + return js_array_set_f64_extend_strict( + arr_handle.get_raw_mut_ptr::(), + index, + value_handle.get_nanbox_f64(), + ); + } + + let mut inherited_owner = + array_custom_array_prototype(arr_handle.get_raw_mut_ptr::()) + .filter(|proto| array_has_own_index(*proto, index)) + .map(|proto| proto as usize) + .unwrap_or(0); + if inherited_owner == 0 { + let proto = array_prototype_addr(); + inherited_owner = if proto != 0 + && proto != arr_handle.get_raw_mut_ptr::() as usize + && array_has_own_index(proto as *const ArrayHeader, index) + { + proto + } else if object_prototype_has_index_flag() + && crate::array::object_prototype_has_index_prop(index) + { + object_prototype_addr() + } else { + 0 + }; + } + + if inherited_owner != 0 { + if let Some(accessor) = crate::object::get_accessor_descriptor(inherited_owner, &key) { + if accessor.set == 0 { + crate::collection_iter::throw_type_error(&format!( + "Cannot set property {index} which has only a getter" + )); + } + crate::object::invoke_accessor_setter( + accessor.set, + receiver(), + value_handle.get_nanbox_f64(), + ); + return arr_handle.get_raw_mut_ptr::(); + } + if crate::object::get_property_attrs(inherited_owner, &key) + .is_some_and(|attrs| !attrs.writable()) + { + throw_frozen_array_index_write(index); + } + } + + js_array_set_f64_extend_strict( + arr_handle.get_raw_mut_ptr::(), + index, + value_handle.get_nanbox_f64(), + ) + } +} + /// Read an own indexed property from an Array prototype while preserving the /// original receiver for an inherited accessor's `this` value. unsafe fn array_inherited_index_get( diff --git a/crates/perry-runtime/src/array/mod.rs b/crates/perry-runtime/src/array/mod.rs index b1724174c4..5f35d3492d 100644 --- a/crates/perry-runtime/src/array/mod.rs +++ b/crates/perry-runtime/src/array/mod.rs @@ -70,7 +70,8 @@ pub use self::element_shape::{ pub(crate) use self::element_shape::{test_element_shape_record_exists, test_serialize}; pub use self::flat_clone::{ js_array_clone, js_array_clone_for_spread, js_array_entries, js_array_flat, - js_array_flat_depth, js_array_keys, js_array_values, js_short_packed_spread_values, + js_array_flat_depth, js_array_keys, js_array_values, js_arraylike_flat, + js_short_packed_spread_values, }; pub use self::from_concat::{ array_from_full, array_of_full, js_array_concat_variadic, js_array_from_mapped, @@ -121,7 +122,7 @@ pub use self::immutable::{ pub(crate) use self::indexing::test_keys_array_slot_fallbacks; pub(crate) use self::indexing::{ array_has_own_index, array_iteration_is_exotic, array_proto_iterator_modified, - array_prototype_has_index_flag, array_spec_get, array_spec_has_index, + array_prototype_has_index_flag, array_spec_get, array_spec_has_index, array_spec_set, invalidate_array_index_fast_path, keys_array_len_capped_to_capacity, keys_array_slot, note_array_proto_iterator_write, note_object_prototype_index_write, object_prototype_has_index_flag, PERRY_ARRAY_INDEX_FAST_PATH_INVALIDATED, @@ -193,9 +194,9 @@ pub use self::jsvalue_api::{ pub(crate) use self::push_pop::guard_writable_length; pub use self::push_pop::{ js_array_delete, js_array_grow, js_array_numeric_push_f64_unboxed, js_array_pop_f64, - js_array_push_f64, js_array_push_hole, js_array_push_spread_f64, js_array_set_length, - js_array_set_length_strict, js_array_shift_f64, js_array_unshift_f64, js_array_unshift_jsvalue, - js_array_unshift_variadic, + js_array_push_f64, js_array_push_f64_spec, js_array_push_hole, js_array_push_spread_f64, + js_array_set_length, js_array_set_length_strict, js_array_shift_f64, js_array_unshift_f64, + js_array_unshift_jsvalue, js_array_unshift_variadic, }; pub use self::reduce_right::js_array_reduce_right; pub use self::search::{ diff --git a/crates/perry-runtime/src/array/push_pop.rs b/crates/perry-runtime/src/array/push_pop.rs index c3625df647..3711d6c23c 100644 --- a/crates/perry-runtime/src/array/push_pop.rs +++ b/crates/perry-runtime/src/array/push_pop.rs @@ -191,6 +191,12 @@ pub extern "C" fn js_array_grow(arr: *mut ArrayHeader, min_capacity: u32) -> *mu (new_ptr as *mut u8).sub(crate::gc::GC_HEADER_SIZE) as *mut crate::gc::GcHeader; (*new_header)._reserved = (*old_header)._reserved; crate::gc::layout_transfer(arr as *mut u8, new_ptr as *mut u8); + // `js_array_grow` is an allocation replacement outside the collector, + // so GC's normal side-table rekey phase does not run. Preserve every + // accessor/property descriptor already owned by the old array before + // turning it into a forwarding stub (reduceRight getter-order cases + // commonly install index 1, then grow again while installing index 2). + crate::object::transfer_descriptor_owner(arr as usize, new_ptr as usize); // #7742-adjacent: the copy above is verbatim at offset 0, so the old // store's dirty-page coverage can be TRANSLATED to the new address // instead of re-derived from 3 M slot values. Falls back to the full @@ -701,6 +707,75 @@ pub extern "C" fn js_array_push_f64(arr: *mut ArrayHeader, value: f64) -> *mut A } } +/// User-observable `Array.prototype.push` for a statically known Array. +/// +/// Most runtime callers use [`js_array_push_f64`] as an internal +/// CreateDataProperty-style append while building a fresh result array. Those +/// writes must ignore inherited indexed setters. JavaScript `push`, however, +/// performs `Set` and therefore needs the descriptor-aware path whenever the +/// receiver or its prototype chain is exotic. +#[no_mangle] +pub extern "C" fn js_array_push_f64_spec(arr: *mut ArrayHeader, value: f64) -> *mut ArrayHeader { + if array_ptr_as_proxy(arr).is_some() { + return js_array_push_f64(arr, value); + } + let cleaned = clean_arr_ptr_mut(arr); + if cleaned.is_null() { + return js_array_push_f64(arr, value); + } + if crate::array::array_iteration_is_exotic(cleaned) { + crate::string::js_string_addref_if_heap_string(value); + return push_array_spec_path(cleaned, value); + } + js_array_push_f64(cleaned, value) +} + +/// The observable Set/Set-length path for push when indexed descriptors, +/// sparse storage, or prototype indices make the dense append inequivalent. +fn push_array_spec_path(arr: *mut ArrayHeader, value: f64) -> *mut ArrayHeader { + let scope = crate::gc::RuntimeHandleScope::new(); + let arr_handle = scope.root_raw_mut_ptr(arr); + let value_handle = scope.root_nanbox_f64(value); + let length = unsafe { (*arr_handle.get_raw_mut_ptr::()).length }; + + if length == u32::MAX { + // 2^32-1 is a named property, not an Array index. The element Set is + // observable before the final ArraySetLength rejects 2^32. + let key_text = length.to_string(); + let key = crate::string::js_string_from_bytes(key_text.as_ptr(), key_text.len() as u32); + unsafe { + array_named_property_set( + arr_handle.get_raw_mut_ptr::(), + key, + value_handle.get_nanbox_f64(), + ); + } + crate::array::array_length_range_error(); + } + + let next = crate::array::array_spec_set( + arr_handle.get_raw_mut_ptr::(), + length, + value_handle.get_nanbox_f64(), + ); + let next = clean_arr_ptr_mut(next); + if !next.is_null() { + arr_handle.set_raw_mut_ptr(next); + } + unsafe { + let current = clean_arr_ptr_mut(arr_handle.get_raw_mut_ptr::()); + arr_handle.set_raw_mut_ptr(current); + // An inherited setter above can change either integrity condition. + if array_is_frozen(current) { + throw_frozen_array_mutation(); + } + guard_writable_length(current); + (*current).length = length + 1; + rebuild_array_layout(current); + current + } +} + #[no_mangle] pub extern "C" fn js_array_push_hole(arr: *mut ArrayHeader) -> *mut ArrayHeader { js_array_push_f64(arr, f64::from_bits(crate::value::TAG_HOLE)) @@ -720,6 +795,9 @@ pub extern "C" fn js_array_numeric_push_f64_unboxed( } guard_writable_length(arr); unsafe { + if crate::array::array_iteration_is_exotic(arr) { + return js_array_push_f64_spec(arr, value); + } if array_numeric_raw_f64_push_inbounds(arr, value) { return arr; } @@ -1209,12 +1287,17 @@ fn shift_array_spec_set( index: u32, value_handle: &crate::gc::RuntimeHandle<'_>, ) { - let _ = arr_handle.across_mut::(|| { + let (next, post_gc) = arr_handle.across_mut::(|| { let value = value_handle.get_nanbox_f64(); - arr_handle.with_mut_ptr(|current| { - crate::array::js_array_set_f64_extend(current, index, value); - }); + arr_handle.with_mut_ptr(|current| crate::array::array_spec_set(current, index, value)) }); + let next = clean_arr_ptr_mut(next); + let current = if next.is_null() { + clean_arr_ptr_mut(post_gc) + } else { + next + }; + arr_handle.set_raw_mut_ptr(current); } fn shift_array_spec_delete(arr_handle: &crate::gc::RuntimeHandle<'_>, index: u32) { @@ -1239,6 +1322,9 @@ pub extern "C" fn js_array_unshift_f64(arr: *mut ArrayHeader, value: f64) -> *mu if arr.is_null() { return js_array_alloc(0); } + if crate::array::array_iteration_is_exotic(arr) { + return unshift_array_spec_path(arr, &[value]); + } if array_is_frozen(arr) { throw_frozen_array_mutation(); } @@ -1299,6 +1385,16 @@ pub extern "C" fn js_array_unshift_variadic( // a frozen array and a non-writable `length` throw before the no-op early // return. Frozen check must come first because freeze doesn't record "length" // attrs, so `guard_writable_length` alone wouldn't catch it. + if count != 0 && crate::array::array_iteration_is_exotic(arr) { + let values = unsafe { + if items.is_null() { + &[][..] + } else { + std::slice::from_raw_parts(items, count as usize) + } + }; + return unshift_array_spec_path(arr, values); + } if array_is_frozen(arr) { throw_frozen_array_mutation(); } @@ -1348,6 +1444,54 @@ pub extern "C" fn js_array_unshift_variadic( } } +fn unshift_array_spec_path(arr: *mut ArrayHeader, items: &[f64]) -> *mut ArrayHeader { + let scope = crate::gc::RuntimeHandleScope::new(); + let arr_handle = scope.root_raw_mut_ptr(arr); + let item_handles: Vec<_> = items + .iter() + .map(|value| scope.root_nanbox_f64(*value)) + .collect(); + let length = unsafe { (*arr_handle.get_raw_mut_ptr::()).length }; + let count = u32::try_from(item_handles.len()).unwrap_or_else(|_| { + crate::array::array_length_range_error(); + }); + let new_length = length.checked_add(count).unwrap_or_else(|| { + crate::array::array_length_range_error(); + }); + + let mut k = length; + while k > 0 { + let from = k - 1; + let to = from + count; + if crate::array::array_spec_has_index(arr_handle.get_raw_mut_ptr::(), from) { + let iteration_scope = crate::gc::RuntimeHandleScope::new(); + let value = + crate::array::array_spec_get(arr_handle.get_raw_mut_ptr::(), from); + let value_handle = iteration_scope.root_nanbox_f64(value); + shift_array_spec_set(&arr_handle, to, &value_handle); + } else { + shift_array_spec_delete(&arr_handle, to); + } + k -= 1; + } + for (index, value) in item_handles.iter().enumerate() { + shift_array_spec_set(&arr_handle, index as u32, value); + } + + unsafe { + let current = clean_arr_ptr_mut(arr_handle.get_raw_mut_ptr::()); + arr_handle.set_raw_mut_ptr(current); + // Getters/setters in the indexed moves may have changed this state. + if array_is_frozen(current) { + throw_frozen_array_mutation(); + } + guard_writable_length(current); + (*current).length = new_length; + rebuild_array_layout(current); + current + } +} + #[cfg(feature = "keepalive-anchors")] #[used] static KEEP_UNSHIFT_VARIADIC: extern "C" fn(*mut ArrayHeader, *const f64, u32) -> *mut ArrayHeader = diff --git a/crates/perry-runtime/src/closure/dynamic_props.rs b/crates/perry-runtime/src/closure/dynamic_props.rs index e1fa71728d..4fe2f096de 100644 --- a/crates/perry-runtime/src/closure/dynamic_props.rs +++ b/crates/perry-runtime/src/closure/dynamic_props.rs @@ -653,8 +653,7 @@ pub(crate) fn function_prototype_fallback_target(ptr: usize, prop: &str) -> Opti // S15.2.4.7_A8 regressions caught after the initial fix). | "toString" | "valueOf" | "hasOwnProperty" | "isPrototypeOf" | "propertyIsEnumerable" | "toLocaleString" - ) || crate::object::canonical_array_index(prop).is_some() - || crate::object::reified_function_method_name(prop).is_some() + ) || crate::object::reified_function_method_name(prop).is_some() { return None; } diff --git a/crates/perry-runtime/src/object/array_object_ops.rs b/crates/perry-runtime/src/object/array_object_ops.rs index c6f3b5eaca..46ff0a1a9a 100644 --- a/crates/perry-runtime/src/object/array_object_ops.rs +++ b/crates/perry-runtime/src/object/array_object_ops.rs @@ -377,6 +377,22 @@ pub(crate) unsafe fn define_array_property( let Some(key_name) = key_name else { return Some(true); }; + // The caller roots these values, but a root in an outer stack frame does + // not rewrite raw locals in this callee. Descriptor field probes and + // closure rebinding below allocate (and may run user code), so keep local + // handles as well and re-read them after every such call. Copy the key name + // onto the Rust heap before opening any allocation window. + let key_name_owned = key_name.to_string(); + let key_name = key_name_owned.as_str(); + let scope = crate::gc::RuntimeHandleScope::new(); + let obj_handle = scope.root_raw_mut_ptr(obj); + let descriptor_handle = scope.root_nanbox_f64(descriptor_value); + let key_handle = scope.root_string_ptr(key_str); + let current_obj = || obj_handle.get_raw_mut_ptr::(); + let current_descriptor = || descriptor_handle.get_nanbox_f64(); + let current_descriptor_ptr = || extract_obj_ptr(current_descriptor()); + let current_key = || key_handle.get_raw_const_ptr::(); + let current_arr = || array_header_mut(current_obj()); // Any explicit per-index/named/length descriptor makes the raw numeric // fast paths ineligible for this array — they can't see accessors or @@ -391,14 +407,15 @@ pub(crate) unsafe fn define_array_property( return Some(array_set_length_from_descriptor(obj, descriptor_value)); } - let desc_ptr = extract_obj_ptr(descriptor_value); + let desc_ptr = current_descriptor_ptr(); if desc_ptr.is_null() { return Some(true); } let value_key = crate::string::js_string_from_bytes(b"value".as_ptr(), 5); // `ToPropertyDescriptor` field presence is HasProperty (own OR inherited). - let has_value = super::desc_has_field(descriptor_value, b"value"); - let value_field = js_object_get_field_by_name(desc_ptr as *const ObjectHeader, value_key); + let has_value = super::desc_has_field(current_descriptor(), b"value"); + let value_field = + js_object_get_field_by_name(current_descriptor_ptr() as *const ObjectHeader, value_key); let value = if has_value { f64::from_bits(value_field.bits()) } else { @@ -408,14 +425,14 @@ pub(crate) unsafe fn define_array_property( // #7548: `obj` may be a pre-grow forwarding stub whose `length`/`capacity` // words hold the forwarding pointer — the `index >= (*arr).length` gate // below and every element read/extend need the array's current home. - let arr = array_header_mut(obj); + let arr = current_arr(); let read_bool = |name: &[u8]| -> Option { - if !super::desc_has_field(descriptor_value, name) { + if !super::desc_has_field(current_descriptor(), name) { return None; } let k = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let v = js_object_get_field_by_name(desc_ptr as *const ObjectHeader, k); + let v = js_object_get_field_by_name(current_descriptor_ptr() as *const ObjectHeader, k); Some(crate::value::js_is_truthy(f64::from_bits(v.bits())) != 0) }; @@ -429,7 +446,7 @@ pub(crate) unsafe fn define_array_property( && (super::get_accessor_descriptor(obj as usize, key_name).is_some() || crate::array::array_named_property_get_by_name(arr, key_name).is_some()); let index_exists = super::canonical_array_index(key_name) - .map(|_| super::has_own_helpers::array_own_key_present(arr, key_str)) + .map(|_| super::has_own_helpers::array_own_key_present(current_arr(), current_key())) .unwrap_or(false); if !named_exists && !index_exists { let gc = gc_header_for(obj); @@ -443,7 +460,7 @@ pub(crate) unsafe fn define_array_property( } if let Some(index) = super::canonical_array_index(key_name) { - let exists = super::has_own_helpers::array_own_key_present(arr, key_str); + let exists = super::has_own_helpers::array_own_key_present(current_arr(), current_key()); // Array exotic `[[DefineOwnProperty]]` (ECMA-262 10.4.2.1) step 3.b: a // NEW index at or beyond `length` requires extending `length`, which is @@ -464,8 +481,8 @@ pub(crate) unsafe fn define_array_property( // ObjectHeader and corrupt it, so handle it here. let get_key = crate::string::js_string_from_bytes(b"get".as_ptr(), 3); let set_key = crate::string::js_string_from_bytes(b"set".as_ptr(), 3); - let desc_has_get = super::desc_has_field(descriptor_value, b"get"); - let desc_has_set = super::desc_has_field(descriptor_value, b"set"); + let desc_has_get = super::desc_has_field(current_descriptor(), b"get"); + let desc_has_set = super::desc_has_field(current_descriptor(), b"set"); if desc_has_get || desc_has_set { // ValidateAndApplyPropertyDescriptor for an existing non-configurable // index: reject the data→accessor switch AND a change to a @@ -493,28 +510,43 @@ pub(crate) unsafe fn define_array_property( ); } } - let get_field = js_object_get_field_by_name(desc_ptr as *const ObjectHeader, get_key); - let set_field = js_object_get_field_by_name(desc_ptr as *const ObjectHeader, set_key); - let recv = crate::value::js_nanbox_pointer(obj as i64); - let prior = super::get_accessor_descriptor(obj as usize, key_name); + let get_field = js_object_get_field_by_name( + current_descriptor_ptr() as *const ObjectHeader, + get_key, + ); + let get_field = scope.root_nanbox_u64(get_field.bits()); + let set_field = js_object_get_field_by_name( + current_descriptor_ptr() as *const ObjectHeader, + set_key, + ); + let set_field = scope.root_nanbox_u64(set_field.bits()); + let prior = super::get_accessor_descriptor(current_arr() as usize, key_name); let get_bits = if desc_has_get { - if get_field.is_undefined() { + if crate::value::JSValue::from_bits(get_field.get_nanbox_u64()).is_undefined() { 0 } else { - crate::closure::clone_closure_rebind_this(get_field.bits(), recv) + crate::closure::clone_closure_rebind_this( + get_field.get_nanbox_u64(), + crate::value::js_nanbox_pointer(current_obj() as i64), + ) } } else { prior.map(|a| a.get).unwrap_or(0) }; + let get_bits = scope.root_nanbox_u64(get_bits); let set_bits = if desc_has_set { - if set_field.is_undefined() { + if crate::value::JSValue::from_bits(set_field.get_nanbox_u64()).is_undefined() { 0 } else { - crate::closure::clone_closure_rebind_this(set_field.bits(), recv) + crate::closure::clone_closure_rebind_this( + set_field.get_nanbox_u64(), + crate::value::js_nanbox_pointer(current_obj() as i64), + ) } } else { prior.map(|a| a.set).unwrap_or(0) }; + let set_bits = scope.root_nanbox_u64(set_bits); // Materialize BEFORE storing the accessor — the extend helper // dispatches accessor setters, so installing the accessor first // would turn this internal materialization into a setter call. @@ -528,10 +560,10 @@ pub(crate) unsafe fn define_array_property( // not the stale `obj`. Without this, a length-extending accessor // (`Object.defineProperty(arr, "20", {get})` on a short array) was // silently dropped: `arr[20]` / `indexOf` never fired the getter. - let mut side_addr = obj as usize; + let mut side_addr = current_arr() as usize; if !exists { let new_arr = crate::array::js_array_set_f64_extend( - arr, + current_arr(), index, f64::from_bits(crate::value::TAG_UNDEFINED), ); @@ -546,8 +578,8 @@ pub(crate) unsafe fn define_array_property( side_addr, key_name.to_string(), AccessorDescriptor { - get: get_bits, - set: set_bits, + get: get_bits.get_nanbox_u64(), + set: set_bits.get_nanbox_u64(), }, ); // Retain existing attrs the descriptor omits when redefining; new @@ -567,7 +599,7 @@ pub(crate) unsafe fn define_array_property( let configurable = read_bool(b"configurable") .unwrap_or_else(|| cur.map(|a| a.configurable()).unwrap_or(false)); set_property_attrs( - side_addr, + current_arr() as usize, key_name.to_string(), PropertyAttrs::new(false, enumerable, configurable), ); diff --git a/crates/perry-runtime/src/object/descriptor_state.rs b/crates/perry-runtime/src/object/descriptor_state.rs index 462f08099f..d6d1c5462b 100644 --- a/crates/perry-runtime/src/object/descriptor_state.rs +++ b/crates/perry-runtime/src/object/descriptor_state.rs @@ -1088,6 +1088,44 @@ pub(crate) fn clear_object_descriptors(obj: usize) { } } +/// Move string-keyed descriptor ownership when `ArrayHeader` growth replaces +/// one live allocation with another. Array growth is not a GC collection, so +/// the metadata-rewrite scanner below does not run; without this explicit +/// transfer, descriptors installed before a later grow remain keyed to the +/// forwarding stub and disappear from reads through the canonical array head. +pub(crate) fn transfer_descriptor_owner(old_owner: usize, new_owner: usize) { + if old_owner == new_owner { + return; + } + let st = state(); + { + let mut attrs = st.descriptors.property_descriptors.borrow_mut(); + let moved = attrs + .keys() + .filter(|(owner, _)| *owner == old_owner) + .cloned() + .collect::>(); + for old_key in moved { + if let Some(value) = attrs.remove(&old_key) { + attrs.insert((new_owner, old_key.1), value); + } + } + } + { + let mut accessors = st.descriptors.accessor_descriptors.borrow_mut(); + let moved = accessors + .keys() + .filter(|(owner, _)| *owner == old_owner) + .cloned() + .collect::>(); + for old_key in moved { + if let Some(value) = accessors.remove(&old_key) { + accessors.insert((new_owner, old_key.1), value); + } + } + } +} + /// Rewrite a descriptor table's owner ADDRESS during the GC metadata-rewrite /// phase (evacuation moved the owning object), mirroring the symbol-keyed /// twin tables' owner rekey (`symbol/gc_roots.rs`). Outside that phase the diff --git a/crates/perry-runtime/src/object/global_this/array_error.rs b/crates/perry-runtime/src/object/global_this/array_error.rs index 09e517a665..e251412dd9 100644 --- a/crates/perry-runtime/src/object/global_this/array_error.rs +++ b/crates/perry-runtime/src/object/global_this/array_error.rs @@ -679,6 +679,13 @@ pub(crate) extern "C" fn array_prototype_values_thunk( crate::array::array_values_iter(this) } +pub(crate) extern "C" fn array_prototype_flat_thunk( + _c: *const crate::closure::ClosureHeader, + depth: f64, +) -> f64 { + crate::array::js_arraylike_flat(crate::object::js_implicit_this_get(), depth) +} + pub(crate) extern "C" fn array_prototype_pop_thunk( _c: *const crate::closure::ClosureHeader, _a: f64, diff --git a/crates/perry-runtime/src/object/global_this/proto_methods.rs b/crates/perry-runtime/src/object/global_this/proto_methods.rs index 5f68060b5c..29f60e3d82 100644 --- a/crates/perry-runtime/src/object/global_this/proto_methods.rs +++ b/crates/perry-runtime/src/object/global_this/proto_methods.rs @@ -127,7 +127,6 @@ pub(crate) fn populate_builtin_prototype_methods(builtin_name: &str, proto_obj: proto_obj, &[ ("entries", 0), - ("flat", 0), ("flatMap", 1), ("keys", 0), ("toLocaleString", 0), @@ -137,6 +136,12 @@ pub(crate) fn populate_builtin_prototype_methods(builtin_name: &str, proto_obj: ("with", 2), ], ); + install_proto_method( + proto_obj, + "flat", + array_prototype_flat_thunk as *const u8, + 0, + ); // Generic mutators get REAL thunks (vs the noop above) so a borrowed // reference works: `obj.pop = Array.prototype.pop; obj.pop()` and // `Array.prototype.splice.call(obj, …)`. Each reads IMPLICIT_THIS and diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index 985c9c7902..24c8847ddc 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -249,8 +249,8 @@ pub(crate) use descriptor_state::{ object_has_descriptors, object_proto_may_intercept_key, owner_may_have_descriptor_entries, plain_data_write_may_intercept, prune_dead_descriptor_owner_entries, reflect_getter_closure_bits, set_accessor_descriptor, set_builtin_accessor_descriptor, - set_builtin_property_attrs, set_property_attrs, AccessorDescriptor, DescriptorTables, - PropertyAttrs, + set_builtin_property_attrs, set_property_attrs, transfer_descriptor_owner, AccessorDescriptor, + DescriptorTables, PropertyAttrs, }; pub(crate) use field_get_set::FieldLookupCaches; pub(crate) use field_get_set::{ diff --git a/crates/perry-runtime/src/object/native_call_method.rs b/crates/perry-runtime/src/object/native_call_method.rs index 500a2cb0d8..06a5393925 100644 --- a/crates/perry-runtime/src/object/native_call_method.rs +++ b/crates/perry-runtime/src/object/native_call_method.rs @@ -2102,9 +2102,20 @@ pub unsafe extern "C-unwind" fn js_native_call_method( let method_key = crate::string::js_string_from_bytes(method_name.as_ptr(), method_name.len() as u32); if !method_key.is_null() { - if let Some(field_val) = - super::prototype_chain::resolve_inherited_field(obj as usize, method_key) - { + let inherited = super::prototype_chain::resolve_inherited_field( + obj as usize, + method_key, + ) + .or_else(|| unsafe { + // A plain object's implicit Object.prototype is not stored in + // the recorded-prototype table. Property reads already use + // this guarded fallback, so direct `obj.method()` dispatch + // must consult it too (including user-added methods such as a + // borrowed Array.prototype.join). The helper rejects arrays, + // exotic/null-prototype objects, and explicit overrides. + super::field_get_set::ordinary_object_prototype_property_value(obj, method_key) + }); + if let Some(field_val) = inherited { if !field_val.is_undefined() && !field_val.is_null() { let bound = crate::closure::clone_closure_rebind_this( field_val.bits(), diff --git a/crates/perry-runtime/src/object/native_call_method/common_methods.rs b/crates/perry-runtime/src/object/native_call_method/common_methods.rs index 8465a8fe85..738315045a 100644 --- a/crates/perry-runtime/src/object/native_call_method/common_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/common_methods.rs @@ -859,7 +859,7 @@ pub(super) unsafe fn dispatch_common( if !args_ptr.is_null() { for i in 0..args_len { let val = *args_ptr.add(i); - arr = crate::array::js_array_push_f64(arr, val); + arr = crate::array::js_array_push_f64_spec(arr, val); } } return Some(crate::array::js_array_length(arr) as f64); diff --git a/crates/perry-runtime/src/object/native_call_method/handle_methods.rs b/crates/perry-runtime/src/object/native_call_method/handle_methods.rs index fb61d0161c..e1c6293e29 100644 --- a/crates/perry-runtime/src/object/native_call_method/handle_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/handle_methods.rs @@ -416,7 +416,7 @@ pub(super) unsafe fn dispatch_handle( } else { f64::from_bits(crate::value::TAG_UNDEFINED) }; - a = crate::array::js_array_push_f64(a, v); + a = crate::array::js_array_push_f64_spec(a, v); } return Some(crate::array::js_array_length(a) as f64); } diff --git a/crates/perry/tests/issue_5898_array_remaining.rs b/crates/perry/tests/issue_5898_array_remaining.rs new file mode 100644 index 0000000000..ff90301be3 --- /dev/null +++ b/crates/perry/tests/issue_5898_array_remaining.rs @@ -0,0 +1,251 @@ +//! Regression coverage for the final built-ins/Array clusters in #5898. + +use std::path::PathBuf; +use std::process::Command; + +fn perry_bin() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_perry")) +} + +#[test] +fn remaining_array_exotic_and_large_length_semantics() { + let dir = tempfile::tempdir().expect("tempdir"); + let entry = dir.path().join("main.ts"); + let output = dir.path().join("main_bin"); + let runtime_dir = perry_bin() + .parent() + .expect("perry binary directory") + .to_path_buf(); + std::fs::write( + &entry, + r#" +(Function.prototype as any).myproperty = 1; +console.log("ctor-proto", (Array as any).myproperty, Array.hasOwnProperty("myproperty")); +delete (Function.prototype as any).myproperty; + +Object.defineProperty(Object.prototype, "1", { + get() { return 6.99; }, + configurable: true +}); +const ctorIndex = (Array as any)[1]; +delete (Object.prototype as any)[1]; +console.log("ctor-index", ctorIndex); + +console.log("huge", new Array(4294967295).length); +console.log( + "of", + Array.isArray(Array.of.call(undefined)), + Array.isArray(Array.of.call(Math.cos)), + Array.isArray(Array.of.call(Math.cos.bind(Math))) +); +console.log( + "flat", + Array.prototype.flat.call(true).length, + Array.prototype.flat.call(false).length +); + +let genericPushThrew = false; +const generic: any = { length: Infinity }; +try { Array.prototype.push.call(generic, "x"); } catch (e) { + genericPushThrew = e instanceof TypeError; +} +console.log("generic-push", genericPushThrew, generic[9007199254740991] === undefined); + +const max: any[] = []; +max.length = 4294967295; +console.log("max-empty-push", max.push()); +let maxPushThrew = false; +try { max.push("x"); } catch (e) { maxPushThrew = e instanceof RangeError; } +console.log("max-push", maxPushThrew, max[4294967295], max.length); + +let freezeCalls = 0; +const frozenDuringPush: any[] = []; +Object.defineProperty(Array.prototype, "0", { + set(_value) { freezeCalls++; Object.freeze(frozenDuringPush); }, + configurable: true +}); +let freezePushThrew = false; +try { frozenDuringPush.push(1); } catch (e) { freezePushThrew = e instanceof TypeError; } +delete (Array.prototype as any)[0]; +console.log( + "push-freeze", + freezePushThrew, + freezeCalls, + frozenDuringPush.hasOwnProperty(0), + frozenDuringPush.length +); + +let lengthCalls = 0; +const lockedDuringPush: any[] = []; +Object.defineProperty(Array.prototype, "0", { + set(_value) { + lengthCalls++; + Object.defineProperty(lockedDuringPush, "length", { writable: false }); + }, + configurable: true +}); +let lockedPushThrew = false; +try { lockedDuringPush.push(1); } catch (e) { lockedPushThrew = e instanceof TypeError; } +delete (Array.prototype as any)[0]; +console.log( + "push-length", + lockedPushThrew, + lengthCalls, + lockedDuringPush.hasOwnProperty(0), + lockedDuringPush.length +); + +let visible = false; +let reduceRightOk = false; +let reduceRightTrace = "missing"; +const reduced: any[] = [0]; +Object.defineProperty(reduced, "1", { + get() { return visible ? 1 : "20"; }, + configurable: true +}); +Object.defineProperty(reduced, "2", { + get() { visible = true; return 2; }, + configurable: true +}); +reduced.reduceRight((previous, current, index) => { + if (index === 1) { + reduceRightTrace = previous + ":" + current; + reduceRightOk = current === 1 && previous === 2; + } +}); +console.log("reduce-right", reduceRightOk, reduceRightTrace, visible); + +(Array.prototype as any)[0] = 1; +const shifted: any[] = []; +shifted.length = 1; +console.log("unshift-array", shifted.unshift(0), shifted[0], shifted[1]); +delete shifted[0]; +console.log("unshift-array-proto", shifted[0]); +delete (Array.prototype as any)[0]; + +(Object.prototype as any)[0] = 1; +(Object.prototype as any).length = 1; +(Object.prototype as any).unshift = Array.prototype.unshift; +let shiftedObject = [9]; +shiftedObject = {}; +console.log( + "unshift-object", + shiftedObject.unshift(0), + shiftedObject[0], + shiftedObject[1], + shiftedObject.length +); +delete shiftedObject[0]; +delete shiftedObject.length; +console.log("unshift-object-proto", shiftedObject[0], shiftedObject.length); +delete (Object.prototype as any)[0]; +delete (Object.prototype as any).length; +delete (Object.prototype as any).unshift; + +(Object.prototype as any)[1] = 1; +(Object.prototype as any).length = 2; +(Object.prototype as any).join = Array.prototype.join; +let joinedObject = [9]; +joinedObject = { 0: 0 }; +console.log("join-object", joinedObject.join()); +delete (Object.prototype as any)[1]; +delete (Object.prototype as any).length; +delete (Object.prototype as any).join; + +let unshiftSetterCalls = 0; +const frozenDuringUnshift: any[] = []; +Object.defineProperty(Array.prototype, "0", { + set(_value) { unshiftSetterCalls++; Object.freeze(frozenDuringUnshift); }, + configurable: true +}); +let freezeUnshiftThrew = false; +try { frozenDuringUnshift.unshift(1); } catch (e) { + freezeUnshiftThrew = e instanceof TypeError; +} +delete (Array.prototype as any)[0]; +console.log( + "unshift-freeze", + freezeUnshiftThrew, + unshiftSetterCalls, + frozenDuringUnshift.hasOwnProperty(0), + frozenDuringUnshift.length +); + +let unshiftLengthCalls = 0; +const lockedDuringUnshift: any[] = []; +Object.defineProperty(Array.prototype, "0", { + set(_value) { + unshiftLengthCalls++; + Object.defineProperty(lockedDuringUnshift, "length", { writable: false }); + }, + configurable: true +}); +let lockedUnshiftThrew = false; +try { lockedDuringUnshift.unshift(1); } catch (e) { + lockedUnshiftThrew = e instanceof TypeError; +} +delete (Array.prototype as any)[0]; +console.log( + "unshift-length", + lockedUnshiftThrew, + unshiftLengthCalls, + lockedDuringUnshift.hasOwnProperty(0), + lockedDuringUnshift.length +); +"#, + ) + .expect("write entry"); + + let compile = Command::new(perry_bin()) + .current_dir(dir.path()) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&output) + .arg("--no-cache") + .env("PERRY_LIB_DIR", &runtime_dir) + .env("PERRY_NO_AUTO_OPTIMIZE", "1") + .env("PERRY_RS4GC", "0") + .output() + .expect("run perry compile"); + assert!( + compile.status.success(), + "perry compile failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&compile.stdout), + String::from_utf8_lossy(&compile.stderr) + ); + + let run = Command::new(&output) + .current_dir(dir.path()) + .output() + .expect("run compiled binary"); + assert!( + run.status.success(), + "compiled binary failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); + assert_eq!( + String::from_utf8_lossy(&run.stdout), + concat!( + "ctor-proto 1 false\n", + "ctor-index 6.99\n", + "huge 4294967295\n", + "of true true true\n", + "flat 0 0\n", + "generic-push true true\n", + "max-empty-push 4294967295\n", + "max-push true x 4294967295\n", + "push-freeze true 1 false 0\n", + "push-length true 1 false 0\n", + "reduce-right true 2:1 true\n", + "unshift-array 2 0 1\n", + "unshift-array-proto 1\n", + "unshift-object 2 0 1 2\n", + "unshift-object-proto 1 1\n", + "join-object 0,1\n", + "unshift-freeze true 1 false 0\n", + "unshift-length true 1 false 0\n" + ) + ); +} diff --git a/scripts/raw_handle_debt_baseline.txt b/scripts/raw_handle_debt_baseline.txt index a0394c20d6..e59798702d 100644 --- a/scripts/raw_handle_debt_baseline.txt +++ b/scripts/raw_handle_debt_baseline.txt @@ -1 +1 @@ -949 +970 diff --git a/scripts/raw_handle_debt_files.txt b/scripts/raw_handle_debt_files.txt index fdb66ef785..974f0d9d3a 100644 --- a/scripts/raw_handle_debt_files.txt +++ b/scripts/raw_handle_debt_files.txt @@ -48,10 +48,11 @@ # # Format: 5 crates/perry-runtime/src/array/header.rs -6 crates/perry-runtime/src/array/indexing.rs +13 crates/perry-runtime/src/array/indexing.rs +4 crates/perry-runtime/src/array/flat_clone.rs 2 crates/perry-runtime/src/array/iter_methods.rs 31 crates/perry-runtime/src/array/iterator.rs -4 crates/perry-runtime/src/array/push_pop.rs +12 crates/perry-runtime/src/array/push_pop.rs 14 crates/perry-runtime/src/array/sort.rs 12 crates/perry-runtime/src/async_hooks.rs 5 crates/perry-runtime/src/atomics.rs @@ -156,3 +157,4 @@ 6 crates/perry-runtime/src/weakref.rs 20 crates/perry-runtime/src/webassembly.rs 6 crates/perry-runtime/src/proxy/put_value.rs +2 crates/perry-runtime/src/object/array_object_ops.rs