diff --git a/crates/perry-codegen/src/expr/index_get.rs b/crates/perry-codegen/src/expr/index_get.rs index 98d227dc09..e0152a7253 100644 --- a/crates/perry-codegen/src/expr/index_get.rs +++ b/crates/perry-codegen/src/expr/index_get.rs @@ -30,7 +30,7 @@ use crate::type_analysis::{ is_numeric_expr, is_set_expr, is_string_expr, is_url_search_params_expr, receiver_class_name, }; #[allow(unused_imports)] -use crate::types::{DOUBLE, I1, I32, I64, I8, PTR}; +use crate::types::{DOUBLE, I1, I16, I32, I64, I8, PTR}; use super::arrays_finds::lower_buffer_index_get_i32; #[allow(unused_imports)] @@ -378,6 +378,16 @@ fn lower_bounded_array_index_get( let fwd_bits = blk.and(I8, &gc_flags, "128"); // GC_FLAG_FORWARDED let is_fwd = blk.icmp_ne(I8, &fwd_bits, "0"); let needs_slow = blk.or(I1, &is_lazy, &is_fwd); + // Index accessors / custom attribute descriptors (`Object.defineProperty + // (arr, i, { get })`) divert element reads through the descriptor tables — + // the raw slot load below would bypass them (test262 sort/precise-*). + // GcHeader._reserved (u16 at -6) carries OBJ_FLAG_ARRAY_DESCRIPTORS=0x400. + let obj_flags_addr = blk.sub(I64, &arr_handle, "6"); + let obj_flags_ptr = blk.inttoptr(I64, &obj_flags_addr); + let obj_flags = blk.load(I16, &obj_flags_ptr); + let desc_bits = blk.and(I16, &obj_flags, "1024"); + let has_desc = blk.icmp_ne(I16, &desc_bits, "0"); + let needs_slow = blk.or(I1, &needs_slow, &has_desc); let lazy_idx = ctx.new_block("bidx.lazy"); let fast_idx = ctx.new_block("bidx.fast"); @@ -443,6 +453,16 @@ fn lower_legacy_array_index_get( let fwd_bits = blk.and(I8, &gc_flags, "128"); // GC_FLAG_FORWARDED let is_fwd = blk.icmp_ne(I8, &fwd_bits, "0"); let needs_slow = blk.or(I1, &is_lazy, &is_fwd); + // Index accessors / custom attribute descriptors (`Object.defineProperty + // (arr, i, { get })`) divert element reads through the descriptor tables — + // the raw slot load below would bypass them (test262 sort/precise-*). + // GcHeader._reserved (u16 at -6) carries OBJ_FLAG_ARRAY_DESCRIPTORS=0x400. + let obj_flags_addr = blk.sub(I64, &arr_handle, "6"); + let obj_flags_ptr = blk.inttoptr(I64, &obj_flags_addr); + let obj_flags = blk.load(I16, &obj_flags_ptr); + let desc_bits = blk.and(I16, &obj_flags, "1024"); + let has_desc = blk.icmp_ne(I16, &desc_bits, "0"); + let needs_slow = blk.or(I1, &needs_slow, &has_desc); let lazy_idx = ctx.new_block("arr.lazy"); let fast_idx = ctx.new_block("arr.fast"); diff --git a/crates/perry-codegen/src/expr/instance_misc1.rs b/crates/perry-codegen/src/expr/instance_misc1.rs index 4eb5091cbc..732369882c 100644 --- a/crates/perry-codegen/src/expr/instance_misc1.rs +++ b/crates/perry-codegen/src/expr/instance_misc1.rs @@ -1192,7 +1192,10 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let out_slot = blk.alloca(I64); blk.store(I64, "0", &out_slot); let arr_handle = unbox_to_i64(blk, &arr_box); - let start_i32 = blk.fptosi(DOUBLE, &start_d, I32); + // ToIntegerOrInfinity via the clamping helper: `fptosi` on + // ±Infinity/NaN is LLVM poison — `splice(Infinity, 3)` deleted + // from index 0 (test262 splice/S15.4.4.12_A2.1_T3). + let start_i32 = blk.call(I32, "js_array_splice_delete_count", &[(DOUBLE, &start_d)]); let count_i32 = blk.call(I32, "js_array_splice_delete_count", &[(DOUBLE, &count_d)]); let (items_ptr, items_count_str) = if item_vals.is_empty() { diff --git a/crates/perry-codegen/src/expr/logical_collections.rs b/crates/perry-codegen/src/expr/logical_collections.rs index 7232be94ff..0063af7c43 100644 --- a/crates/perry-codegen/src/expr/logical_collections.rs +++ b/crates/perry-codegen/src/expr/logical_collections.rs @@ -330,6 +330,42 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ], ) } + // sort(comparator?): validated + run by the runtime engine. + "sort" => { + let cmp = nth(0).unwrap_or_else(undef); + blk.call( + DOUBLE, + "js_arraylike_sort", + &[(DOUBLE, &recv_box), (DOUBLE, &cmp)], + ) + } + // splice(...) / concat(...): variadic — pass an alloca buffer + // of raw NaN-boxed doubles + count (mirrors the dense + // `js_array_concat_variadic` lowering). + "splice" | "concat" => { + let n = arg_boxes.len(); + let (buf_reg, count_str) = if n == 0 { + ("null".to_string(), "0".to_string()) + } else { + let buf_reg = blk.next_reg(); + blk.emit_raw(format!("{} = alloca [{} x double]", buf_reg, n)); + for (i, val) in arg_boxes.iter().enumerate() { + let slot = blk.gep(DOUBLE, &buf_reg, &[(I64, &format!("{}", i))]); + blk.store(DOUBLE, val, &slot); + } + (buf_reg, format!("{}", n)) + }; + let fname = if method == "splice" { + "js_arraylike_splice" + } else { + "js_arraylike_concat" + }; + blk.call( + DOUBLE, + fname, + &[(DOUBLE, &recv_box), (PTR, &buf_reg), (I32, &count_str)], + ) + } other => bail!("unsupported generic array-like method '{other}'"), }; Ok(result) diff --git a/crates/perry-codegen/src/lower_array_method.rs b/crates/perry-codegen/src/lower_array_method.rs index 0633e497ba..5f5713f05b 100644 --- a/crates/perry-codegen/src/lower_array_method.rs +++ b/crates/perry-codegen/src/lower_array_method.rs @@ -53,6 +53,26 @@ pub(crate) fn lower_array_method( Ok(nanbox_string_inline(blk, &result_handle)) } "some" | "every" => { + // An explicit `thisArg` (2nd argument) must bind the callback's + // `this`; the dense helpers don't take one (they bind undefined), + // so route through the generic array-like engine, which does + // (real-array receivers keep a fast element path there). + if args.len() >= 2 { + let cb_box = lower_expr(ctx, &args[0])?; + let this_box = lower_expr(ctx, &args[1])?; + let blk = ctx.block(); + let gen_fn = if property == "some" { + "js_arraylike_some" + } else { + "js_arraylike_every" + }; + return Ok(blk.call( + DOUBLE, + gen_fn, + &[(DOUBLE, &recv_box), (DOUBLE, &cb_box), (DOUBLE, &this_box)], + )); + } + // `arr.some()` / `arr.every()` with no callback must throw a // runtime TypeError ("undefined is not a function"), not fail to // compile — pad a missing callback with `undefined` and let @@ -251,6 +271,22 @@ pub(crate) fn lower_array_method( // as HIR variants but may reach here as generic MethodCall when // the HIR lowering doesn't recognize the pattern. "find" => { + // An explicit `thisArg` (2nd argument) must bind the callback's + // `this`; the dense helpers don't take one (they bind undefined), + // so route through the generic array-like engine, which does + // (real-array receivers keep a fast element path there). + if args.len() >= 2 { + let cb_box = lower_expr(ctx, &args[0])?; + let this_box = lower_expr(ctx, &args[1])?; + let blk = ctx.block(); + let gen_fn = "js_arraylike_find"; + return Ok(blk.call( + DOUBLE, + gen_fn, + &[(DOUBLE, &recv_box), (DOUBLE, &cb_box), (DOUBLE, &this_box)], + )); + } + // 0-arg → runtime TypeError (pad undefined), not compile-fail. let cb_box = if let Some(arg) = args.first() { lower_expr(ctx, arg)? @@ -268,6 +304,22 @@ pub(crate) fn lower_array_method( )) } "findIndex" => { + // An explicit `thisArg` (2nd argument) must bind the callback's + // `this`; the dense helpers don't take one (they bind undefined), + // so route through the generic array-like engine, which does + // (real-array receivers keep a fast element path there). + if args.len() >= 2 { + let cb_box = lower_expr(ctx, &args[0])?; + let this_box = lower_expr(ctx, &args[1])?; + let blk = ctx.block(); + let gen_fn = "js_arraylike_findIndex"; + return Ok(blk.call( + DOUBLE, + gen_fn, + &[(DOUBLE, &recv_box), (DOUBLE, &cb_box), (DOUBLE, &this_box)], + )); + } + // 0-arg → runtime TypeError (pad undefined), not compile-fail. let cb_box = if let Some(arg) = args.first() { lower_expr(ctx, arg)? @@ -393,6 +445,22 @@ pub(crate) fn lower_array_method( )) } "map" => { + // An explicit `thisArg` (2nd argument) must bind the callback's + // `this`; the dense helpers don't take one (they bind undefined), + // so route through the generic array-like engine, which does + // (real-array receivers keep a fast element path there). + if args.len() >= 2 { + let cb_box = lower_expr(ctx, &args[0])?; + let this_box = lower_expr(ctx, &args[1])?; + let blk = ctx.block(); + let gen_fn = "js_arraylike_map"; + return Ok(blk.call( + DOUBLE, + gen_fn, + &[(DOUBLE, &recv_box), (DOUBLE, &cb_box), (DOUBLE, &this_box)], + )); + } + // 0-arg → runtime TypeError (pad undefined), not compile-fail. let cb_box = if let Some(arg) = args.first() { lower_expr(ctx, arg)? @@ -417,6 +485,22 @@ pub(crate) fn lower_array_method( Ok(nanbox_pointer_inline(blk, &result)) } "filter" => { + // An explicit `thisArg` (2nd argument) must bind the callback's + // `this`; the dense helpers don't take one (they bind undefined), + // so route through the generic array-like engine, which does + // (real-array receivers keep a fast element path there). + if args.len() >= 2 { + let cb_box = lower_expr(ctx, &args[0])?; + let this_box = lower_expr(ctx, &args[1])?; + let blk = ctx.block(); + let gen_fn = "js_arraylike_filter"; + return Ok(blk.call( + DOUBLE, + gen_fn, + &[(DOUBLE, &recv_box), (DOUBLE, &cb_box), (DOUBLE, &this_box)], + )); + } + // 0-arg → runtime TypeError (pad undefined), not compile-fail. let cb_box = if let Some(arg) = args.first() { lower_expr(ctx, arg)? @@ -435,6 +519,22 @@ pub(crate) fn lower_array_method( Ok(nanbox_pointer_inline(blk, &result)) } "forEach" => { + // An explicit `thisArg` (2nd argument) must bind the callback's + // `this`; the dense helpers don't take one (they bind undefined), + // so route through the generic array-like engine, which does + // (real-array receivers keep a fast element path there). + if args.len() >= 2 { + let cb_box = lower_expr(ctx, &args[0])?; + let this_box = lower_expr(ctx, &args[1])?; + let blk = ctx.block(); + let gen_fn = "js_arraylike_forEach"; + return Ok(blk.call( + DOUBLE, + gen_fn, + &[(DOUBLE, &recv_box), (DOUBLE, &cb_box), (DOUBLE, &this_box)], + )); + } + // 0-arg → runtime TypeError (pad undefined), not compile-fail. let cb_box = if let Some(arg) = args.first() { lower_expr(ctx, arg)? @@ -759,7 +859,12 @@ pub(crate) fn lower_array_method( let out_slot = blk.alloca(I64); blk.store(I64, "0", &out_slot); let recv_handle = unbox_to_i64(blk, &recv_box); - let start_i32 = blk.fptosi(DOUBLE, &start_d, I32); + // ToIntegerOrInfinity via the clamping helper: `fptosi` on + // ±Infinity/NaN is LLVM poison — `splice(Infinity, 3)` deleted + // from index 0 (test262 splice/S15.4.4.12_A2.1_T3). The helper + // clamps +Inf → i32::MAX (→ len downstream), -Inf → i32::MIN + // (→ 0 after relative-index resolution), NaN → 0. + let start_i32 = blk.call(I32, "js_array_splice_delete_count", &[(DOUBLE, &start_d)]); let count_i32 = blk.call(I32, "js_array_splice_delete_count", &[(DOUBLE, &count_d)]); let (items_ptr, items_count_str) = if item_vals.is_empty() { ("null".to_string(), "0".to_string()) diff --git a/crates/perry-codegen/src/runtime_decls/arrays.rs b/crates/perry-codegen/src/runtime_decls/arrays.rs index 2e791409ac..df8bdd19fc 100644 --- a/crates/perry-codegen/src/runtime_decls/arrays.rs +++ b/crates/perry-codegen/src/runtime_decls/arrays.rs @@ -194,6 +194,9 @@ pub fn declare_phase_b_arrays(module: &mut LlModule) { DOUBLE, &[DOUBLE, DOUBLE, I32, DOUBLE, I32], ); + module.declare_function("js_arraylike_sort", DOUBLE, &[DOUBLE, DOUBLE]); + module.declare_function("js_arraylike_splice", DOUBLE, &[DOUBLE, PTR, I32]); + module.declare_function("js_arraylike_concat", DOUBLE, &[DOUBLE, PTR, I32]); // Spread `[...x]` — strict GetIterator/materialization. module.declare_function("js_array_clone_for_spread", I64, &[DOUBLE]); module.declare_function("js_array_spread_append", I64, &[I64, DOUBLE]); diff --git a/crates/perry-hir/src/lower/expr_call/intrinsics.rs b/crates/perry-hir/src/lower/expr_call/intrinsics.rs index 80e19cd751..dddcff81bd 100644 --- a/crates/perry-hir/src/lower/expr_call/intrinsics.rs +++ b/crates/perry-hir/src/lower/expr_call/intrinsics.rs @@ -1789,6 +1789,14 @@ fn try_arraylike_receiver_method( | "slice" | "at" | "join" + // Generic mutators with dedicated runtime engines (#4597 + // extension): `sort` sorts the receiver in place via + // Get/HasProperty/Set/Delete; `splice`/`concat` apply the spec + // algorithms over the array-like (test262 sort/call-with-primitive, + // splice/set_length_no_args, concat/call-with-boolean). + | "sort" + | "splice" + | "concat" ); if generic { // Receiver lowers before the positional args, matching source order. diff --git a/crates/perry-hir/src/lower_types.rs b/crates/perry-hir/src/lower_types.rs index a3d45654c7..7ae2011242 100644 --- a/crates/perry-hir/src/lower_types.rs +++ b/crates/perry-hir/src/lower_types.rs @@ -278,14 +278,33 @@ pub(crate) fn infer_type_from_expr(expr: &ast::Expr, ctx: &LoweringContext) -> T // Template literals are always strings ast::Expr::Tpl(_) => Type::String, - // Array literals → infer element type from first element + // Array literals → unified element type across ALL elements. Using just + // the first element claimed `Array(Number)` for a mixed literal like + // `[1, true, "x"]`, and codegen trusted that lie: `a[i] === b[j]` + // lowered to a raw `fcmp` where NaN-boxed booleans/strings/undefined + // are unordered → strict equality between two mixed-array loads was + // always false (test262 sort/S15.4.4.11_A2.1_T3 et al). Divergent + // element types now infer `Array(Any)` so the comparison (and every + // other consumer) takes the tag-aware path. A spread element's + // contribution is unknown statically → Any. ast::Expr::Array(arr) => { - let elem_ty = arr - .elems - .iter() - .find_map(|e| e.as_ref().map(|elem| infer_type_from_expr(&elem.expr, ctx))) - .unwrap_or(Type::Any); - Type::Array(Box::new(elem_ty)) + let mut unified: Option = None; + for e in arr.elems.iter().flatten() { + let t = if e.spread.is_some() { + Type::Any + } else { + infer_type_from_expr(&e.expr, ctx) + }; + match &unified { + None => unified = Some(t), + Some(u) if *u == t => {} + Some(_) => { + unified = Some(Type::Any); + break; + } + } + } + Type::Array(Box::new(unified.unwrap_or(Type::Any))) } // Variable reference → look up known type diff --git a/crates/perry-runtime/src/array/concat_reverse.rs b/crates/perry-runtime/src/array/concat_reverse.rs index 62a0ab68ba..e5af1358a2 100644 --- a/crates/perry-runtime/src/array/concat_reverse.rs +++ b/crates/perry-runtime/src/array/concat_reverse.rs @@ -21,14 +21,20 @@ fn fill_to_number(value: f64) -> f64 { } } -fn fill_to_length(value: f64) -> u32 { - let number = fill_to_number(value); +fn fill_to_length(value: f64) -> i64 { + // `ToLength` clamps to 2^53-1 (NOT u32::MAX — an array-like with + // `length: Number.MAX_SAFE_INTEGER` must keep its near-limit indices + // addressable; test262 fill/length-near-integer-limit). + const MAX_LENGTH: f64 = 9_007_199_254_740_991.0; + // Real ToNumber: fires `valueOf` on an object-valued length and throws + // TypeError for Symbol/BigInt (fill/return-abrupt-from-this-length*). + let number = crate::builtins::js_number_coerce(value); if number.is_nan() || number <= 0.0 { 0 - } else if !number.is_finite() || number > u32::MAX as f64 { - u32::MAX + } else if !number.is_finite() || number > MAX_LENGTH { + MAX_LENGTH as i64 } else { - number as u32 + number as i64 } } @@ -403,25 +409,32 @@ pub extern "C" fn js_array_fill_range( // end). The previous `idx.is_nan()` clamp silently mapped a NaN-boxed // object argument to 0 and never threw. The default-end sentinel // (+Infinity from codegen) is a real f64 and survives coercion unchanged. + // An explicit `undefined` end means "to length" (step 7) — coercing it + // gave NaN → 0 → no-op (`[0, 0].fill(1, 0, undefined)` stayed [0, 0], + // test262 fill/coerced-indexes). let start = crate::builtins::js_to_integer_or_infinity(start); - let end = crate::builtins::js_to_integer_or_infinity(end); + let end = if end.to_bits() == crate::value::TAG_UNDEFINED { + f64::INFINITY + } else { + crate::builtins::js_to_integer_or_infinity(end) + }; unsafe { let len = (*arr).length as i64; if len == 0 { return arr; } - let clamp = |idx: f64, default_to_len: bool| -> i64 { + let clamp = |idx: f64, _default_to_len: bool| -> i64 { if idx.is_nan() { return 0; } let mut i = idx as i64; if idx.is_infinite() { + // ToIntegerOrInfinity: +∞ → len, -∞ → 0 (for BOTH start and + // end — `fill(1, 0, -Infinity)` fills nothing; the absent-end + // codegen sentinel is +∞ and clamps to len above). if idx > 0.0 { return len; } - if default_to_len { - return len; - } return 0; } if i < 0 { @@ -489,10 +502,17 @@ pub extern "C" fn js_array_fill_generic( return f64::from_bits(JSValue::pointer(result as *mut u8).bits()); } if obj_type == crate::gc::GC_TYPE_OBJECT || obj_type == crate::gc::GC_TYPE_CLOSURE { - let obj = raw as *mut crate::object::ObjectHeader; + // `Get(O, "length")` — must fire an own `length` accessor + // (propagating its throw, test262 + // fill/return-abrupt-from-this-length) and walk the prototype + // chain; `ToLength` then fires a `valueOf` on an object-valued + // length (return-abrupt-from-this-length-as-symbol throws in + // the coercion). let length_key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); - let len_value = crate::object::js_object_get_field_by_name_f64(obj, length_key); - let len = fill_to_length(len_value) as i64; + let key_v = f64::from_bits(JSValue::string_ptr(length_key).bits()); + let len_value = + unsafe { crate::object::js_object_get_property_key(receiver, key_v) }; + let len = fill_to_length(len_value); if len == 0 { return receiver; } diff --git a/crates/perry-runtime/src/array/from_concat.rs b/crates/perry-runtime/src/array/from_concat.rs index 25882c9f00..84eb49d8c6 100644 --- a/crates/perry-runtime/src/array/from_concat.rs +++ b/crates/perry-runtime/src/array/from_concat.rs @@ -160,7 +160,44 @@ pub extern "C" fn js_array_concat_variadic( args_ptr: *const f64, count: i32, ) -> *mut ArrayHeader { - let result = js_array_alloc(0); + // Defensive receiver re-dispatch: a variable whose STATIC type was + // inferred `Array` but was reassigned to a plain object at runtime + // (`var x = [0]; … x = {0: 0}; x.concat()`, test262 S15.4.4.4_A3_T1) + // reaches this dense entry with an ObjectHeader — reading it as an + // ArrayHeader spreads garbage. Route plain objects/closures through the + // generic array-like engine instead. + { + let raw = recv as usize; + if raw >= crate::gc::GC_HEADER_SIZE + 0x1000 { + let obj_type = unsafe { + let hdr = + (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + (*hdr).obj_type + }; + if obj_type == crate::gc::GC_TYPE_OBJECT || obj_type == crate::gc::GC_TYPE_CLOSURE { + let recv_value = f64::from_bits(JSValue::pointer(recv as *const u8).bits()); + let result = crate::array::js_arraylike_concat(recv_value, args_ptr, count); + return crate::value::js_nanbox_get_pointer(result) as *mut ArrayHeader; + } + } + } + // ECMA-262 §23.1.3.5 step 2: ArraySpeciesCreate(O, 0) runs BEFORE any + // element is read — it fires the receiver's `constructor` / `@@species` + // accessors (propagating a poisoned-getter throw) and throws TypeError on + // a non-constructor species (test262 concat/create-ctor-poisoned, + // create-ctor-non-object, create-non-array). + let recv_value = f64::from_bits(JSValue::pointer(recv as *const u8).bits()); + let (result_box, result_is_plain) = unsafe { + let b = crate::array::species::array_species_create(recv_value, 0); + (b, crate::array::species::species_result_is_plain_array(b)) + }; + let result = if result_is_plain { + crate::value::js_nanbox_get_pointer(result_box) as *mut ArrayHeader + } else { + // Custom species container: build the elements in a plain staging + // array first, then CreateDataProperty them onto the container below. + js_array_alloc(0) + }; // The receiver itself is always spread (it's the array on which `.concat` // was invoked). Materialize a clone to read its elements safely. let result = append_spread_array(result, recv as *const ArrayHeader); @@ -171,6 +208,19 @@ pub extern "C" fn js_array_concat_variadic( result = append_concat_arg(result, value); } } + if !result_is_plain { + unsafe { + let len = (*result).length as usize; + let elems = (result as *const u8).add(std::mem::size_of::()) as *const f64; + for i in 0..len { + let v = *elems.add(i); + if v.to_bits() != crate::value::TAG_HOLE { + crate::array::species::species_result_set(result_box, i, v); + } + } + return crate::value::js_nanbox_get_pointer(result_box) as *mut ArrayHeader; + } + } result } @@ -182,7 +232,7 @@ static KEEP_ARRAY_CONCAT_VARIADIC: extern "C" fn( ) -> *mut ArrayHeader = js_array_concat_variadic; /// Append a single concat argument to `result`, applying spreadability rules. -fn append_concat_arg(result: *mut ArrayHeader, value: f64) -> *mut ArrayHeader { +pub(crate) fn append_concat_arg(result: *mut ArrayHeader, value: f64) -> *mut ArrayHeader { let bits = value.to_bits(); let jv = JSValue::from_bits(bits); if !jv.is_pointer() { @@ -651,8 +701,24 @@ fn append_spread_array(result: *mut ArrayHeader, src: *const ArrayHeader) -> *mu let elems = (materialized as *const u8).add(std::mem::size_of::()) as *const f64; let mut out = result; + // ECMA-262 §23.1.3.5 step 5.c.iii: each index goes through + // `HasProperty(E, k)` / `Get(E, k)` — a hole filled by an inherited + // `Array.prototype[k]` element lands as an OWN property of the result + // (test262 concat/S15.4.4.4_A3_T1); a genuinely absent index stays a + // hole. The dense raw read only differs when the clone has holes, so + // gate the spec reads on the per-element hole check. for i in 0..len as usize { - out = js_array_push_f64(out, *elems.add(i)); + let v = *elems.add(i); + if v.to_bits() == crate::value::TAG_HOLE { + if crate::array::array_spec_has_index(materialized, i as u32) { + out = js_array_push_f64( + out, + crate::array::array_spec_get(materialized, i as u32), + ); + continue; + } + } + out = js_array_push_f64(out, v); } out } diff --git a/crates/perry-runtime/src/array/generic.rs b/crates/perry-runtime/src/array/generic.rs index 73d317874a..c426185e4b 100644 --- a/crates/perry-runtime/src/array/generic.rs +++ b/crates/perry-runtime/src/array/generic.rs @@ -139,6 +139,16 @@ fn as_real_array(recv: f64) -> *mut ArrayHeader { if raw < crate::gc::GC_HEADER_SIZE + 0x1000 { return ptr::null_mut(); } + // Pointer-shaped non-heap handles (Proxy ids live at 0xF0000+, stream ids + // and friends in nearby bands) would be dereferenced as a GcHeader below — + // `Array.prototype.indexOf.call(proxy, …)` SIGSEGV'd. The Linux heap + // range check alone admits the id bands (they start at 0x1000), so gate + // on the handle-band classifier too. + if !crate::value::addr_class::is_above_handle_band(raw) + || !crate::object::is_valid_obj_ptr(raw as *const u8) + { + return ptr::null_mut(); + } let obj_type = unsafe { let hdr = (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; (*hdr).obj_type @@ -170,16 +180,41 @@ enum PtrKind { /// Typed array or buffer — `js_value_length_f64` / /// `js_object_get_index_polymorphic` handle these by registry. IndexedNative, - /// Date / Map / Set / Symbol / BigInt / … — no safe array-like access; + /// Date / RegExp / Error / … exotic cell that carries expando own + /// properties in the `exotic_expando` side table (`d = new Date(); + /// d.length = 2; d[0] = 11`). Array-like reads resolve through that + /// table (test262 Array.prototype.*-1-11/-1-14 "applied to Date"). + ExpandoExotic(crate::object::exotic_expando::ExoticKind), + /// A Proxy id — every array-like op routes through its traps. + Proxy, + /// Map / Set / Symbol / BigInt / … — no safe array-like access; /// treated as an empty array-like. Exotic, } +fn proxy_string_key(k: i64) -> f64 { + let s = k.to_string(); + let key = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + f64::from_bits(JSValue::string_ptr(key).bits()) +} + +fn proxy_named_key(name: &str) -> f64 { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + f64::from_bits(JSValue::string_ptr(key).bits()) +} + fn classify_pointer(recv: f64) -> Option { let b = recv.to_bits(); if top16(b) != 0x7FFD { return None; } + // Proxies are small registered ids (pointer-shaped, below the heap floor) + // — classify BEFORE any address-based probe. All array-like ops route + // through the proxy traps (so a revoked proxy's Get(length) throws — + // test262 {map,filter,splice,concat}/create-revoked-proxy). + if crate::proxy::js_proxy_is_proxy(recv) != 0 { + return Some(PtrKind::Proxy); + } let raw = (b & 0x0000_FFFF_FFFF_FFFF) as usize; // Typed arrays / buffers are `std::alloc`-backed (no GC header) — probe // their registries before any GC-header read. @@ -191,15 +226,24 @@ fn classify_pointer(recv: f64) -> Option { if raw < crate::gc::GC_HEADER_SIZE + 0x1000 { return Some(PtrKind::Exotic); } + // Non-heap registry ids (stream/fetch handles) must not be dereferenced + // as a GcHeader. (The Linux heap-range check alone admits the id bands.) + if !crate::value::addr_class::is_above_handle_band(raw) + || !crate::object::is_valid_obj_ptr(raw as *const u8) + { + return Some(PtrKind::Exotic); + } let obj_type = unsafe { let hdr = (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; (*hdr).obj_type }; if obj_type == crate::gc::GC_TYPE_OBJECT || obj_type == crate::gc::GC_TYPE_CLOSURE { - Some(PtrKind::Object) - } else { - Some(PtrKind::Exotic) + return Some(PtrKind::Object); + } + if let Some(kind) = crate::object::exotic_expando::exotic_expando_kind(raw) { + return Some(PtrKind::ExpandoExotic(kind)); } + Some(PtrKind::Exotic) } /// `LengthOfArrayLike(ToObject(recv))`. @@ -218,13 +262,43 @@ fn al_length(recv: f64) -> i64 { } match classify_pointer(recv) { Some(PtrKind::Object) => { - // Plain object / function: read the `length` property (its absence - // ToLength-coerces to 0). Safe — guaranteed `GC_TYPE_OBJECT/CLOSURE`. - let key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); - let len_val = crate::object::js_object_get_field_by_name_f64( - (b & 0x0000_FFFF_FFFF_FFFF) as *const crate::object::ObjectHeader, - key, - ); + // Plain object / function: `Get(O, "length")`. An OWN accessor — + // even a setter-only one — shadows anything inherited (test262 + // some/15.4.4.17-2-12): fire its getter or read undefined, and + // never fall through to the prototype probes. + let raw_addr = (b & 0x0000_FFFF_FFFF_FFFF) as usize; + let mut len_val; + if let Some(acc) = crate::object::get_accessor_descriptor(raw_addr, "length") { + len_val = if acc.get != 0 { + f64::from_bits( + unsafe { crate::object::invoke_accessor_getter(acc.get, recv) }.bits(), + ) + } else { + undef() + }; + } else { + let key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); + len_val = crate::object::js_object_get_field_by_name_f64( + raw_addr as *const crate::object::ObjectHeader, + key, + ); + let key_v = f64::from_bits(JSValue::string_ptr(key).bits()); + let own_present = + crate::object::js_object_has_own(recv, key_v).to_bits() == TAG_TRUE; + // `Get(O, "length")` walks the prototype chain — an inherited + // `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 { + 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 + // the `Object` constructor) — probe it as a last resort. + if len_val.to_bits() == TAG_UNDEFINED { + len_val = canonical_object_prototype_named_get("length"); + } + } + } // `LengthOfArrayLike` is `ToLength(ToNumber(Get(O, "length")))`. // A non-numeric `length` (e.g. `length: true` → 1, `length: "2"` → // 2) must be ToNumber-coerced first — the raw NaN-boxed bool/string @@ -233,11 +307,92 @@ fn al_length(recv: f64) -> i64 { } // Typed arrays / buffers expose a real length via the safe dispatcher. Some(PtrKind::IndexedNative) => to_length(crate::value::js_value_length_f64(recv)), + // Proxy: Get("length") through the `get` trap (throws on revoked). + Some(PtrKind::Proxy) => to_length(crate::builtins::js_number_coerce( + crate::proxy::js_proxy_get(recv, proxy_named_key("length")), + )), + // Date/RegExp/Error expando receiver: `length` lives in the exotic + // side table. + Some(PtrKind::ExpandoExotic(kind)) => { + let raw = (b & 0x0000_FFFF_FFFF_FFFF) as usize; + match crate::object::exotic_expando::value_lookup(kind, raw, "length") { + Some(bits) => to_length(crate::builtins::js_number_coerce(f64::from_bits(bits))), + None => 0, + } + } // Exotic cells / bare primitives → empty array-like. Some(PtrKind::Exotic) | None => 0, } } +/// Read a named property off the user-visible `Object.prototype` (resolved +/// through the `Object` constructor, where user writes like +/// `Object.prototype.length = 2` actually land). +fn canonical_object_prototype_named_get(name: &str) -> f64 { + let ctor = crate::object::js_get_global_this_builtin_value(b"Object".as_ptr(), 6); + let ctor_v = JSValue::from_bits(ctor.to_bits()); + if !ctor_v.is_pointer() { + return undef(); + } + let proto = + crate::closure::closure_get_dynamic_prop(ctor_v.as_pointer::() as usize, "prototype"); + let proto_v = JSValue::from_bits(proto.to_bits()); + if !proto_v.is_pointer() { + return undef(); + } + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + crate::object::js_object_get_field_by_name_f64( + proto_v.as_pointer::(), + key, + ) +} + +/// `Get(O, name)` over the recorded/default prototype chain for an ordinary +/// heap object, for a *named* (non-index) key. Companion to +/// `object_get_property_chain`; used when the direct own read misses. +fn object_get_named_property_chain(obj_ptr: usize, name: &str) -> f64 { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let key_val = f64::from_bits(JSValue::string_ptr(key).bits()); + let mut cur = obj_ptr; + for _ in 0..1000 { + if cur == 0 { + return undef(); + } + let cur_val = f64::from_bits(crate::value::js_nanbox_pointer(cur as i64).to_bits()); + if crate::object::js_object_has_own(cur_val, key_val).to_bits() == TAG_TRUE { + return crate::object::js_object_get_field_by_name_f64( + cur as *const crate::object::ObjectHeader, + key, + ); + } + let proto_bits = match crate::object::prototype_chain::object_static_prototype(cur) { + Some(bits) => bits, + None => match unsafe { + crate::object::prototype_chain::default_object_prototype_for_owner(cur) + } { + Some(bits) => bits, + None => return undef(), + }, + }; + if proto_bits == TAG_NULL { + return undef(); + } + let t16 = proto_bits >> 48; + let next = if t16 == 0x7FFD { + (proto_bits & 0x0000_FFFF_FFFF_FFFF) as usize + } else if t16 == 0 && proto_bits > 0x10000 { + proto_bits as usize + } else { + return undef(); + }; + if next == cur { + return undef(); + } + cur = next; + } + undef() +} + /// `Get(ToObject(recv), k)` (returns `undefined` for absent/out-of-range). fn al_get(recv: f64, k: i64) -> f64 { let arr = as_real_array(recv); @@ -265,11 +420,42 @@ fn al_get(recv: f64, k: i64) -> f64 { // index living on `Object.prototype[k]` must resolve. Fall back to a // chain read only when the direct read missed. if v.to_bits() == TAG_UNDEFINED { - object_get_property_chain((b & 0x0000_FFFF_FFFF_FFFF) as usize, k) + // An OWN property — even a setter-only accessor — shadows + // anything inherited (test262 some/15.4.4.17-7-c-i-19): + // `Get` resolves to undefined, never the prototype value. + let raw_addr = (b & 0x0000_FFFF_FFFF_FFFF) as usize; + let key_s = k.to_string(); + if crate::object::get_accessor_descriptor(raw_addr, &key_s).is_some() { + return undef(); + } + let key = crate::string::js_string_from_bytes(key_s.as_ptr(), key_s.len() as u32); + let key_v = f64::from_bits(JSValue::string_ptr(key).bits()); + if crate::object::js_object_has_own(recv, key_v).to_bits() == TAG_TRUE { + return undef(); + } + let chained = object_get_property_chain(raw_addr, k); + if chained.to_bits() == TAG_UNDEFINED && k >= 0 && k <= u32::MAX as i64 { + // Canonical Object.prototype probe (data or accessor) — + // the recorded/default proto tables may miss it. + if crate::array::object_prototype_has_index_prop(k as u32) { + return super::sort::object_prototype_index_get(k as u32); + } + } + chained } else { v } } + // Date/RegExp/Error expando receiver: indexed expandos live in the + // exotic side table. + Some(PtrKind::ExpandoExotic(kind)) => { + let raw = (b & 0x0000_FFFF_FFFF_FFFF) as usize; + match crate::object::exotic_expando::value_lookup(kind, raw, &k.to_string()) { + Some(bits) => f64::from_bits(bits), + None => undef(), + } + } + Some(PtrKind::Proxy) => crate::proxy::js_proxy_get(recv, proxy_string_key(k)), Some(PtrKind::Exotic) | None => undef(), } } @@ -342,12 +528,31 @@ fn al_has(recv: f64, k: i64) -> bool { match classify_pointer(recv) { Some(PtrKind::Object) => { let s = k.to_string(); + // An own accessor (even setter-only) counts as present. + if crate::object::get_accessor_descriptor((b & 0x0000_FFFF_FFFF_FFFF) as usize, &s) + .is_some() + { + return true; + } let key = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); let key_val = f64::from_bits(JSValue::string_ptr(key).bits()); - object_has_property_chain((b & 0x0000_FFFF_FFFF_FFFF) as usize, key_val) + if object_has_property_chain((b & 0x0000_FFFF_FFFF_FFFF) as usize, key_val) { + return true; + } + // Canonical Object.prototype probe (data or accessor). + k >= 0 + && k <= u32::MAX as i64 + && crate::array::object_prototype_has_index_prop(k as u32) } // Typed arrays / buffers are dense over their length. Some(PtrKind::IndexedNative) => k < al_length(recv), + Some(PtrKind::ExpandoExotic(kind)) => { + let raw = (b & 0x0000_FFFF_FFFF_FFFF) as usize; + crate::object::exotic_expando::exotic_has_own_property(kind, raw, &k.to_string()) + } + Some(PtrKind::Proxy) => { + crate::value::js_is_truthy(crate::proxy::js_proxy_has(recv, proxy_string_key(k))) != 0 + } Some(PtrKind::Exotic) | None => false, } } @@ -446,6 +651,11 @@ pub extern "C" fn js_arraylike_map(recv: f64, cb: f64, this_arg: f64) -> f64 { // callback is missing/non-callable. Read `len` first, then validate `cb`. let len = al_length(recv); let cb = callable(cb); + // ArraySpeciesCreate → ArrayCreate throws RangeError for len ≥ 2^32 + // (test262 map/create-non-array-invalid-len) — BEFORE any callback runs. + if len > u32::MAX as i64 { + crate::array::array_length_range_error(); + } let result = js_array_alloc_with_length(len.max(0) as u32); let elems = unsafe { (result as *mut u8).add(std::mem::size_of::()) as *mut f64 }; let _g = ThisGuard::new(this_arg); @@ -938,18 +1148,57 @@ static KEEP_ARRAYLIKE_SLICE: extern "C" fn(f64, f64, i32, f64, i32) -> f64 = js_ /// `Set(O, ToString(k), v, true)` for an array-like object receiver. fn al_set(recv: f64, k: i64, v: f64) { + if crate::proxy::js_proxy_is_proxy(recv) != 0 { + crate::proxy::js_proxy_set(recv, proxy_string_key(k), v); + return; + } crate::object::js_object_set_index_polymorphic(recv.to_bits() as i64, k as f64, v); } /// `DeletePropertyOrThrow(O, ToString(k))` for an array-like object receiver. fn al_delete(recv: f64, k: i64) { + if crate::proxy::js_proxy_is_proxy(recv) != 0 { + crate::proxy::js_proxy_delete(recv, proxy_string_key(k)); + return; + } let raw = (recv.to_bits() & 0x0000_FFFF_FFFF_FFFF) as *mut crate::object::ObjectHeader; crate::object::js_object_delete_dynamic(raw, k as f64); } -/// `Set(O, "length", len, true)` for an array-like object receiver. +/// `Set(O, "length", len, true)` for an array-like object receiver. An own +/// `length` ACCESSOR fires its setter — and throws TypeError when there is +/// none (getter-only `length`, test262 splice/S15.4.4.12_A6.1_T3), matching +/// `Set(..., true)` on a non-writable slot. fn al_set_length(recv: f64, len: i64) { - let raw = (recv.to_bits() & 0x0000_FFFF_FFFF_FFFF) as *mut crate::object::ObjectHeader; + let raw_addr = (recv.to_bits() & 0x0000_FFFF_FFFF_FFFF) as usize; + if let Some(acc) = crate::object::get_accessor_descriptor(raw_addr, "length") { + if acc.set != 0 { + unsafe { crate::object::invoke_accessor_setter(acc.set, recv, len as f64) }; + return; + } + crate::collection_iter::throw_type_error( + "Cannot set property length of object which has only a getter", + ); + } + // An object-LITERAL `get length()` lives in the anon-shape class vtable, + // not the defineProperty descriptor table — a getter with no setter makes + // `Set(O, "length", ..., true)` throw (test262 splice/S15.4.4.12_A6.1_T3). + { + let raw = raw_addr as *const crate::object::ObjectHeader; + let class_id = crate::object::js_object_get_class_id(raw); + if class_id != 0 { + if let Some((getter, setter)) = + crate::object::class_own_accessor_ptrs(class_id, "length") + { + if setter == 0 && getter != 0 { + crate::collection_iter::throw_type_error( + "Cannot set property length of object which has only a getter", + ); + } + } + } + } + let raw = raw_addr as *mut crate::object::ObjectHeader; let key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); crate::object::js_object_set_field_by_name(raw, key, len as f64); } @@ -992,6 +1241,32 @@ fn arg_at(args_ptr: *const f64, args_len: usize, i: usize) -> f64 { } } +/// A receiver that reached a dense `ArrayHeader` entry point but is actually +/// a plain object/closure at runtime (a variable whose static type was +/// inferred `Array` and later reassigned — `var x = [1, 0]; … x = {0:1,1:0}; +/// x.sort()`, test262 sort/S15.4.4.11_A6_T2 #5, splice/S15.4.4.12_A4_T1 #7). +/// Returns it NaN-boxed for the generic engine, or `None` for real arrays. +pub(crate) fn non_array_object_receiver(arr: *const ArrayHeader) -> Option { + let raw = arr as usize; + if raw < crate::gc::GC_HEADER_SIZE + 0x1000 { + return None; + } + if !crate::value::addr_class::is_above_handle_band(raw) + || !crate::object::is_valid_obj_ptr(raw as *const u8) + { + return None; + } + let obj_type = unsafe { + let hdr = (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + (*hdr).obj_type + }; + if obj_type == crate::gc::GC_TYPE_OBJECT || obj_type == crate::gc::GC_TYPE_CLOSURE { + Some(f64::from_bits(JSValue::pointer(raw as *const u8).bits())) + } else { + None + } +} + /// If `arr` points to a *plain* object (an object literal — `GC_TYPE_OBJECT` /// with `class_id == 0` or an anonymous shape id), return it NaN-boxed as a /// receiver value, else `None`. Used by the dense `Array.prototype` mutator @@ -1119,7 +1394,7 @@ fn object_reverse(recv: f64) -> f64 { /// `Array.prototype.splice` over an array-like object receiver. Returns a fresh /// plain array of the removed elements (holes preserved). -fn object_splice(recv: f64, args_ptr: *const f64, args_len: usize) -> f64 { +pub(crate) fn object_splice(recv: f64, args_ptr: *const f64, args_len: usize) -> f64 { let len = al_length(recv); let actual_start = relative_index(arg_at(args_ptr, args_len, 0), len); let delete_count = if args_len == 0 { @@ -1130,7 +1405,11 @@ fn object_splice(recv: f64, args_ptr: *const f64, args_len: usize) -> f64 { let dc = to_integer_or_infinity(arg_at(args_ptr, args_len, 1)); dc.max(0.0).min((len - actual_start) as f64) as i64 }; - // Removed elements -> fresh plain array (holes preserved). + // Removed elements -> fresh plain array (holes preserved). ArrayCreate + // throws RangeError for a count ≥ 2^32 (splice/create-non-array-invalid-len). + if delete_count > u32::MAX as i64 { + crate::array::array_length_range_error(); + } let removed = js_array_alloc_with_length(delete_count.max(0) as u32); let removed_elems = unsafe { (removed as *mut u8).add(std::mem::size_of::()) as *mut f64 }; @@ -1191,6 +1470,201 @@ fn object_splice(recv: f64, args_ptr: *const f64, args_len: usize) -> f64 { nanbox_arr(removed) } +/// `Array.prototype.sort` over an array-like (non-real-array) receiver: +/// ECMA-262 SortIndexedProperties with holes skipped — collect via +/// `HasProperty`/`Get`, sort (undefined trailing, never compared), write back +/// via `Set` and `Delete` the trailing range. Returns the receiver. +/// `cmp_validated` is the already-validated comparator (null = default sort). +pub(crate) fn object_sort(recv: f64, cmp_validated: *const ClosureHeader) -> f64 { + let cmp = if cmp_validated.is_null() { + None + } else { + Some(super::sort::ComparatorCall::new(cmp_validated)) + }; + let len = al_length(recv); + unsafe { + // Rooted temp array: keeps accessor-produced values alive across + // comparator calls (a Rust Vec would be invisible to the GC scan). + let temp = js_array_alloc_with_length(len.clamp(0, u32::MAX as i64) as u32); + let temp_elems = (temp as *mut u8).add(std::mem::size_of::()) as *mut f64; + let mut count = 0usize; + let mut undef_count = 0usize; + for j in 0..len { + if al_has(recv, j) { + let v = al_get(recv, j); + if v.to_bits() == TAG_UNDEFINED { + undef_count += 1; + } else { + // GC_STORE_AUDIT(BARRIERED): temp collection array rebuilt below. + ptr::write(temp_elems.add(count), v); + count += 1; + } + } + } + (*temp).length = count as u32; + rebuild_array_layout(temp); + super::sort::sort_rooted_values(temp_elems, count, cmp); + rebuild_array_layout(temp); + for j in 0..count { + al_set(recv, j as i64, *temp_elems.add(j)); + } + for j in count..count + undef_count { + al_set(recv, j as i64, undef()); + } + for j in (count + undef_count) as i64..len { + al_delete(recv, j); + } + } + recv +} + +/// `Array.prototype.concat` over a non-real-array receiver: the receiver is +/// the first concat element (spread only when `@@isConcatSpreadable` says so — +/// a plain object/wrapper lands as a single element), then each argument is +/// appended with the usual spreadability rules. +fn object_concat(recv: f64, args_ptr: *const f64, args_len: usize) -> f64 { + let mut result = super::from_concat::append_concat_arg(js_array_alloc(0), recv); + for i in 0..args_len { + result = super::from_concat::append_concat_arg(result, arg_at(args_ptr, args_len, i)); + } + nanbox_arr(result) +} + +/// Generic `Array.prototype.sort.call(receiver, comparator?)` entry +/// (#4597 extension): ToObject + route real arrays to the dense/spec sort, +/// everything else through the array-like engine. Returns the receiver. +#[no_mangle] +pub extern "C" fn js_arraylike_sort(recv: f64, comparator: f64) -> f64 { + // Spec step 1: comparator must be undefined or callable — BEFORE ToObject. + let cmp = crate::array::js_validate_array_comparator(comparator) as *const ClosureHeader; + let o = to_object(recv); + let arr = as_real_array(o); + if !arr.is_null() { + let r = crate::array::js_array_sort_with_comparator(arr, cmp); + return nanbox_arr(r); + } + object_sort(o, cmp) +} + +/// Generic `Array.prototype.concat.call(receiver, ...items)` entry. +#[no_mangle] +pub extern "C" fn js_arraylike_concat(recv: f64, args_ptr: *const f64, count: i32) -> f64 { + let o = to_object(recv); + let arr = as_real_array(o); + if !arr.is_null() { + let r = crate::array::js_array_concat_variadic(arr, args_ptr, count.max(0)); + return nanbox_arr(r); + } + object_concat(o, args_ptr, count.max(0) as usize) +} + +/// Generic `Array.prototype.splice.call(receiver, start?, deleteCount?, ...items)`. +#[no_mangle] +pub extern "C" fn js_arraylike_splice(recv: f64, args_ptr: *const f64, count: i32) -> f64 { + let o = to_object(recv); + let count = count.max(0) as usize; + let arr = as_real_array(o); + if !arr.is_null() { + return unsafe { real_array_mutator(arr, "splice", args_ptr, count) }; + } + object_splice(o, args_ptr, count) +} + +#[used] +static KEEP_ARRAYLIKE_SORT: extern "C" fn(f64, f64) -> f64 = js_arraylike_sort; +#[used] +static KEEP_ARRAYLIKE_VARIADIC: [extern "C" fn(f64, *const f64, i32) -> f64; 2] = + [js_arraylike_concat, js_arraylike_splice]; + +/// Walk the receiver's [[Prototype]] chain looking for a *real array* link — +/// the `function foo() {}; foo.prototype = new Array(1, 2, 3); new foo()` +/// shape (test262 filter/15.4.4.20-6-*, some/15.4.4.17-8-*). Such a receiver +/// inherits `Array.prototype` methods through the array, so the generic +/// engine must serve them; a plain `{}` (no array on the chain) keeps the +/// normal "not a function" behavior. +fn proto_chain_contains_real_array(obj_ptr: usize) -> bool { + let mut cur = obj_ptr; + for _ in 0..64 { + let proto_bits = match crate::object::prototype_chain::object_static_prototype(cur) { + Some(bits) => bits, + None => match unsafe { + crate::object::prototype_chain::default_object_prototype_for_owner(cur) + } { + Some(bits) => bits, + None => return false, + }, + }; + if proto_bits == TAG_NULL { + return false; + } + let t16 = proto_bits >> 48; + let next = if t16 == 0x7FFD { + (proto_bits & 0x0000_FFFF_FFFF_FFFF) as usize + } else if t16 == 0 && proto_bits > 0x10000 { + proto_bits as usize + } else { + return false; + }; + if next == cur { + return false; + } + let next_val = f64::from_bits(crate::value::js_nanbox_pointer(next as i64).to_bits()); + if !as_real_array(next_val).is_null() { + return true; + } + cur = next; + } + false +} + +/// Dynamic-dispatch hook: a plain-object receiver whose prototype chain +/// contains a real array inherits the `Array.prototype` methods through it. +/// Routes the generic-engine methods; returns `None` for receivers with an +/// own user method of this name, no array on the chain, or unsupported names. +pub fn try_array_proto_chain_method( + object: f64, + method: &str, + args_ptr: *const f64, + args_len: usize, +) -> Option { + if !matches!(classify_pointer(object), Some(PtrKind::Object)) { + return None; + } + let raw = (object.to_bits() & 0x0000_FFFF_FFFF_FFFF) as *const crate::object::ObjectHeader; + let key = crate::string::js_string_from_bytes(method.as_ptr(), method.len() as u32); + let own = crate::object::js_object_get_field_by_name_f64(raw, key); + if matches!(classify_own_slot(own), OwnSlot::UserMethod) { + return None; + } + if !proto_chain_contains_real_array(raw as usize) { + return None; + } + let a = |i: usize| arg_at(args_ptr, args_len, i); + let has = |i: usize| (args_len > i) as i32; + Some(match method { + "forEach" => js_arraylike_forEach(object, a(0), a(1)), + "map" => js_arraylike_map(object, a(0), a(1)), + "filter" => js_arraylike_filter(object, a(0), a(1)), + "some" => js_arraylike_some(object, a(0), a(1)), + "every" => js_arraylike_every(object, a(0), a(1)), + "find" => js_arraylike_find(object, a(0), a(1)), + "findIndex" => js_arraylike_findIndex(object, a(0), a(1)), + "findLast" => js_arraylike_findLast(object, a(0), a(1)), + "findLastIndex" => js_arraylike_findLastIndex(object, a(0), a(1)), + "reduce" => js_arraylike_reduce(object, a(0), has(1), a(1)), + "reduceRight" => js_arraylike_reduceRight(object, a(0), has(1), a(1)), + "indexOf" => js_arraylike_indexOf(object, a(0), a(1), has(1)), + "lastIndexOf" => js_arraylike_lastIndexOf(object, a(0), a(1), has(1)), + "includes" => js_arraylike_includes(object, a(0), a(1), has(1)), + "at" => js_arraylike_at(object, a(0)), + "join" => js_arraylike_join(object, a(0)), + "slice" => js_arraylike_slice(object, a(0), has(0), a(1), has(1)), + "sort" => js_arraylike_sort(object, a(0)), + "concat" => js_arraylike_concat(object, args_ptr, args_len as i32), + _ => return None, + }) +} + /// Run a generic `Array.prototype` mutator on `recv` for the reified prototype /// method thunks (`Array.prototype.pop`, etc.). A real-array receiver routes to /// the dense helpers; a plain array-like object routes to the spec-generic @@ -1243,14 +1717,28 @@ unsafe fn real_array_mutator( crate::array::js_array_length(r) as f64 } } + "sort" => { + let cmp = + crate::array::js_validate_array_comparator(arg_or_undef(args_ptr, args_len, 0)) + as *const ClosureHeader; + nanbox_arr(crate::array::js_array_sort_with_comparator(arr, cmp)) + } + "concat" => { + let count = if args_ptr.is_null() { + 0 + } else { + args_len as i32 + }; + nanbox_arr(crate::array::js_array_concat_variadic(arr, args_ptr, count)) + } "splice" => { + // ToIntegerOrInfinity with i32 clamping: NaN → 0, +Infinity → + // i32::MAX (clamps to len downstream), -Infinity → i32::MIN + // (relative-from-end clamps to 0). The old `is_infinite() → 0` + // made `splice(Infinity, 3)` delete from the front (test262 + // splice/S15.4.4.12_A2.1_T3). let arg_i32 = |i: usize| -> i32 { - let v = arg_or_undef(args_ptr, args_len, i); - if v.is_nan() || v.is_infinite() { - 0 - } else { - v as i32 - } + crate::array::js_array_splice_delete_count(arg_or_undef(args_ptr, args_len, i)) }; let start = if args_len >= 1 { arg_i32(0) } else { 0 }; let delete_count = if args_len == 0 { @@ -1309,7 +1797,14 @@ fn classify_own_slot(v: f64) -> OwnSlot { let fp = crate::closure::get_valid_func_ptr(c); if fp.is_null() { OwnSlot::Absent - } else if fp == crate::closure::BOUND_METHOD_FUNC_PTR { + } else if fp == crate::closure::BOUND_METHOD_FUNC_PTR + // A raw built-in prototype-method closure (`{ splice: + // Array.prototype.splice }` stores the thunk itself, not a bound + // reification) must also run the generic engine on THIS receiver — + // dispatching it as a user method loses the receiver entirely + // (test262 splice/S15.4.4.12_A6.1_T3). + || crate::object::builtin_closure_is_non_constructable_value(v) + { OwnSlot::BorrowedBuiltin } else { OwnSlot::UserMethod @@ -1383,6 +1878,12 @@ pub fn run_object_mutator( "unshift" => object_unshift(recv, args_ptr, args_len), "reverse" => object_reverse(recv), "splice" => object_splice(recv, args_ptr, args_len), + "sort" => { + let cmp = crate::array::js_validate_array_comparator(arg_at(args_ptr, args_len, 0)) + as *const ClosureHeader; + object_sort(recv, cmp) + } + "concat" => object_concat(recv, args_ptr, args_len), _ => return None, }; Some(result) diff --git a/crates/perry-runtime/src/array/immutable.rs b/crates/perry-runtime/src/array/immutable.rs index 23c9ace711..9d8f957b33 100644 --- a/crates/perry-runtime/src/array/immutable.rs +++ b/crates/perry-runtime/src/array/immutable.rs @@ -74,7 +74,16 @@ pub extern "C" fn js_array_to_sorted_default(arr: *const ArrayHeader) -> *mut Ar let src = (arr as *const u8).add(std::mem::size_of::()) as *const f64; let dst = (new_arr as *mut u8).add(std::mem::size_of::()) as *mut f64; // GC_STORE_AUDIT(BARRIERED): sorted clone copy initializes a fresh array rebuilt below. - std::ptr::copy_nonoverlapping(src, dst, len); + // toSorted reads via Get (no HasProperty skip): holes become present + // `undefined` elements in the dense copy (ECMA-262 §23.1.3.34). + for i in 0..len { + let v = *src.add(i); + *dst.add(i) = if v.to_bits() == crate::value::TAG_HOLE { + f64::from_bits(crate::value::TAG_UNDEFINED) + } else { + v + }; + } rebuild_array_layout(new_arr); // Sort the copy in-place using default sort js_array_sort_default(new_arr); @@ -111,7 +120,16 @@ pub extern "C" fn js_array_to_sorted_with_comparator( let src = (arr as *const u8).add(std::mem::size_of::()) as *const f64; let dst = (new_arr as *mut u8).add(std::mem::size_of::()) as *mut f64; // GC_STORE_AUDIT(BARRIERED): comparator sorted clone copy initializes a fresh array rebuilt below. - std::ptr::copy_nonoverlapping(src, dst, len); + // toSorted reads via Get (no HasProperty skip): holes become present + // `undefined` elements in the dense copy (ECMA-262 §23.1.3.34). + for i in 0..len { + let v = *src.add(i); + *dst.add(i) = if v.to_bits() == crate::value::TAG_HOLE { + f64::from_bits(crate::value::TAG_UNDEFINED) + } else { + v + }; + } rebuild_array_layout(new_arr); // Sort the copy in-place js_array_sort_with_comparator(new_arr, comparator); @@ -298,6 +316,16 @@ pub extern "C" fn js_array_copy_within( len_i64 }; unsafe { + // A side-effecting `valueOf` in the index coercions above may have + // mutated the array (e.g. shrunk `length` — test262 + // copyWithin/coerced-values-start-change-*). The raw memmove below + // would then copy stale storage; run the per-index spec loop + // (HasProperty / Get / Set / Delete against the CURRENT state, with + // the ORIGINAL captured len driving the bounds) instead. + if (*arr).length as i64 != len_i64 { + copy_within_spec_dense(arr, len_i64, t, s, e); + return arr; + } let len = len_i64 as isize; let (t, s, e) = (t as isize, s as isize, e as isize); let elements = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; @@ -319,6 +347,38 @@ pub extern "C" fn js_array_copy_within( } } +/// ECMA-262 §23.1.3.4 steps 11-12 over a real-array receiver whose state +/// changed during index coercion: per-index HasProperty (own + inherited) / +/// Get / Set / Delete against the live array, bounds driven by the originally +/// captured `len`. +unsafe fn copy_within_spec_dense(arr: *mut ArrayHeader, len: i64, t: i64, s: i64, e: i64) { + let count = (e - s).min(len - t); + if count <= 0 { + return; + } + let (mut from, mut to, dir) = if s < t && t < s + count { + (s + count - 1, t + count - 1, -1i64) + } else { + (s, t, 1i64) + }; + let mut cur = arr; + let mut remaining = count; + while remaining > 0 { + let from_present = from >= 0 + && from <= u32::MAX as i64 + && crate::array::array_spec_has_index(cur, from as u32); + if from_present { + let v = crate::array::array_spec_get(cur, from as u32); + cur = js_array_set_f64_extend(cur, to as u32, v); + } else if to >= 0 && to <= u32::MAX as i64 { + crate::array::js_array_delete(cur, to as u32); + } + from += dir; + to += dir; + remaining -= 1; + } +} + #[cold] fn throw_copy_within_type_error(message: &[u8]) -> ! { let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); @@ -337,18 +397,22 @@ fn copy_within_to_integer_or_infinity(value: f64) -> f64 { } } -fn copy_within_to_length(value: f64) -> u32 { +fn copy_within_to_length(value: f64) -> i64 { + // `ToLength` clamps to 2^53-1, not u32::MAX — an array-like with + // `length: Number.MAX_SAFE_INTEGER` keeps near-limit indices addressable + // (test262 copyWithin/length-near-integer-limit). + const MAX_LENGTH: f64 = 9_007_199_254_740_991.0; let number = crate::builtins::js_number_coerce(value); if number.is_nan() || number <= 0.0 { 0 } else if number.is_infinite() { if number.is_sign_positive() { - u32::MAX + MAX_LENGTH as i64 } else { 0 } } else { - number.trunc().min(u32::MAX as f64) as u32 + number.trunc().min(MAX_LENGTH) as i64 } } @@ -363,14 +427,40 @@ fn copy_within_relative_index(value: f64, len: i64) -> i64 { } } -fn copy_within_length_of_array_like(receiver: f64) -> u32 { - let length = unsafe { - crate::value::js_dynamic_object_get_property(receiver, b"length".as_ptr() as *const i8, 6) +fn copy_within_length_of_array_like(receiver: f64) -> i64 { + // A Proxy receiver resolves `length` through its `get` trap (falling back + // to the target when untrapped) — the raw dynamic read doesn't route + // proxies (test262 copyWithin/return-abrupt-from-has-start needs the + // subsequent HasProperty trap to fire, which requires a real `len` here). + let length = if crate::proxy::js_proxy_is_proxy(receiver) != 0 { + let key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); + let key_v = f64::from_bits(crate::value::JSValue::string_ptr(key).bits()); + crate::proxy::js_proxy_get(receiver, key_v) + } else { + unsafe { + crate::value::js_dynamic_object_get_property( + receiver, + b"length".as_ptr() as *const i8, + 6, + ) + } }; copy_within_to_length(length) } +fn copy_within_index_key(index: i64) -> f64 { + let s = index.to_string(); + let key = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + f64::from_bits(crate::value::JSValue::string_ptr(key).bits()) +} + fn copy_within_has_property(receiver: f64, index: i64) -> bool { + // `HasProperty(O, fromKey)` fires a Proxy `has` trap, propagating its + // throw (test262 copyWithin/return-abrupt-from-has-start). + if crate::proxy::js_proxy_is_proxy(receiver) != 0 { + let v = crate::proxy::js_proxy_has(receiver, copy_within_index_key(index)); + return crate::value::js_is_truthy(v) != 0; + } crate::value::js_is_truthy(crate::object::js_object_has_own(receiver, index as f64)) != 0 } @@ -393,7 +483,7 @@ pub extern "C" fn js_array_copy_within_value( } let receiver = crate::object::js_object_coerce(receiver); - let len = copy_within_length_of_array_like(receiver) as i64; + let len = copy_within_length_of_array_like(receiver); let to = copy_within_relative_index(target, len); let from = copy_within_relative_index(start, len); let final_index = if has_end != 0 { @@ -421,11 +511,26 @@ pub extern "C" fn js_array_copy_within_value( }; let receiver_bits = receiver.to_bits() as i64; + let is_proxy = crate::proxy::js_proxy_is_proxy(receiver) != 0; for _ in 0..count { if copy_within_has_property(receiver, from_idx) { - let value = - crate::object::js_object_get_index_polymorphic(receiver_bits, from_idx as f64); - crate::object::js_object_set_index_polymorphic(receiver_bits, to_idx as f64, value); + let value = if is_proxy { + crate::proxy::js_proxy_get(receiver, copy_within_index_key(from_idx)) + } else { + crate::object::js_object_get_index_polymorphic(receiver_bits, from_idx as f64) + }; + if is_proxy { + crate::proxy::js_proxy_set(receiver, copy_within_index_key(to_idx), value); + } else { + crate::object::js_object_set_index_polymorphic(receiver_bits, to_idx as f64, value); + } + } else if is_proxy { + // `DeletePropertyOrThrow` through the Proxy `deleteProperty` trap + // (test262 copyWithin/return-abrupt-from-delete-proxy-target). + let ok = crate::proxy::js_proxy_delete(receiver, copy_within_index_key(to_idx)); + if crate::value::js_is_truthy(ok) == 0 { + throw_copy_within_type_error(b"Cannot delete property"); + } } else { let obj = unsafe { crate::object::extract_obj_ptr(receiver) }; if !obj.is_null() && crate::object::js_object_delete_dynamic(obj, to_idx as f64) == 0 { diff --git a/crates/perry-runtime/src/array/indexing.rs b/crates/perry-runtime/src/array/indexing.rs index 1b25f3e00c..2caa5f50af 100644 --- a/crates/perry-runtime/src/array/indexing.rs +++ b/crates/perry-runtime/src/array/indexing.rs @@ -26,6 +26,63 @@ const DENSE_ARRAY_GAP_LIMIT: u32 = 1024; static ARRAY_PROTO_ADDR: AtomicUsize = AtomicUsize::new(usize::MAX); static ARRAY_PROTO_HAS_INDEX: AtomicBool = AtomicBool::new(false); +/// Same idea for `Object.prototype`: a numeric index installed there +/// (`Object.prototype[2] = 2`, or a defineProperty accessor) shows through +/// array HOLES and OOB reads (chain: arr → Array.prototype → +/// Object.prototype; test262 concat/S15.4.4.4_A3_T3). Flipped by the object +/// index-write/defineProperty hooks; consulted by the typed-feedback guards +/// and the hole/OOB read fallbacks. +static OBJECT_PROTO_ADDR: AtomicUsize = AtomicUsize::new(usize::MAX); +static OBJECT_PROTO_HAS_INDEX: AtomicBool = AtomicBool::new(false); + +fn object_prototype_addr() -> usize { + let cached = OBJECT_PROTO_ADDR.load(Ordering::Relaxed); + if cached != usize::MAX { + return cached; + } + let ctor = crate::object::js_get_global_this_builtin_value(b"Object".as_ptr(), 6); + let ctor_value = crate::value::JSValue::from_bits(ctor.to_bits()); + let addr = if ctor_value.is_pointer() { + let ctor_ptr = ctor_value.as_pointer::() as usize; + let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); + let proto_value = crate::value::JSValue::from_bits(proto.to_bits()); + if proto_value.is_pointer() { + proto_value.as_pointer::() as usize + } else { + 0 + } + } else { + 0 + }; + // Cache only a successful resolution — an early call (before globalThis + // init) must retry later rather than pinning 0. + if addr != 0 { + OBJECT_PROTO_ADDR.store(addr, Ordering::Relaxed); + } + addr +} + +/// Record (if `obj` is the canonical `Object.prototype`) that it now carries +/// an indexed property. Called from the object index-write / numeric +/// defineProperty paths; cheap (relaxed loads + compare). +#[inline] +pub(crate) fn note_object_prototype_index_write(obj: usize) { + if !OBJECT_PROTO_HAS_INDEX.load(Ordering::Relaxed) && obj != 0 && obj == object_prototype_addr() + { + OBJECT_PROTO_HAS_INDEX.store(true, Ordering::Relaxed); + } +} + +pub(crate) fn object_prototype_has_index_flag() -> bool { + OBJECT_PROTO_HAS_INDEX.load(Ordering::Relaxed) +} + +/// `true` when `addr` is the canonical `Object.prototype` (cheap: cached +/// atomic + compare; lazily computes the address on first use). +pub(crate) fn object_prototype_addr_matches(addr: usize) -> bool { + addr != 0 && addr == object_prototype_addr() +} + fn array_prototype_addr() -> usize { let cached = ARRAY_PROTO_ADDR.load(Ordering::Relaxed); if cached != usize::MAX { @@ -66,18 +123,32 @@ pub(crate) fn note_array_index_write(arr: usize) { #[inline] unsafe fn array_oob_prototype_get(receiver: usize, index: u32) -> f64 { const TAG_UNDEFINED_F64: f64 = f64::from_bits(0x7FFC_0000_0000_0001u64); - if !ARRAY_PROTO_HAS_INDEX.load(Ordering::Relaxed) { - return TAG_UNDEFINED_F64; + // A custom array [[Prototype]] (Object.setPrototypeOf(arr, otherArray)) + // replaces the default chain — gated on a global relaxed flag. + if crate::object::prototype_chain::array_static_proto_recorded() { + if let Some(proto_arr) = array_custom_array_prototype(receiver as *const ArrayHeader) { + if index < (*proto_arr).length && array_has_own_index(proto_arr, index) { + return js_array_get_f64(proto_arr, index); + } + } } - let proto = array_prototype_addr(); - if proto == 0 || proto == receiver { - return TAG_UNDEFINED_F64; + if ARRAY_PROTO_HAS_INDEX.load(Ordering::Relaxed) { + let proto = array_prototype_addr(); + if proto != 0 && proto != receiver { + let proto_arr = proto as *const ArrayHeader; + if index < (*proto_arr).length && array_has_own_index(proto_arr, index) { + return js_array_get_f64(proto_arr, index); + } + } } - let proto_arr = proto as *const ArrayHeader; - if index >= (*proto_arr).length { - return TAG_UNDEFINED_F64; + // Object.prototype indexed property (data or defineProperty accessor): + // arr → Array.prototype → Object.prototype (concat/S15.4.4.4_A3_T3). + if OBJECT_PROTO_HAS_INDEX.load(Ordering::Relaxed) + && crate::array::object_prototype_has_index_prop(index) + { + return crate::array::sort_object_prototype_index_get(index); } - js_array_get_f64(proto_arr, index) + TAG_UNDEFINED_F64 } #[inline] @@ -125,7 +196,7 @@ pub(crate) fn array_iteration_is_exotic(arr: *const ArrayHeader) -> bool { /// Spec `OrdinaryGetOwnProperty(O, ToString(index)) != undefined` for an Array: /// is `index` present as an *own* property (dense non-hole slot, sparse named /// data property, or an accessor descriptor)? -unsafe fn array_has_own_index(arr: *const ArrayHeader, index: u32) -> bool { +pub(crate) unsafe fn array_has_own_index(arr: *const ArrayHeader, index: u32) -> bool { if crate::object::descriptors_in_use() { let key = index.to_string(); if crate::object::get_accessor_descriptor(arr as usize, &key).is_some() { @@ -156,6 +227,14 @@ pub(crate) fn array_spec_has_index(arr: *const ArrayHeader, index: u32) -> bool if array_has_own_index(arr, index) { return true; } + // An explicit `Object.setPrototypeOf(arr, otherArray)` replaces the + // default chain — consult that array's own indices first (test262 + // copyWithin/coerced-values-start-change-*). + if let Some(proto_arr) = array_custom_array_prototype(arr) { + if index < (*proto_arr).length && array_has_own_index(proto_arr, index) { + return true; + } + } if ARRAY_PROTO_HAS_INDEX.load(Ordering::Relaxed) { let proto = array_prototype_addr(); if proto != 0 && proto != arr as usize { @@ -165,10 +244,39 @@ pub(crate) fn array_spec_has_index(arr: *const ArrayHeader, index: u32) -> bool } } } + if OBJECT_PROTO_HAS_INDEX.load(Ordering::Relaxed) + && crate::array::object_prototype_has_index_prop(index) + { + return true; + } false } } +/// A custom `[[Prototype]]` installed on `arr` via `Object.setPrototypeOf` +/// that happens to be a real array — `null` otherwise. +unsafe fn array_custom_array_prototype(arr: *const ArrayHeader) -> Option<*const ArrayHeader> { + let bits = crate::object::prototype_chain::object_static_prototype(arr as usize)?; + // The recorded proto may be NaN-boxed (0x7FFD) or a RAW untagged pointer + // (module-level arrays are stored as raw I64s). + let raw = if (bits >> 48) == 0x7FFD { + (bits & crate::value::POINTER_MASK) as usize + } else if (bits >> 48) == 0 && bits > 0x10000 { + bits as usize + } else { + return None; + }; + if raw < crate::gc::GC_HEADER_SIZE + 0x1000 || raw == arr as usize { + return None; + } + let hdr = (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + if (*hdr).obj_type == crate::gc::GC_TYPE_ARRAY { + Some(raw as *const ArrayHeader) + } else { + None + } +} + /// Spec `[[Get]]`(O, ToString(index)) for an ordinary Array receiver: own value /// (firing index accessors via `js_array_get_f64`) or, for an absent own index, /// the inherited `Array.prototype[index]`. Returns `undefined` when absent. @@ -182,6 +290,11 @@ pub(crate) fn array_spec_get(arr: *const ArrayHeader, index: u32) -> f64 { if array_has_own_index(arr, index) { return js_array_get_f64(arr, index); } + if let Some(proto_arr) = array_custom_array_prototype(arr) { + if index < (*proto_arr).length && array_has_own_index(proto_arr, index) { + return js_array_get_f64(proto_arr, index); + } + } if ARRAY_PROTO_HAS_INDEX.load(Ordering::Relaxed) { let proto = array_prototype_addr(); if proto != 0 && proto != arr as usize { @@ -191,6 +304,11 @@ pub(crate) fn array_spec_get(arr: *const ArrayHeader, index: u32) -> f64 { } } } + if OBJECT_PROTO_HAS_INDEX.load(Ordering::Relaxed) + && crate::array::object_prototype_has_index_prop(index) + { + return crate::array::sort_object_prototype_index_get(index); + } TAG_UNDEFINED_F64 } } @@ -243,6 +361,28 @@ pub extern "C" fn js_array_length(arr: *const ArrayHeader) -> u32 { if !raw_ptr.is_null() && (raw_ptr as usize) >= crate::gc::GC_HEADER_SIZE + 0x1000 { let gc_header = (raw_ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + // Runtime plain-object receiver behind a statically-Array + // variable (`var x = []; … x = {0:0}; x.length` — test262 + // splice/S15.4.4.12_A4_T1 #10): reading the ObjectHeader words + // as (length, capacity) returns garbage. Read the `length` + // property like any object instead. + if crate::value::addr_class::is_above_handle_band(raw_ptr as usize) + && crate::object::is_valid_obj_ptr(raw_ptr as *const u8) + && ((*gc_header).obj_type == crate::gc::GC_TYPE_OBJECT + || (*gc_header).obj_type == crate::gc::GC_TYPE_CLOSURE) + { + let key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); + let v = crate::object::js_object_get_field_by_name_f64( + raw_ptr as *const crate::object::ObjectHeader, + key, + ); + let n = crate::builtins::js_number_coerce(v); + return if n.is_nan() || n <= 0.0 { + 0 + } else { + n.min(u32::MAX as f64) as u32 + }; + } if (*gc_header).obj_type == crate::gc::GC_TYPE_LAZY_ARRAY { let lazy = raw_ptr as *const crate::json_tape::LazyArrayHeader; if (*lazy).magic == crate::json_tape::LAZY_ARRAY_MAGIC { @@ -467,14 +607,31 @@ pub extern "C" fn js_array_get_f64(arr: *const ArrayHeader, index: u32) -> f64 { let elements_ptr = (arr as *const u8).add(std::mem::size_of::()) as *const f64; let raw = *elements_ptr.add(index as usize); // Issue #323: translate HOLE sentinel back to `undefined` (see - // `js_array_alloc_with_length` for context). + // `js_array_alloc_with_length` for context). Per OrdinaryGet a hole + // falls through to the prototype chain — a custom array prototype or + // an `Array.prototype[i]` element shows through (test262 + // concat/S15.4.4.4_A3_T2 reads `a[2]` with a hole at 2). Both probes + // are gated (registry lookup / relaxed atomic) so the dense hot path + // is unchanged. if raw.to_bits() == crate::value::TAG_HOLE { - return TAG_UNDEFINED_F64; + if let Some(proto_arr) = array_custom_array_prototype(arr) { + if index < (*proto_arr).length && array_has_own_index(proto_arr, index) { + return js_array_get_f64(proto_arr, index); + } + } + return array_oob_prototype_get(arr as usize, index); } raw } } +/// Relaxed read of the `Array.prototype`-has-indexed-properties flag, for the +/// typed-feedback guards (a polluted prototype invalidates the raw-slot fast +/// path: holes must read through the chain). +pub(crate) fn array_prototype_has_index_flag() -> bool { + ARRAY_PROTO_HAS_INDEX.load(Ordering::Relaxed) +} + /// Fast-path array element write: skips all polymorphic registry checks /// (buffer). Only does bounds checking and element write. /// Use when the codegen KNOWS the pointer is a plain Array (not Buffer). diff --git a/crates/perry-runtime/src/array/iter_methods.rs b/crates/perry-runtime/src/array/iter_methods.rs index 85cd052fea..c3a3aed29d 100644 --- a/crates/perry-runtime/src/array/iter_methods.rs +++ b/crates/perry-runtime/src/array/iter_methods.rs @@ -38,6 +38,27 @@ unsafe fn array_element_get_value(elements_ptr: *const f64, index: usize) -> f64 } } +/// Bind the callback's `this` to `undefined` for the duration of a dense +/// iteration (spec: absent `thisArg` means the callback's `this` is +/// `undefined` — NOT whatever ambient receiver the enclosing call left in +/// IMPLICIT_THIS; test262 some/15.4.4.17-5-25, filter/15.4.4.20-5-30). +/// Explicit-`thisArg` call sites route through the `js_arraylike_*` engine +/// instead of these helpers. Arrow callbacks capture `this` lexically and +/// are unaffected. +struct DenseThisGuard(f64); +impl DenseThisGuard { + fn bind_undefined() -> Self { + DenseThisGuard(crate::object::js_implicit_this_set(f64::from_bits( + crate::value::TAG_UNDEFINED, + ))) + } +} +impl Drop for DenseThisGuard { + fn drop(&mut self) { + crate::object::js_implicit_this_set(self.0); + } +} + /// forEach - call callback(element, index) for each element /// Returns nothing (void) #[no_mangle] @@ -56,6 +77,7 @@ pub extern "C" fn js_array_forEach(arr: *const ArrayHeader, callback: *const Clo unsafe { let length = (*arr).length; let arr_value = array_receiver_value(arr); + let _tg = DenseThisGuard::bind_undefined(); if crate::array::array_iteration_is_exotic(arr) { for i in 0..length as usize { if !crate::array::array_spec_has_index(arr, i as u32) { @@ -103,6 +125,7 @@ pub extern "C" fn js_array_map( let length = (*arr).length; let elements_ptr = array_elements_ptr(arr); let arr_value = array_receiver_value(arr); + let _tg = DenseThisGuard::bind_undefined(); // ECMA-262 §23.1.3.20 step 5: ArraySpeciesCreate(O, len) runs BEFORE // the iteration — it reads `O.constructor` / `@@species` (firing any @@ -165,6 +188,7 @@ pub extern "C" fn js_array_map_discard(arr: *const ArrayHeader, callback: *const unsafe { let length = (*arr).length; let arr_value = array_receiver_value(arr); + let _tg = DenseThisGuard::bind_undefined(); if crate::array::array_iteration_is_exotic(arr) { for i in 0..length as usize { if !crate::array::array_spec_has_index(arr, i as u32) { @@ -206,6 +230,7 @@ pub extern "C" fn js_array_filter( let length = (*arr).length; let elements_ptr = array_elements_ptr(arr); let arr_value = array_receiver_value(arr); + let _tg = DenseThisGuard::bind_undefined(); // ECMA-262 §23.1.3.7 step 5: ArraySpeciesCreate(O, 0) runs before the // iteration (validates `O.constructor` / `@@species`, throwing on a @@ -263,6 +288,7 @@ pub extern "C" fn js_array_find(arr: *const ArrayHeader, callback: *const Closur let length = (*arr).length; let elements_ptr = array_elements_ptr(arr); let arr_value = array_receiver_value(arr); + let _tg = DenseThisGuard::bind_undefined(); let exotic = crate::array::array_iteration_is_exotic(arr); for i in 0..length as usize { @@ -304,6 +330,7 @@ pub extern "C" fn js_array_findIndex( let length = (*arr).length; let elements_ptr = array_elements_ptr(arr); let arr_value = array_receiver_value(arr); + let _tg = DenseThisGuard::bind_undefined(); let exotic = crate::array::array_iteration_is_exotic(arr); for i in 0..length as usize { @@ -344,6 +371,7 @@ pub extern "C" fn js_array_find_last( let length = (*arr).length as usize; let elements_ptr = array_elements_ptr(arr); let arr_value = array_receiver_value(arr); + let _tg = DenseThisGuard::bind_undefined(); let exotic = crate::array::array_iteration_is_exotic(arr); for i in (0..length).rev() { let element = if exotic { @@ -381,6 +409,7 @@ pub extern "C" fn js_array_find_last_index( let length = (*arr).length as usize; let elements_ptr = array_elements_ptr(arr); let arr_value = array_receiver_value(arr); + let _tg = DenseThisGuard::bind_undefined(); let exotic = crate::array::array_iteration_is_exotic(arr); for i in (0..length).rev() { let element = if exotic { @@ -463,6 +492,7 @@ pub extern "C" fn js_array_some(arr: *const ArrayHeader, callback: *const Closur let length = (*arr).length; let elements_ptr = array_elements_ptr(arr); let arr_value = array_receiver_value(arr); + let _tg = DenseThisGuard::bind_undefined(); let exotic = crate::array::array_iteration_is_exotic(arr); for i in 0..length as usize { @@ -507,6 +537,7 @@ pub extern "C" fn js_array_every(arr: *const ArrayHeader, callback: *const Closu let length = (*arr).length; let elements_ptr = array_elements_ptr(arr); let arr_value = array_receiver_value(arr); + let _tg = DenseThisGuard::bind_undefined(); let exotic = crate::array::array_iteration_is_exotic(arr); for i in 0..length as usize { @@ -548,6 +579,7 @@ pub extern "C" fn js_array_flatMap( let mut result = js_array_alloc(length); let arr_value = array_receiver_value(arr); + let _tg = DenseThisGuard::bind_undefined(); for i in 0..length as usize { let Some(element) = present_array_element(elements_ptr, i) else { diff --git a/crates/perry-runtime/src/array/mod.rs b/crates/perry-runtime/src/array/mod.rs index 5698fd22a8..1896b818fb 100644 --- a/crates/perry-runtime/src/array/mod.rs +++ b/crates/perry-runtime/src/array/mod.rs @@ -42,14 +42,16 @@ pub use self::from_concat::{ }; pub use self::generic::array_proto_mutator; pub use self::generic::{ - js_arraylike_at, js_arraylike_every, js_arraylike_filter, js_arraylike_find, - js_arraylike_findIndex, js_arraylike_findLast, js_arraylike_findLastIndex, + js_arraylike_at, js_arraylike_concat, js_arraylike_every, js_arraylike_filter, + js_arraylike_find, js_arraylike_findIndex, js_arraylike_findLast, js_arraylike_findLastIndex, js_arraylike_forEach, js_arraylike_includes, js_arraylike_indexOf, js_arraylike_join, js_arraylike_lastIndexOf, js_arraylike_map, js_arraylike_reduce, js_arraylike_reduceRight, - js_arraylike_slice, js_arraylike_some, try_object_arraylike_mutator, + js_arraylike_slice, js_arraylike_some, js_arraylike_sort, js_arraylike_splice, + try_array_proto_chain_method, try_object_arraylike_mutator, }; pub(crate) use self::generic::{ - object_pop as generic_object_pop, object_shift as generic_object_shift, plain_object_value, + non_array_object_receiver, object_pop as generic_object_pop, + object_shift as generic_object_shift, object_sort, object_splice, plain_object_value, }; pub(crate) use self::header::{array_has_arguments_object_flag, mark_array_as_arguments_object}; pub use self::header::{ @@ -63,7 +65,11 @@ pub use self::immutable::{ js_array_to_sorted_default, js_array_to_sorted_with_comparator, js_array_to_spliced, js_array_with, }; -pub(crate) use self::indexing::{array_iteration_is_exotic, array_spec_get, array_spec_has_index}; +pub(crate) use self::indexing::{ + array_has_own_index, array_iteration_is_exotic, array_prototype_has_index_flag, array_spec_get, + array_spec_has_index, note_object_prototype_index_write, object_prototype_addr_matches, + object_prototype_has_index_flag, +}; pub use self::indexing::{ js_array_get_element, js_array_get_element_f64, js_array_get_f64, js_array_get_f64_unchecked, js_array_get_index_or_string, js_array_get_length, js_array_length, @@ -89,6 +95,8 @@ pub(crate) use self::iterator::is_builtin_iterator_class_id; pub use self::iterator::{ js_array_spread_append, js_for_of_to_array, js_get_async_iterator, js_iterator_to_array, }; +pub(crate) use self::sort::object_prototype_has_index_prop; +pub(crate) use self::sort::object_prototype_index_get as sort_object_prototype_index_get; // Issue #1572 — flatten helpers reused by `node_stream::ns_iter_flat_map` // so an `async function*` mapper return is driven through the iterator // protocol instead of being appended as a single chunk. diff --git a/crates/perry-runtime/src/array/push_pop.rs b/crates/perry-runtime/src/array/push_pop.rs index 123a66cd36..01d23c50f0 100644 --- a/crates/perry-runtime/src/array/push_pop.rs +++ b/crates/perry-runtime/src/array/push_pop.rs @@ -20,7 +20,7 @@ fn throw_frozen_array_mutation() -> ! { /// this covers the non-writable-`length`-only case. (test262 /// Array.prototype.{push,pop,shift,unshift}/set-length-*-non-writable.) #[inline] -fn array_length_is_non_writable(arr: *const ArrayHeader) -> bool { +pub(crate) fn array_length_is_non_writable(arr: *const ArrayHeader) -> bool { let flags = array_object_flags(arr); flags & crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS != 0 && crate::object::get_property_attrs(arr as usize, "length") @@ -36,7 +36,7 @@ fn throw_non_writable_length() -> ! { } #[inline] -fn guard_writable_length(arr: *const ArrayHeader) { +pub(crate) fn guard_writable_length(arr: *const ArrayHeader) { if array_length_is_non_writable(arr) { throw_non_writable_length(); } diff --git a/crates/perry-runtime/src/array/sort.rs b/crates/perry-runtime/src/array/sort.rs index cf19524f9f..4e60626692 100644 --- a/crates/perry-runtime/src/array/sort.rs +++ b/crates/perry-runtime/src/array/sort.rs @@ -1,16 +1,427 @@ -//! Mutating sort — default + comparator. +//! Mutating sort — default + comparator, plus the spec-ops path for exotic +//! receivers (index accessors, sparse storage, inherited prototype elements). use super::*; use crate::closure::{js_closure_call2, ClosureHeader}; use std::ptr; +// --------------------------------------------------------------------------- +// SortCompare helpers shared by the dense fast paths, the exotic spec path, +// and the generic array-like engine (`object_sort`). +// --------------------------------------------------------------------------- + +/// Resolved comparator state: the closure + (when shapes allow) the direct +/// 2-arg call target hoisted out of the comparison loops. +#[derive(Clone, Copy)] +pub(crate) struct ComparatorCall { + comparator: *const ClosureHeader, + direct: Option f64>, +} + +impl ComparatorCall { + pub(crate) fn new(comparator: *const ClosureHeader) -> Self { + ComparatorCall { + comparator, + direct: crate::closure::resolve_call2_direct(comparator), + } + } + + /// Raw `Call(comparator, undefined, « a, b »)`. + #[inline(always)] + fn call_raw(&self, a: f64, b: f64) -> f64 { + match self.direct { + Some(f) => f(self.comparator, a, b), + None => js_closure_call2(self.comparator, a, b), + } + } + + /// ECMA-262 `CompareArrayElements` numeric result: `ToNumber(Call(...))` + /// with NaN → +0. A plain finite/±inf f64 result skips the coercion; any + /// NaN-boxed value (boolean, string, object with `valueOf`, undefined) + /// goes through real ToNumber (firing user `valueOf`, throwing on + /// BigInt/Symbol per spec). + #[inline(always)] + pub(crate) fn compare(&self, a: f64, b: f64) -> f64 { + let r = self.call_raw(a, b); + if r == r { + return r; + } + let n = crate::builtins::js_number_coerce(r); + if n.is_nan() { + 0.0 + } else { + n + } + } +} + +/// ToString(value) as an owned Rust `String` for the default (no-comparator) +/// sort's lexicographic key. Fires user `toString`/`valueOf` via the runtime +/// ToString machinery. +fn sort_key_string(value: f64) -> String { + use crate::string::StringHeader; + use crate::value::js_jsvalue_to_string; + let str_ptr = js_jsvalue_to_string(value); + if str_ptr.is_null() { + return String::new(); + } + unsafe { + let header = &*(str_ptr as *const StringHeader); + let bytes_ptr = (str_ptr as *const u8).add(std::mem::size_of::()); + let slice = std::slice::from_raw_parts(bytes_ptr, header.byte_len as usize); + std::str::from_utf8(slice).unwrap_or("").to_string() + } +} + +#[inline(always)] +fn is_undefined_bits(bits: u64) -> bool { + bits == crate::value::TAG_UNDEFINED +} + +/// Stable bottom-up merge sort over a raw f64 buffer pair using `le(a, b)` +/// ("a sorts at-or-before b"). Tolerant of an inconsistent user comparator +/// (never panics, unlike `slice::sort_by` which detects total-order +/// violations). `src` and `dst` must each hold `n` elements; the sorted run +/// is guaranteed to end up back in `src`'s buffer. +/// +/// SAFETY: caller keeps both buffers alive (and GC-visible when the values +/// are NaN-boxed pointers and `le` can allocate — see the rooted temp-array +/// usage in the spec path). +unsafe fn stable_merge_sort_raw( + src0: *mut f64, + dst0: *mut f64, + n: usize, + mut le: impl FnMut(f64, f64) -> bool, +) { + if n <= 1 { + return; + } + let mut src = src0; + let mut dst = dst0; + let mut width = 1usize; + while width < n { + let mut i = 0; + while i < n { + let left = i; + let mid = (i + width).min(n); + let right = (i + 2 * width).min(n); + let (mut l, mut r, mut k) = (left, mid, left); + // GC_STORE_AUDIT(STACK): merge writes target caller-rooted scratch buffers. + while l < mid && r < right { + if le(*src.add(l), *src.add(r)) { + *dst.add(k) = *src.add(l); + l += 1; + } else { + *dst.add(k) = *src.add(r); + r += 1; + } + k += 1; + } + // GC_STORE_AUDIT(STACK): tail copies target caller-rooted scratch buffers. + while l < mid { + *dst.add(k) = *src.add(l); + l += 1; + k += 1; + } + while r < right { + *dst.add(k) = *src.add(r); + r += 1; + k += 1; + } + i += 2 * width; + } + std::mem::swap(&mut src, &mut dst); + width *= 2; + } + if src != src0 { + // GC_STORE_AUDIT(STACK): final copy between the two caller-rooted buffers. + ptr::copy_nonoverlapping(src, src0, n); + } +} + +/// Sort `defined` (no holes / no undefined) per `SortCompare`: with the user +/// comparator when present, else the default ToString lexicographic order. +/// The default path materializes each key once (eager), matching the previous +/// behavior; `String` keys make the Rust sort total (never panics). +fn sort_defined_values(defined: &mut [f64], scratch: *mut f64, cmp: Option) { + let n = defined.len(); + if n <= 1 { + return; + } + match cmp { + Some(c) => unsafe { + stable_merge_sort_raw(defined.as_mut_ptr(), scratch, n, |a, b| { + c.compare(a, b) <= 0.0 + }); + }, + None => { + let mut pairs: Vec<(String, f64)> = Vec::with_capacity(n); + for &v in defined.iter() { + pairs.push((sort_key_string(v), v)); + } + pairs.sort_by(|a, b| a.0.cmp(&b.0)); + for (i, (_, v)) in pairs.into_iter().enumerate() { + defined[i] = v; + } + } + } +} + +/// Sort `count` values held in the element buffer of a GC-rooted array (the +/// caller keeps the array pointer on its stack). The scratch buffer is a +/// second rooted array so every value stays GC-visible across comparator +/// calls. Used by the exotic spec path and the generic array-like engine. +pub(crate) unsafe fn sort_rooted_values( + elems: *mut f64, + count: usize, + cmp: Option, +) { + if count <= 1 { + return; + } + let scratch = js_array_alloc_with_length(count as u32); + let scratch_elems = (scratch as *mut u8).add(std::mem::size_of::()) as *mut f64; + match cmp { + Some(c) => { + stable_merge_sort_raw(elems, scratch_elems, count, |a, b| c.compare(a, b) <= 0.0); + } + None => { + let mut defined: Vec = Vec::with_capacity(count); + for i in 0..count { + defined.push(*elems.add(i)); + } + sort_defined_values(&mut defined, scratch_elems, None); + for (i, v) in defined.into_iter().enumerate() { + // GC_STORE_AUDIT(BARRIERED): caller rebuilds the rooted array's layout. + ptr::write(elems.add(i), v); + } + } + } +} + +// --------------------------------------------------------------------------- +// Object.prototype indexed-property probe (sort-local). +// +// `array_spec_has_index` / `array_spec_get` consult own properties and +// `Array.prototype`, but an element can also be inherited from +// `Object.prototype` (`Object.prototype[2] = 4` — test262 +// sort/precise-prototype-element). Probing per call keeps the hot iteration +// predicates in `indexing.rs` untouched; sort is the only caller that pays. +// --------------------------------------------------------------------------- + +fn object_prototype_value() -> Option { + let ctor = crate::object::js_get_global_this_builtin_value(b"Object".as_ptr(), 6); + let ctor_v = crate::value::JSValue::from_bits(ctor.to_bits()); + if !ctor_v.is_pointer() { + return None; + } + let proto = + crate::closure::closure_get_dynamic_prop(ctor_v.as_pointer::() as usize, "prototype"); + let proto_v = crate::value::JSValue::from_bits(proto.to_bits()); + if proto_v.is_pointer() { + Some(proto) + } else { + None + } +} + +/// Own array-index keys of `Object.prototype` (usually empty). Computed once +/// per sort call; the result gates the per-index inherited reads below. +fn object_prototype_numeric_keys() -> Vec { + let Some(proto) = object_prototype_value() else { + return Vec::new(); + }; + let names = crate::object::js_object_get_own_property_names(proto); + let jv = crate::value::JSValue::from_bits(names.to_bits()); + if !jv.is_pointer() { + return Vec::new(); + } + let arr = jv.as_pointer::(); + if arr.is_null() { + return Vec::new(); + } + let mut keys = Vec::new(); + unsafe { + let len = (*arr).length as usize; + let elems = (arr as *const u8).add(std::mem::size_of::()) as *const f64; + for i in 0..len { + let s = sort_key_string(*elems.add(i)); + if !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) { + if let Ok(k) = s.parse::() { + keys.push(k); + } + } + } + } + keys +} + +pub(crate) fn object_prototype_index_get(index: u32) -> f64 { + match object_prototype_value() { + Some(proto) => { + // Fire an accessor getter installed via + // `Object.defineProperty(Object.prototype, '', { get })` — the + // polymorphic read misses the descriptor side table. + let addr = (proto.to_bits() & crate::value::POINTER_MASK) as usize; + if let Some(acc) = crate::object::get_accessor_descriptor(addr, &index.to_string()) { + if acc.get != 0 { + return f64::from_bits( + unsafe { crate::object::invoke_accessor_getter(acc.get, proto) }.bits(), + ); + } + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + crate::object::js_object_get_index_polymorphic(proto.to_bits() as i64, index as f64) + } + None => f64::from_bits(crate::value::TAG_UNDEFINED), + } +} + +/// `true` when `Object.prototype` carries an own `` property (data or +/// accessor). Used by the sort spec path and the `in` operator's array arm. +pub(crate) fn object_prototype_has_index_prop(index: u32) -> bool { + let Some(proto) = object_prototype_value() else { + return false; + }; + let addr = (proto.to_bits() & crate::value::POINTER_MASK) as usize; + if crate::object::get_accessor_descriptor(addr, &index.to_string()).is_some() { + return true; + } + let s = index.to_string(); + let key = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + let key_v = f64::from_bits(crate::value::JSValue::string_ptr(key).bits()); + crate::object::js_object_has_own(proto, key_v).to_bits() == 0x7FFC_0000_0000_0004 +} + +/// Spec `Set(O, ToString(j), v, true)` for the sort write-back: when the +/// receiver has NO own property at `j` and `Object.prototype` carries an +/// accessor there, OrdinarySet walks the chain and invokes that SETTER with +/// the array as receiver (test262 sort/precise-prototype-accessors) — a plain +/// `js_array_set_f64_extend` would create an own data slot instead. +unsafe fn sort_spec_set( + arr: *mut ArrayHeader, + index: u32, + value: f64, + objproto_keys: &[u32], +) -> *mut ArrayHeader { + if objproto_keys.contains(&index) && !crate::array::array_has_own_index(arr, index) { + if let Some(proto) = object_prototype_value() { + let addr = (proto.to_bits() & crate::value::POINTER_MASK) as usize; + if let Some(acc) = crate::object::get_accessor_descriptor(addr, &index.to_string()) { + if acc.set != 0 { + crate::object::invoke_accessor_setter( + acc.set, + crate::value::js_nanbox_pointer(arr as i64), + value, + ); + } + return arr; + } + } + } + js_array_set_f64_extend(arr, index, value) +} + +// --------------------------------------------------------------------------- +// Spec-ops sort path for a real-array receiver (ECMA-262 §23.1.3.30 +// SortIndexedProperties with holes skipped): collect via [[HasProperty]] / +// [[Get]] (own accessors + Array.prototype + Object.prototype), sort the +// collected values (undefined trailing, never fed to the comparator), then +// write back via [[Set]] (firing setters) and [[Delete]] the trailing range. +// --------------------------------------------------------------------------- + +unsafe fn array_sort_spec_path( + arr: *mut ArrayHeader, + cmp: Option, + objproto_keys: &[u32], +) -> *mut ArrayHeader { + let len = (*arr).length; + + // Collect present elements into a GC-rooted temp array (stack-local + // pointer keeps it alive under the conservative scan; its element buffer + // keeps accessor-produced values alive across comparator calls — a plain + // Rust Vec would not be traced). + let temp = js_array_alloc_with_length(len); + let temp_elems = (temp as *mut u8).add(std::mem::size_of::()) as *mut f64; + let mut count = 0usize; + let mut undef_count = 0usize; + for j in 0..len { + let (present, value) = if crate::array::array_spec_has_index(arr, j) { + (true, crate::array::array_spec_get(arr, j)) + } else if objproto_keys.contains(&j) { + (true, object_prototype_index_get(j)) + } else { + (false, 0.0) + }; + if present { + if is_undefined_bits(value.to_bits()) { + undef_count += 1; + } else { + // GC_STORE_AUDIT(BARRIERED): temp collection array is rebuilt below. + ptr::write(temp_elems.add(count), value); + count += 1; + } + } + } + (*temp).length = count as u32; + rebuild_array_layout(temp); + let item_count = count + undef_count; + + // Sort the defined values; the scratch buffer is a second rooted array. + sort_rooted_values(temp_elems, count, cmp); + rebuild_array_layout(temp); + + // Write back via [[Set]] (fires index setters / honors attrs), then + // [[Delete]] the trailing [itemCount, len) range — restoring sparseness. + let mut cur = arr; + for j in 0..count { + cur = sort_spec_set(cur, j as u32, *temp_elems.add(j), objproto_keys); + } + for j in count..item_count { + cur = sort_spec_set( + cur, + j as u32, + f64::from_bits(crate::value::TAG_UNDEFINED), + objproto_keys, + ); + } + for j in item_count..len as usize { + crate::array::js_array_delete(cur, j as u32); + } + cur +} + +/// Whether the dense raw-store sort would diverge from the spec protocol for +/// this receiver. Mirrors `array_iteration_is_exotic`, plus the +/// `Object.prototype` numeric-key pollution case that only matters when the +/// array actually has holes for an inherited element to show through. +unsafe fn sort_needs_spec_path(arr: *const ArrayHeader, objproto_keys: &[u32]) -> bool { + if crate::array::array_iteration_is_exotic(arr) { + return true; + } + if objproto_keys.is_empty() { + return false; + } + let length = (*arr).length as usize; + let elements = (arr as *const u8).add(std::mem::size_of::()) as *const f64; + (0..length).any(|i| (*elements.add(i)).to_bits() == crate::value::TAG_HOLE) +} + /// Array.prototype.sort() default sort with no comparator. Per JS /// semantics, elements are converted to strings and compared -/// lexicographically. Sorts in place and returns the same array pointer. +/// lexicographically; undefined elements trail every defined value and holes +/// trail those (neither is ever compared). Sorts in place and returns the +/// same array pointer. #[no_mangle] pub extern "C" fn js_array_sort_default(arr: *mut ArrayHeader) -> *mut ArrayHeader { - use crate::string::StringHeader; - use crate::value::js_jsvalue_to_string; unsafe { + // Runtime plain-object receiver behind a statically-Array variable + // (test262 sort/S15.4.4.11_A6_T2 #5) — run the generic engine. + // Probe the RAW pointer BEFORE the array-plausibility clean (which + // may NULL an object receiver out, silently no-op'ing the sort). + if let Some(recv) = crate::array::non_array_object_receiver(arr) { + crate::array::object_sort(recv, std::ptr::null()); + return arr; + } let arr = clean_arr_ptr(arr as *const ArrayHeader) as *mut ArrayHeader; if arr.is_null() { return arr; @@ -27,41 +438,7 @@ pub extern "C" fn js_array_sort_default(arr: *mut ArrayHeader) -> *mut ArrayHead arr as *mut crate::typedarray::TypedArrayHeader, ) as *mut ArrayHeader; } - let length = (*arr).length as usize; - if length <= 1 { - return arr; - } - let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; - - // Materialize each element as an owned Rust `String` while keeping the - // original f64 bits. Using strings (not pointer equality) guarantees - // correct ordering for numbers, NaN-boxed strings, booleans, null and - // undefined — matching JS default sort semantics. - let mut pairs: Vec<(String, f64)> = Vec::with_capacity(length); - for i in 0..length { - let val = *elements_ptr.add(i); - let str_ptr = js_jsvalue_to_string(val); - let s = if str_ptr.is_null() { - String::new() - } else { - let header = &*(str_ptr as *const StringHeader); - let bytes_ptr = (str_ptr as *const u8).add(std::mem::size_of::()); - let slice = std::slice::from_raw_parts(bytes_ptr, header.byte_len as usize); - std::str::from_utf8(slice).unwrap_or("").to_string() - }; - pairs.push((s, val)); - } - - // Stable lexicographic sort on the string keys. - pairs.sort_by(|a, b| a.0.cmp(&b.0)); - - for (i, (_, val)) in pairs.into_iter().enumerate() { - // GC_STORE_AUDIT(BARRIERED): default sort writes are followed by layout/barrier rebuild. - *elements_ptr.add(i) = val; - } - rebuild_array_layout(arr); - - arr + sort_array_receiver(arr, None) } } @@ -137,6 +514,12 @@ pub extern "C" fn js_array_sort_with_comparator( return js_array_sort_default(arr); } unsafe { + // Runtime plain-object receiver behind a statically-Array variable — + // probe the RAW pointer before the array-plausibility clean. + if let Some(recv) = crate::array::non_array_object_receiver(arr) { + crate::array::object_sort(recv, comparator); + return arr; + } let arr = clean_arr_ptr(arr as *const ArrayHeader) as *mut ArrayHeader; if arr.is_null() { return arr; @@ -151,161 +534,149 @@ pub extern "C" fn js_array_sort_with_comparator( comparator, ) as *mut ArrayHeader; } - let length = (*arr).length as usize; - if length <= 1 { - return arr; - } - let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; - mark_array_layout_unknown(arr); + sort_array_receiver(arr, Some(ComparatorCall::new(comparator))) + } +} + +/// Shared real-array sort body: route exotic receivers to the spec-ops path, +/// dense receivers to the in-place fast paths (with the hole/undefined +/// partition applied for BOTH the default and comparator sorts). +unsafe fn sort_array_receiver( + arr: *mut ArrayHeader, + cmp: Option, +) -> *mut ArrayHeader { + let objproto_keys = object_prototype_numeric_keys(); + if sort_needs_spec_path(arr, &objproto_keys) { + return array_sort_spec_path(arr, cmp, &objproto_keys); + } + + let length = (*arr).length as usize; + if length <= 1 { + return arr; + } + let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; - // Hoist the closure-dispatch resolution out of the hot loops. - // For a 1.25M-element sort we'd otherwise hit ~50M HashMap lookups - // (per-call rest + arity registry queries inside `js_closure_call2`). - // When the comparator is a plain (a,b) => ... arrow with no captures - // and no rest, `direct_call` is `Some(typed_fn)` and we call it - // unconditionally inside the loop. Falls back to `js_closure_call2` - // for the rare bound-method / rest / over-arity comparators. - let direct_call = crate::closure::resolve_call2_direct(comparator); - - #[inline(always)] - unsafe fn cmp_with( - comparator: *const ClosureHeader, - direct: Option f64>, - a: f64, - b: f64, - ) -> f64 { - match direct { - Some(f) => f(comparator, a, b), - None => js_closure_call2(comparator, a, b), + // ECMAScript SortIndexedProperties + CompareArrayElements: array holes + // are excluded from the sort and trail every element, and `undefined` + // elements sort to the very end (after all defined values) WITHOUT the + // comparator / ToString ever running for them. Detect their presence up + // front; a dense, all-defined array keeps the fast in-place path. + let mut has_special = false; + for i in 0..length { + let bits = (*elements_ptr.add(i)).to_bits(); + if bits == crate::value::TAG_HOLE || is_undefined_bits(bits) { + has_special = true; + break; + } + } + if has_special { + // Gather defined (non-hole, non-undefined) elements; tally the + // undefined and hole counts to re-emit as a trailing suffix. + let mut defined: Vec = Vec::with_capacity(length); + let mut undef_count = 0usize; + let mut hole_count = 0usize; + for i in 0..length { + let v = *elements_ptr.add(i); + let bits = v.to_bits(); + if bits == crate::value::TAG_HOLE { + hole_count += 1; + } else if is_undefined_bits(bits) { + undef_count += 1; + } else { + defined.push(v); } } + // All defined values remain reachable through the (unmodified) array + // storage until the write-back below, so the Vec scratch is safe. + let n = defined.len(); + if n > 1 { + let mut buf: Vec = vec![0.0; n]; + sort_defined_values(&mut defined, buf.as_mut_ptr(), cmp); + } + // Write back: sorted defined values, then `undefined` ×N, then + // holes ×N — restoring the array's exotic sparseness. + mark_array_layout_unknown(arr); + let mut idx = 0usize; + // GC_STORE_AUDIT(BARRIERED): write-back is included in the rebuild below. + for &v in &defined { + *elements_ptr.add(idx) = v; + idx += 1; + } + // GC_STORE_AUDIT(POINTER_FREE): undefined/hole suffix has no child pointer; + // covered by the rebuild below anyway. + for _ in 0..undef_count { + *elements_ptr.add(idx) = f64::from_bits(crate::value::TAG_UNDEFINED); + idx += 1; + } + // GC_STORE_AUDIT(POINTER_FREE): hole suffix has no child pointer. + for _ in 0..hole_count { + *elements_ptr.add(idx) = f64::from_bits(crate::value::TAG_HOLE); + idx += 1; + } + rebuild_array_layout(arr); + return arr; + } - // ECMAScript SortIndexedProperties + CompareArrayElements: array - // holes are excluded from the sort and trail every element, and - // `undefined` elements sort to the very end (after all defined - // values) WITHOUT the comparator ever being called for them. The - // in-place TimSort below would instead feed holes / undefined - // straight to the user comparator and mis-order them (e.g. - // `[3, 1, undefined, 2].sort((a, b) => (a ?? 0) - (b ?? 0))` put - // `undefined` first). Detect their presence up front; a dense, - // all-defined array skips this and keeps the fast in-place path. - let mut has_special = false; + let Some(c) = cmp else { + // Dense all-defined default sort: materialize each element's string + // key once, sort stably on the keys, write back. + let mut pairs: Vec<(String, f64)> = Vec::with_capacity(length); for i in 0..length { - let bits = (*elements_ptr.add(i)).to_bits(); - if bits == crate::value::TAG_HOLE - || crate::value::JSValue::from_bits(bits).is_undefined() - { - has_special = true; - break; - } + let val = *elements_ptr.add(i); + pairs.push((sort_key_string(val), val)); + } + pairs.sort_by(|a, b| a.0.cmp(&b.0)); + mark_array_layout_unknown(arr); + for (i, (_, val)) in pairs.into_iter().enumerate() { + // GC_STORE_AUDIT(BARRIERED): default sort writes are followed by layout/barrier rebuild. + *elements_ptr.add(i) = val; } - if has_special { - // Gather defined (non-hole, non-undefined) elements; tally the - // undefined and hole counts to re-emit as a trailing suffix. - let mut defined: Vec = Vec::with_capacity(length); - let mut undef_count = 0usize; - let mut hole_count = 0usize; - for i in 0..length { - let v = *elements_ptr.add(i); - let bits = v.to_bits(); - if bits == crate::value::TAG_HOLE { - hole_count += 1; - } else if crate::value::JSValue::from_bits(bits).is_undefined() { - undef_count += 1; + rebuild_array_layout(arr); + return arr; + }; + + mark_array_layout_unknown(arr); + + // TimSort-style hybrid: insertion sort for small runs, merge sort for large arrays. + // Stable, O(n log n) worst case. Insertion sort is used for runs <= 32 elements + // because it has lower overhead for small inputs. + const INSERTION_THRESHOLD: usize = 32; + + if length <= INSERTION_THRESHOLD { + // Insertion sort for small arrays + for i in 1..length { + let key = *elements_ptr.add(i); + let mut j = i as isize - 1; + while j >= 0 { + if c.compare(*elements_ptr.add(j as usize), key) > 0.0 { + // GC_STORE_AUDIT(BARRIERED): insertion-sort shift is included in the rebuild below. + ptr::write( + elements_ptr.add((j + 1) as usize), + *elements_ptr.add(j as usize), + ); + j -= 1; } else { - defined.push(v); - } - } - // Stable bottom-up merge sort of the defined elements with the - // user comparator (same stable contract as the in-place path, - // and tolerant of an inconsistent comparator without panicking). - let n = defined.len(); - if n > 1 { - let mut buf: Vec = vec![0.0; n]; - let mut width = 1usize; - let mut src = defined.as_mut_ptr(); - let mut dst = buf.as_mut_ptr(); - while width < n { - let mut i = 0; - while i < n { - let left = i; - let mid = (i + width).min(n); - let right = (i + 2 * width).min(n); - let mut l = left; - let mut r = mid; - let mut k = left; - // GC_STORE_AUDIT(STACK): merge writes go to the `defined`/`buf` Vec temporaries. - while l < mid && r < right { - let cmp = cmp_with(comparator, direct_call, *src.add(l), *src.add(r)); - if cmp <= 0.0 { - *dst.add(k) = *src.add(l); - l += 1; - } else { - *dst.add(k) = *src.add(r); - r += 1; - } - k += 1; - } - // GC_STORE_AUDIT(STACK): tail copies also target the Vec temporaries. - while l < mid { - *dst.add(k) = *src.add(l); - l += 1; - k += 1; - } - // GC_STORE_AUDIT(STACK): right-tail copy targets the Vec temporaries. - while r < right { - *dst.add(k) = *src.add(r); - r += 1; - k += 1; - } - i += 2 * width; - } - std::mem::swap(&mut src, &mut dst); - width *= 2; - } - // Make sure the sorted run lives back in `defined`. - if src != defined.as_mut_ptr() { - // GC_STORE_AUDIT(STACK): copy between the two Vec temporaries. - ptr::copy_nonoverlapping(src, defined.as_mut_ptr(), n); + break; } } - // Write back: sorted defined values, then `undefined` ×N, then - // holes ×N — restoring the array's exotic sparseness. - let mut idx = 0usize; - // GC_STORE_AUDIT(BARRIERED): write-back is included in the rebuild below. - for &v in &defined { - *elements_ptr.add(idx) = v; - idx += 1; - } - // GC_STORE_AUDIT(POINTER_FREE): undefined/hole suffix has no child pointer; - // covered by the rebuild below anyway. - for _ in 0..undef_count { - *elements_ptr.add(idx) = f64::from_bits(crate::value::TAG_UNDEFINED); - idx += 1; - } - // GC_STORE_AUDIT(POINTER_FREE): hole suffix has no child pointer. - for _ in 0..hole_count { - *elements_ptr.add(idx) = f64::from_bits(crate::value::TAG_HOLE); - idx += 1; - } - rebuild_array_layout(arr); - return arr; + // GC_STORE_AUDIT(BARRIERED): insertion-sort key write is included in the rebuild below. + ptr::write(elements_ptr.add((j + 1) as usize), key); } + } else { + // Bottom-up merge sort for large arrays — O(n log n) stable sort + let mut buf: Vec = Vec::with_capacity(length); + buf.set_len(length); - // TimSort-style hybrid: insertion sort for small runs, merge sort for large arrays. - // Stable, O(n log n) worst case. Insertion sort is used for runs <= 32 elements - // because it has lower overhead for small inputs. - const INSERTION_THRESHOLD: usize = 32; - - if length <= INSERTION_THRESHOLD { - // Insertion sort for small arrays - for i in 1..length { + // Phase 1: Sort small runs with insertion sort + let mut run_start = 0; + while run_start < length { + let run_end = (run_start + INSERTION_THRESHOLD).min(length); + for i in (run_start + 1)..run_end { let key = *elements_ptr.add(i); let mut j = i as isize - 1; - while j >= 0 { - let cmp = cmp_with(comparator, direct_call, *elements_ptr.add(j as usize), key); - if cmp > 0.0 { - // GC_STORE_AUDIT(BARRIERED): insertion-sort shift is included in the rebuild below. + while j >= run_start as isize { + if c.compare(*elements_ptr.add(j as usize), key) > 0.0 { + // GC_STORE_AUDIT(BARRIERED): large-sort insertion shift is included in the rebuild below. ptr::write( elements_ptr.add((j + 1) as usize), *elements_ptr.add(j as usize), @@ -315,98 +686,65 @@ pub extern "C" fn js_array_sort_with_comparator( break; } } - // GC_STORE_AUDIT(BARRIERED): insertion-sort key write is included in the rebuild below. + // GC_STORE_AUDIT(BARRIERED): large-sort insertion key write is included in the rebuild below. ptr::write(elements_ptr.add((j + 1) as usize), key); } - } else { - // Bottom-up merge sort for large arrays — O(n log n) stable sort - let mut buf: Vec = Vec::with_capacity(length); - buf.set_len(length); - - // Phase 1: Sort small runs with insertion sort - let mut run_start = 0; - while run_start < length { - let run_end = (run_start + INSERTION_THRESHOLD).min(length); - for i in (run_start + 1)..run_end { - let key = *elements_ptr.add(i); - let mut j = i as isize - 1; - while j >= run_start as isize { - let cmp = - cmp_with(comparator, direct_call, *elements_ptr.add(j as usize), key); - if cmp > 0.0 { - // GC_STORE_AUDIT(BARRIERED): large-sort insertion shift is included in the rebuild below. - ptr::write( - elements_ptr.add((j + 1) as usize), - *elements_ptr.add(j as usize), - ); - j -= 1; - } else { - break; - } - } - // GC_STORE_AUDIT(BARRIERED): large-sort insertion key write is included in the rebuild below. - ptr::write(elements_ptr.add((j + 1) as usize), key); - } - run_start = run_end; - } + run_start = run_end; + } - // Phase 2: Merge runs, doubling width each pass - let buf_ptr = buf.as_mut_ptr(); - let mut width = INSERTION_THRESHOLD; - let mut src = elements_ptr; - let mut dst = buf_ptr; - - while width < length { - let mut i = 0; - while i < length { - let left = i; - let mid = (i + width).min(length); - let right = (i + 2 * width).min(length); - - // Merge [left..mid) and [mid..right) into dst - let mut l = left; - let mut r = mid; - let mut k = left; - // GC_STORE_AUDIT(STACK): merge destination is a function-local Vec buffer, not GC heap. - while l < mid && r < right { - let cmp = cmp_with(comparator, direct_call, *src.add(l), *src.add(r)); - if cmp <= 0.0 { - *dst.add(k) = *src.add(l); - l += 1; - } else { - *dst.add(k) = *src.add(r); - r += 1; - } - k += 1; - } - // GC_STORE_AUDIT(STACK): remaining left run copies into the temporary merge buffer. - while l < mid { + // Phase 2: Merge runs, doubling width each pass. Values are always + // present in either the array storage or the scratch buffer; the + // array itself roots them for the conservative scan. + let mut width = INSERTION_THRESHOLD; + let mut src = elements_ptr; + let mut dst = buf.as_mut_ptr(); + + while width < length { + let mut i = 0; + while i < length { + let left = i; + let mid = (i + width).min(length); + let right = (i + 2 * width).min(length); + + let (mut l, mut r, mut k) = (left, mid, left); + // GC_STORE_AUDIT(STACK): merge destination is a function-local Vec buffer, not GC heap. + while l < mid && r < right { + if c.compare(*src.add(l), *src.add(r)) <= 0.0 { *dst.add(k) = *src.add(l); l += 1; - k += 1; - } - // GC_STORE_AUDIT(STACK): remaining right run copies into the temporary merge buffer. - while r < right { + } else { *dst.add(k) = *src.add(r); r += 1; - k += 1; } - - i += 2 * width; + k += 1; + } + // GC_STORE_AUDIT(STACK): remaining left run copies into the temporary merge buffer. + while l < mid { + *dst.add(k) = *src.add(l); + l += 1; + k += 1; + } + // GC_STORE_AUDIT(STACK): remaining right run copies into the temporary merge buffer. + while r < right { + *dst.add(k) = *src.add(r); + r += 1; + k += 1; } - // Swap src and dst for next pass - std::mem::swap(&mut src, &mut dst); - width *= 2; - } - // If final result is in buf, copy back to elements - if src != elements_ptr { - // GC_STORE_AUDIT(BARRIERED): merge buffer copyback is followed by layout/barrier rebuild. - ptr::copy_nonoverlapping(src, elements_ptr, length); + i += 2 * width; } + // Swap src and dst for next pass + std::mem::swap(&mut src, &mut dst); + width *= 2; } - rebuild_array_layout(arr); - arr + // If final result is in buf, copy back to elements + if src != elements_ptr { + // GC_STORE_AUDIT(BARRIERED): merge buffer copyback is followed by layout/barrier rebuild. + ptr::copy_nonoverlapping(src, elements_ptr, length); + } } + rebuild_array_layout(arr); + + arr } diff --git a/crates/perry-runtime/src/array/species.rs b/crates/perry-runtime/src/array/species.rs index e61b2ee23b..f93e1c0c3c 100644 --- a/crates/perry-runtime/src/array/species.rs +++ b/crates/perry-runtime/src/array/species.rs @@ -47,6 +47,24 @@ fn is_constructor(value: f64) -> bool { /// prototype chain (resolving to `Array.prototype.constructor` = the intrinsic /// `Array` for an ordinary array). Propagates a poisoned-getter exception. unsafe fn read_constructor(original: f64) -> f64 { + // An own `constructor` ACCESSOR installed directly on the array + // (`Object.defineProperty(a, 'constructor', { get })`) lives in the + // descriptor side table, which the generic property read below does not + // consult for array receivers — fire it here (its throw propagates; + // test262 {map,filter,splice,concat}/create-ctor-poisoned). + if crate::object::descriptors_in_use() { + let raw = crate::value::js_nanbox_get_pointer(original) as usize; + if raw != 0 { + if let Some(acc) = crate::object::get_accessor_descriptor(raw, "constructor") { + if acc.get != 0 { + return f64::from_bits( + crate::object::invoke_accessor_getter(acc.get, original).bits(), + ); + } + return f64::from_bits(TAG_UNDEFINED); + } + } + } let key = crate::string::js_string_from_bytes(b"constructor".as_ptr(), 11); let key_v = f64::from_bits(JSValue::string_ptr(key).bits()); crate::object::js_object_get_property_key(original, key_v) diff --git a/crates/perry-runtime/src/array/splice_slice.rs b/crates/perry-runtime/src/array/splice_slice.rs index b98c38c6bb..d73d70960f 100644 --- a/crates/perry-runtime/src/array/splice_slice.rs +++ b/crates/perry-runtime/src/array/splice_slice.rs @@ -20,6 +20,33 @@ pub extern "C" fn js_array_splice( out_arr: *mut *mut ArrayHeader, ) -> *mut ArrayHeader { unsafe { + // Runtime plain-object receiver behind a statically-Array variable + // (`var x = []; … x = {0:0,1:1}; x.splice(1,1)` — test262 + // splice/S15.4.4.12_A4_T1 #7): reading it as an ArrayHeader corrupts + // (and `clean_arr_ptr` may NULL it out, silently no-op'ing); run the + // generic spec engine on the object instead. Probe the RAW pointer + // BEFORE the array-plausibility clean. + if let Some(recv) = crate::array::non_array_object_receiver(arr) { + let mut args: Vec = vec![ + start as f64, + if delete_count == i32::MAX { + f64::INFINITY + } else { + delete_count as f64 + }, + ]; + if !items.is_null() { + for i in 0..items_count as usize { + args.push(*items.add(i)); + } + } + let removed = crate::array::object_splice(recv, args.as_ptr(), args.len()); + let removed_ptr = crate::value::js_nanbox_get_pointer(removed) as *mut ArrayHeader; + if !out_arr.is_null() { + *out_arr = arr; + } + return removed_ptr; + } let arr = clean_arr_ptr_mut(arr); if arr.is_null() { if !out_arr.is_null() { @@ -55,23 +82,33 @@ pub extern "C" fn js_array_splice( let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; - // Copy deleted elements to return array + // Copy deleted elements to return array. ECMA-262 §23.1.3.31 step + // 12.b: each removed index goes through HasProperty/Get — a hole + // backed by an inherited `Array.prototype[k]` element lands as an OWN + // property of the deleted array (test262 splice/S15.4.4.12_A4_T3); + // a genuinely absent index stays a hole. + let spec_read = |i: usize| -> f64 { + let v = *elements_ptr.add(start_idx as usize + i); + if v.to_bits() == crate::value::TAG_HOLE { + let idx = start_idx + i as u32; + if crate::array::array_spec_has_index(arr, idx) { + return crate::array::array_spec_get(arr, idx); + } + } + v + }; if deleted_is_plain { (*deleted).length = actual_delete; let deleted_elements = (deleted as *mut u8).add(std::mem::size_of::()) as *mut f64; for i in 0..actual_delete as usize { // GC_STORE_AUDIT(BARRIERED): deleted-array init is followed by layout/barrier rebuild. - ptr::write( - deleted_elements.add(i), - *elements_ptr.add(start_idx as usize + i), - ); + ptr::write(deleted_elements.add(i), spec_read(i)); } rebuild_array_layout(deleted); } else { for i in 0..actual_delete as usize { - let v = *elements_ptr.add(start_idx as usize + i); - crate::array::species::species_result_set(deleted_box, i, v); + crate::array::species::species_result_set(deleted_box, i, spec_read(i)); } } @@ -106,6 +143,9 @@ pub extern "C" fn js_array_splice( } } + // ECMA-262 §23.1.3.31 step 24: Set(O, "length", …, true) — throws on a + // non-writable `length` (test262 splice/S15.4.4.12_A6.1_T2/T3). + super::push_pop::guard_writable_length(arr); (*arr).length = new_len; rebuild_array_layout(arr); diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index aa286477d5..7bb916f4f3 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -531,7 +531,25 @@ pub extern "C" fn js_set_function_prototype(func: f64, proto: f64) -> u32 { } let gc_header = (proto_ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc_header).obj_type != crate::gc::GC_TYPE_OBJECT { + let obj_type = (*gc_header).obj_type; + // `foo.prototype = new Array(...)` — a real-array prototype can't join + // the class-id machinery (it has no ObjectHeader), but it must not be + // DROPPED: store it as the closure's `prototype` dynamic prop so reads + // reflect it and `js_new_function_construct` links instances to it + // (test262 filter/15.4.4.20-6-*, some/15.4.4.17-8-*, map/15.4.4.19-9-3). + if obj_type == crate::gc::GC_TYPE_ARRAY || obj_type == crate::gc::GC_TYPE_LAZY_ARRAY { + let func_ptr = (func_bits & crate::value::POINTER_MASK) as usize; + if func_ptr != 0 && crate::closure::is_closure_ptr(func_ptr) { + crate::closure::closure_set_dynamic_prop(func_ptr, "prototype", proto); + set_builtin_property_attrs( + func_ptr, + "prototype".to_string(), + PropertyAttrs::new(true, false, false), + ); + } + return 0; + } + if obj_type != crate::gc::GC_TYPE_OBJECT { return 0; } } @@ -2178,12 +2196,44 @@ pub unsafe extern "C" fn js_new_function_construct( // synthetic class id's entry in CLASS_PROTOTYPE_METHODS. let obj_ptr = js_object_alloc(cid, 0); let nan_boxed = crate::value::js_nanbox_pointer(obj_ptr as i64); - let proto = ensure_function_prototype_object(func_value, cid); - if !proto.is_null() { - super::prototype_chain::object_set_static_prototype( - obj_ptr as usize, - crate::value::js_nanbox_pointer(proto as i64).to_bits(), - ); + // A user-assigned `foo.prototype = ` lives as the closure's + // "prototype" dynamic prop; the instance's [[Prototype]] must be THAT + // value — notably a real array (`foo.prototype = new Array(1,2,3)`), + // which `ensure_function_prototype_object` would shadow with a fresh + // empty object (test262 filter/15.4.4.20-6-*, some/15.4.4.17-8-*). + let mut linked_user_proto = false; + { + let fp = (func_value.to_bits() & crate::value::POINTER_MASK) as usize; + if fp != 0 && crate::closure::is_closure_ptr(fp) { + let dyn_proto = crate::closure::closure_get_dynamic_prop(fp, "prototype"); + let dp = JSValue::from_bits(dyn_proto.to_bits()); + if dp.is_pointer() { + let raw = dp.as_pointer::() as usize; + let is_array = raw >= crate::gc::GC_HEADER_SIZE + 0x1000 && { + let hdr = unsafe { + &*((raw - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader) + }; + hdr.obj_type == crate::gc::GC_TYPE_ARRAY + || hdr.obj_type == crate::gc::GC_TYPE_LAZY_ARRAY + }; + if is_array { + super::prototype_chain::object_set_static_prototype( + obj_ptr as usize, + dyn_proto.to_bits(), + ); + linked_user_proto = true; + } + } + } + } + if !linked_user_proto { + let proto = ensure_function_prototype_object(func_value, cid); + if !proto.is_null() { + super::prototype_chain::object_set_static_prototype( + obj_ptr as usize, + crate::value::js_nanbox_pointer(proto as i64).to_bits(), + ); + } } // Only run the constructor body when the callee is recognised as // a closure shape. The codegen LocalGet path widens the route to diff --git a/crates/perry-runtime/src/object/delete_rest.rs b/crates/perry-runtime/src/object/delete_rest.rs index 3d37f9ae32..068dccfddb 100644 --- a/crates/perry-runtime/src/object/delete_rest.rs +++ b/crates/perry-runtime/src/object/delete_rest.rs @@ -144,6 +144,26 @@ pub extern "C" fn js_object_delete_field( } return 1; } + // An accessor-ONLY property (defineProperty get/set with no data + // slot) has no keys_array entry — the scan below would "succeed + // vacuously" while leaving the descriptor in the side table, so + // `delete obj[1]` left a ghost accessor behind (test262 + // map/15.4.4.19-8-b-8: a getter deletes a sibling accessor + // mid-iteration and HasProperty must turn false). + if let Some(name) = super::has_own_helpers::str_from_string_header(key) { + if get_accessor_descriptor(obj as usize, name).is_some() { + if let Some(attrs) = get_property_attrs(obj as usize, name) { + if !attrs.configurable() { + return 0; + } + } + super::clear_accessor_descriptor(obj as usize, name); + super::clear_property_attrs(obj as usize, name); + // defineProperty may ALSO have planted a keys_array + // placeholder entry for the key — fall through to the scan + // below so hasOwnProperty / Object.keys stop seeing it. + } + } let keys = (*obj).keys_array; if keys.is_null() { // No keys array means no fields to delete, but delete "succeeds" vacuously diff --git a/crates/perry-runtime/src/object/field_get_set.rs b/crates/perry-runtime/src/object/field_get_set.rs index b34167206f..4882e2620b 100644 --- a/crates/perry-runtime/src/object/field_get_set.rs +++ b/crates/perry-runtime/src/object/field_get_set.rs @@ -2379,23 +2379,20 @@ pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 { None }; if let Some(idx) = idx { - if idx >= length { - return nanbox_false; - } - let sparse_key = idx.to_string(); - if crate::array::array_named_property_get_by_name(arr, &sparse_key).is_some() { + let _ = length; + // Spec HasProperty: own (dense slot / sparse named prop / + // accessor descriptor) OR inherited — a custom array + // [[Prototype]], `Array.prototype[i]`, or an + // `Object.prototype` index (data or accessor; test262 + // sort/precise-comparefn-throws checks `'2' in array` + // against an Object.prototype accessor). + if crate::array::array_spec_has_index(arr, idx) { return nanbox_true; } - if idx >= (*arr).capacity { - return nanbox_false; - } - let elements = (arr as *const u8) - .add(std::mem::size_of::()) - as *const u64; - if std::ptr::read(elements.add(idx as usize)) == crate::value::TAG_HOLE { - return nanbox_false; + if crate::array::object_prototype_has_index_prop(idx) { + return nanbox_true; } - return nanbox_true; + return nanbox_false; } if key_val.is_any_string() { let key_str = crate::value::js_get_string_pointer_unified(key) @@ -2407,7 +2404,18 @@ pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 { if super::has_own_helpers::array_own_key_present(arr, key_str) { return nanbox_true; } - if super::canonical_array_index(key_name).is_some() { + if let Some(idx) = super::canonical_array_index(key_name) { + // Same spec HasProperty protocol as the + // numeric-key arm above: own + inherited + // (custom array proto / Array.prototype / + // Object.prototype data-or-accessor index; + // test262 sort/precise-comparefn-throws does + // `'2' in array`). + if crate::array::array_spec_has_index(arr, idx) + || crate::array::object_prototype_has_index_prop(idx) + { + return nanbox_true; + } return nanbox_false; } if array_prototype_property_value(key_name, obj_ptr as usize).is_some() 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 03a200ad18..0f06136a9b 100644 --- a/crates/perry-runtime/src/object/field_set_by_name.rs +++ b/crates/perry-runtime/src/object/field_set_by_name.rs @@ -180,6 +180,21 @@ pub extern "C" fn js_object_set_field_by_name( key: *const crate::StringHeader, value: f64, ) { + // `Object.prototype["2"] = v` (stringified-index write) makes the index + // visible through array hole/OOB reads. Cheap gate: one relaxed flag + // load, then an address compare against the cached canonical + // Object.prototype; the digit scan only runs on a match (test262 + // concat/S15.4.4.4_A3_T3). + { + let raw = (obj as u64 & 0x0000_FFFF_FFFF_FFFF) as usize; + if crate::array::object_prototype_addr_matches(raw) && !key.is_null() { + if let Some(name) = unsafe { super::has_own_helpers::str_from_string_header(key) } { + if !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit()) { + crate::array::note_object_prototype_index_write(raw); + } + } + } + } // A `Temporal.*` value is an opaque, immutable NaN-boxed cell that is NOT // an `ObjectHeader` — writing an arbitrary property (e.g. test262's // `instance.constructor = …` subclassing probes) must NOT interpret the diff --git a/crates/perry-runtime/src/object/global_this.rs b/crates/perry-runtime/src/object/global_this.rs index 91d35878bf..9c53ff5628 100644 --- a/crates/perry-runtime/src/object/global_this.rs +++ b/crates/perry-runtime/src/object/global_this.rs @@ -1765,6 +1765,127 @@ extern "C" fn array_prototype_splice_thunk( let args = global_this_rest_array_values(rest); crate::array::array_proto_mutator(this, "splice", args.as_ptr(), args.len()) } +extern "C" fn array_prototype_sort_thunk( + _c: *const crate::closure::ClosureHeader, + comparator: f64, +) -> f64 { + let this = crate::object::js_implicit_this_get(); + crate::array::js_arraylike_sort(this, comparator) +} + +/// Real thunks for the generic `Array.prototype` iteration / search methods, +/// each routing the call-site receiver (IMPLICIT_THIS) through the +/// `js_arraylike_*` engine. These replace the previous noop thunks so a +/// reflective resolution — `Array.prototype.map.call(x, …)` through a stored +/// reference, or a method reached through an object whose [[Prototype]] chain +/// contains a real array (`foo.prototype = new Array(…)`; test262 +/// filter/15.4.4.20-6-*, some/15.4.4.17-8-*) — runs the real algorithm +/// instead of returning garbage. Rest-arg shape (like `push`/`splice` above) +/// keeps the closure call convention independent of the spec `.length`. +macro_rules! array_proto_arraylike_cb_thunk { + ($name:ident, $engine:path) => { + extern "C" fn $name(_c: *const crate::closure::ClosureHeader, rest: f64) -> f64 { + let this = crate::object::js_implicit_this_get(); + let args = global_this_rest_array_values(rest); + let a = |i: usize| { + args.get(i) + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)) + }; + $engine(this, a(0), a(1)) + } + }; +} +array_proto_arraylike_cb_thunk!( + array_proto_forEach_thunk, + crate::array::js_arraylike_forEach +); +array_proto_arraylike_cb_thunk!(array_proto_map_thunk, crate::array::js_arraylike_map); +array_proto_arraylike_cb_thunk!(array_proto_filter_thunk, crate::array::js_arraylike_filter); +array_proto_arraylike_cb_thunk!(array_proto_some_thunk, crate::array::js_arraylike_some); +array_proto_arraylike_cb_thunk!(array_proto_every_thunk, crate::array::js_arraylike_every); +array_proto_arraylike_cb_thunk!(array_proto_find_thunk, crate::array::js_arraylike_find); +array_proto_arraylike_cb_thunk!( + array_proto_findIndex_thunk, + crate::array::js_arraylike_findIndex +); +array_proto_arraylike_cb_thunk!( + array_proto_findLast_thunk, + crate::array::js_arraylike_findLast +); +array_proto_arraylike_cb_thunk!( + array_proto_findLastIndex_thunk, + crate::array::js_arraylike_findLastIndex +); + +macro_rules! array_proto_arraylike_optarg_thunk { + ($name:ident, $engine:path) => { + extern "C" fn $name(_c: *const crate::closure::ClosureHeader, rest: f64) -> f64 { + let this = crate::object::js_implicit_this_get(); + let args = global_this_rest_array_values(rest); + let a = |i: usize| { + args.get(i) + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)) + }; + $engine(this, a(0), (args.len() > 1) as i32, a(1)) + } + }; +} +array_proto_arraylike_optarg_thunk!(array_proto_reduce_thunk, reduce_engine); +array_proto_arraylike_optarg_thunk!(array_proto_reduceRight_thunk, reduce_right_engine); + +// `js_arraylike_reduce*` take (recv, cb, has_init, init) — adapt arg order. +fn reduce_engine(recv: f64, cb: f64, has_init: i32, init: f64) -> f64 { + crate::array::js_arraylike_reduce(recv, cb, has_init, init) +} +fn reduce_right_engine(recv: f64, cb: f64, has_init: i32, init: f64) -> f64 { + crate::array::js_arraylike_reduceRight(recv, cb, has_init, init) +} + +macro_rules! array_proto_arraylike_search_thunk { + ($name:ident, $engine:path) => { + extern "C" fn $name(_c: *const crate::closure::ClosureHeader, rest: f64) -> f64 { + let this = crate::object::js_implicit_this_get(); + let args = global_this_rest_array_values(rest); + let a = |i: usize| { + args.get(i) + .copied() + .unwrap_or(f64::from_bits(crate::value::TAG_UNDEFINED)) + }; + $engine(this, a(0), a(1), (args.len() > 1) as i32) + } + }; +} +array_proto_arraylike_search_thunk!( + array_proto_indexOf_thunk, + crate::array::js_arraylike_indexOf +); +array_proto_arraylike_search_thunk!( + array_proto_lastIndexOf_thunk, + crate::array::js_arraylike_lastIndexOf +); +array_proto_arraylike_search_thunk!( + array_proto_includes_thunk, + crate::array::js_arraylike_includes +); + +extern "C" fn array_proto_at_thunk(_c: *const crate::closure::ClosureHeader, idx: f64) -> f64 { + let this = crate::object::js_implicit_this_get(); + crate::array::js_arraylike_at(this, idx) +} +extern "C" fn array_proto_join_thunk(_c: *const crate::closure::ClosureHeader, sep: f64) -> f64 { + let this = crate::object::js_implicit_this_get(); + crate::array::js_arraylike_join(this, sep) +} +extern "C" fn array_prototype_concat_thunk( + _c: *const crate::closure::ClosureHeader, + rest: f64, +) -> f64 { + let this = crate::object::js_implicit_this_get(); + let args = global_this_rest_array_values(rest); + crate::array::js_arraylike_concat(this, args.as_ptr(), args.len() as i32) +} fn array_buffer_receiver_addr() -> Option { let this_bits = IMPLICIT_THIS.with(|c| c.get()); @@ -6448,30 +6569,12 @@ fn populate_builtin_prototype_methods(builtin_name: &str, proto_obj: *mut Object install_noop_proto_methods( proto_obj, &[ - ("at", 1), - ("concat", 1), ("copyWithin", 2), ("entries", 0), - ("every", 1), ("fill", 1), - ("filter", 1), - ("find", 1), - ("findIndex", 1), - ("findLast", 1), - ("findLastIndex", 1), ("flat", 0), ("flatMap", 1), - ("forEach", 1), - ("includes", 1), - ("indexOf", 1), - ("join", 1), ("keys", 0), - ("lastIndexOf", 1), - ("map", 1), - ("reduce", 1), - ("reduceRight", 1), - ("some", 1), - ("sort", 1), ("toLocaleString", 0), ("toReversed", 0), ("toSorted", 1), @@ -6519,6 +6622,48 @@ fn populate_builtin_prototype_methods(builtin_name: &str, proto_obj: *mut Object 2, 0, ); + // `sort` / `concat` get real thunks too: a borrowed + // `obj.sort = Array.prototype.sort; obj.sort()` must run the + // generic engine on the receiver (test262 sort/S15.4.4.11_A3_T1, + // A4_T3, concat/S15.4.4.4_A2_T1) — the previous noop thunk + // silently returned undefined. + install_proto_method( + proto_obj, + "sort", + array_prototype_sort_thunk as *const u8, + 1, + ); + // Iteration / search methods: real generic-engine thunks (rest + // shape — spec `.length` recorded separately below). + type RestThunk = extern "C" fn(*const crate::closure::ClosureHeader, f64) -> f64; + let arraylike_thunks: [(&str, RestThunk, u32); 14] = [ + ("forEach", array_proto_forEach_thunk, 1), + ("map", array_proto_map_thunk, 1), + ("filter", array_proto_filter_thunk, 1), + ("some", array_proto_some_thunk, 1), + ("every", array_proto_every_thunk, 1), + ("find", array_proto_find_thunk, 1), + ("findIndex", array_proto_findIndex_thunk, 1), + ("findLast", array_proto_findLast_thunk, 1), + ("findLastIndex", array_proto_findLastIndex_thunk, 1), + ("reduce", array_proto_reduce_thunk, 1), + ("reduceRight", array_proto_reduceRight_thunk, 1), + ("indexOf", array_proto_indexOf_thunk, 1), + ("lastIndexOf", array_proto_lastIndexOf_thunk, 1), + ("includes", array_proto_includes_thunk, 1), + ]; + for (name, thunk, len) in arraylike_thunks { + install_proto_method_rest_with_length(proto_obj, name, thunk as *const u8, len, 0); + } + install_proto_method(proto_obj, "at", array_proto_at_thunk as *const u8, 1); + install_proto_method(proto_obj, "join", array_proto_join_thunk as *const u8, 1); + install_proto_method_rest_with_length( + proto_obj, + "concat", + array_prototype_concat_thunk as *const u8, + 1, + 0, + ); install_noop_proto_methods(proto_obj, OBJECT_PROTO_METHODS); } "ArrayBuffer" => { diff --git a/crates/perry-runtime/src/object/native_call_method.rs b/crates/perry-runtime/src/object/native_call_method.rs index cd08a63088..f1220c5e60 100644 --- a/crates/perry-runtime/src/object/native_call_method.rs +++ b/crates/perry-runtime/src/object/native_call_method.rs @@ -1372,7 +1372,7 @@ pub unsafe extern "C" fn js_native_call_method( // is handled by the real prototype-method thunks instead.) if matches!( method_name, - "pop" | "shift" | "push" | "unshift" | "reverse" | "splice" + "pop" | "shift" | "push" | "unshift" | "reverse" | "splice" | "sort" | "concat" ) { if let Some(result) = crate::array::try_object_arraylike_mutator(object, method_name, args_ptr, args_len) @@ -1380,6 +1380,42 @@ pub unsafe extern "C" fn js_native_call_method( return result; } } + // A plain object whose [[Prototype]] chain contains a real array + // (`function foo() {}; foo.prototype = new Array(1, 2, 3); new foo()`) + // inherits the `Array.prototype` methods through that array, but the + // field-scan dispatch below finds no own/proto slot for them and threw + // " is not a function" (test262 filter/15.4.4.20-6-*, + // some/15.4.4.17-8-*, map/15.4.4.19-9-3). Route the generic array-like + // engine; receivers with an own user method or no array on the chain + // fall through unchanged. + if matches!( + method_name, + "forEach" + | "map" + | "filter" + | "some" + | "every" + | "find" + | "findIndex" + | "findLast" + | "findLastIndex" + | "reduce" + | "reduceRight" + | "indexOf" + | "lastIndexOf" + | "includes" + | "at" + | "join" + | "slice" + | "sort" + | "concat" + ) { + if let Some(result) = + crate::array::try_array_proto_chain_method(object, method_name, args_ptr, args_len) + { + return result; + } + } // #4795: dynamic dispatch for `DisposableStack` / `AsyncDisposableStack` // instance methods. The codegen fast path handles statically-typed stack // locals, but a stack held in an `any`-typed value — e.g. the result of @@ -2875,14 +2911,14 @@ pub unsafe extern "C" fn js_native_call_method( // inserted at `start`. "splice" => { let arr = raw_ptr as *mut crate::array::ArrayHeader; + // ToIntegerOrInfinity with i32 clamping: NaN → 0, + // +Infinity → i32::MAX (clamps to len downstream), + // -Infinity → i32::MIN (relative-from-end → 0). The + // old `is_infinite() → 0` made `splice(Infinity, 3)` + // delete from the front (test262 S15.4.4.12_A2.1_T3). let arg_i32 = |i: usize| -> i32 { if i < args_len && !args_ptr.is_null() { - let v = *args_ptr.add(i); - if v.is_nan() || v.is_infinite() { - 0 - } else { - v as i32 - } + crate::array::js_array_splice_delete_count(*args_ptr.add(i)) } else { 0 } diff --git a/crates/perry-runtime/src/object/object_ops.rs b/crates/perry-runtime/src/object/object_ops.rs index 4256e91e0e..0877fab9fb 100644 --- a/crates/perry-runtime/src/object/object_ops.rs +++ b/crates/perry-runtime/src/object/object_ops.rs @@ -1250,6 +1250,29 @@ pub extern "C" fn js_object_define_property( return obj_value; } + // A numeric key defined on `Object.prototype` (data or accessor) shows + // through array hole/OOB reads — flip the global flag. + { + let kb = key_value.to_bits(); + let is_numeric_key = + (kb >> 48) == 0x7FFE || crate::value::JSValue::from_bits(kb).is_number() || { + let sp = crate::value::js_get_string_pointer_unified(key_value) + as *const crate::StringHeader; + !sp.is_null() + && super::has_own_helpers::str_from_string_header(sp) + .map(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())) + .unwrap_or(false) + }; + if is_numeric_key { + let ob = obj_value.to_bits(); + if (ob >> 48) == 0x7FFD { + crate::array::note_object_prototype_index_write( + (ob & crate::value::POINTER_MASK) as usize, + ); + } + } + } + // #2817: ES Object.defineProperty validation. // 1. Target must be an object (or class-ref / function — all objects // in Node). Primitives / null / undefined throw. @@ -2893,6 +2916,24 @@ pub extern "C" fn js_object_set_prototype_of(obj_value: f64, proto: f64) -> f64 && is_valid_obj_ptr(obj_ptr_for_record as *const u8) { super::prototype_chain::object_set_static_prototype(obj_ptr_for_record, proto_bits); + // A grown array's local may still hold the FORWARDED (old) pointer; + // the spec [[HasProperty]]/[[Get]] helpers look the prototype up by + // the CLEANED address. Record under both keys so either resolves + // (test262 copyWithin/coerced-values-start-change-* second case). + unsafe { + let hdr = (obj_ptr_for_record as *const u8).sub(crate::gc::GC_HEADER_SIZE) + as *const crate::gc::GcHeader; + if (*hdr).obj_type == crate::gc::GC_TYPE_ARRAY + || (*hdr).obj_type == crate::gc::GC_TYPE_LAZY_ARRAY + { + let cleaned = crate::array::clean_arr_ptr( + obj_ptr_for_record as *const crate::array::ArrayHeader, + ) as usize; + if cleaned != 0 && cleaned != obj_ptr_for_record { + super::prototype_chain::object_set_static_prototype(cleaned, proto_bits); + } + } + } } // Spec: `Object.setPrototypeOf(O, proto)` returns O. diff --git a/crates/perry-runtime/src/object/polymorphic_index.rs b/crates/perry-runtime/src/object/polymorphic_index.rs index dcfe824beb..2977b75e84 100644 --- a/crates/perry-runtime/src/object/polymorphic_index.rs +++ b/crates/perry-runtime/src/object/polymorphic_index.rs @@ -140,6 +140,12 @@ pub extern "C" fn js_object_get_index_polymorphic(obj_handle: i64, idx: f64) -> /// bad-args contract of `js_array_set_f64` / `js_object_set_field_by_name`. #[no_mangle] pub extern "C" fn js_object_set_index_polymorphic(obj_handle: i64, idx: f64, value: f64) { + // `Object.prototype[i] = v` makes the index visible through every array's + // hole/OOB reads — flip the global flag (cheap compare; see + // `note_object_prototype_index_write`). + crate::array::note_object_prototype_index_write( + (obj_handle as u64 & 0x0000_FFFF_FFFF_FFFF) as usize, + ); // Strip NaN-box tags defensively. Codegen calls this with the lower-48 // bits already extracted via `unbox_to_i64`, but match the convention // of every other entry-point so a stray un-stripped caller (or a JIT diff --git a/crates/perry-runtime/src/object/prototype_chain.rs b/crates/perry-runtime/src/object/prototype_chain.rs index 2a81bd62f0..94340fdc52 100644 --- a/crates/perry-runtime/src/object/prototype_chain.rs +++ b/crates/perry-runtime/src/object/prototype_chain.rs @@ -19,8 +19,20 @@ //! when the *owner* object itself is evacuated. use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Mutex, OnceLock}; +/// Set when `Object.setPrototypeOf` has retargeted a REAL ARRAY's +/// [[Prototype]] anywhere in the program. The typed-feedback array guards +/// consult it (one relaxed load) so the inline raw-slot fast path stands +/// down: holes/OOB reads must then walk the custom chain (test262 +/// copyWithin/coerced-values-start-change-*). +static ARRAY_TARGET_PROTO_RECORDED: AtomicBool = AtomicBool::new(false); + +pub(crate) fn array_static_proto_recorded() -> bool { + ARRAY_TARGET_PROTO_RECORDED.load(Ordering::Relaxed) +} + const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; static OBJECT_PROTOTYPES: OnceLock>> = OnceLock::new(); @@ -36,6 +48,20 @@ pub fn object_set_static_prototype(obj_ptr: usize, proto_bits: u64) { if obj_ptr == 0 { return; } + if !ARRAY_TARGET_PROTO_RECORDED.load(Ordering::Relaxed) + && obj_ptr >= crate::gc::GC_HEADER_SIZE + 0x1000 + && crate::value::addr_class::is_above_handle_band(obj_ptr) + && crate::object::is_valid_obj_ptr(obj_ptr as *const u8) + { + let obj_type = unsafe { + let hdr = + (obj_ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + (*hdr).obj_type + }; + if obj_type == crate::gc::GC_TYPE_ARRAY || obj_type == crate::gc::GC_TYPE_LAZY_ARRAY { + ARRAY_TARGET_PROTO_RECORDED.store(true, Ordering::Relaxed); + } + } let mut slot_addr = 0usize; if let Ok(mut map) = get_object_prototypes().lock() { let slot = map.entry(obj_ptr).or_insert(0); diff --git a/crates/perry-runtime/src/typed_feedback.rs b/crates/perry-runtime/src/typed_feedback.rs index b1d8685d4b..6b3b2ecae2 100644 --- a/crates/perry-runtime/src/typed_feedback.rs +++ b/crates/perry-runtime/src/typed_feedback.rs @@ -1047,6 +1047,24 @@ fn plain_array_index_guard(arr: *const ArrayHeader, index: u32, require_in_bound { return false; } + // Index accessors / custom attribute descriptors divert element + // reads and writes through the descriptor tables; the inline + // raw-slot fast path the guard admits would bypass them (test262 + // sort/precise-* read accessor indices after defineProperty). + if (*header)._reserved & crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS != 0 { + return false; + } + // A polluted `Array.prototype[i]` (or custom array prototype) makes + // holes read through the chain — the raw slot load would return + // undefined instead (test262 concat/S15.4.4.4_A3_T2, + // copyWithin/coerced-values-start-change-*). Rare global flags; + // two relaxed atomic loads. + if crate::array::array_prototype_has_index_flag() + || crate::array::object_prototype_has_index_flag() + || crate::object::prototype_chain::array_static_proto_recorded() + { + return false; + } let arr = raw_addr as *const ArrayHeader; let len = (*arr).length; let cap = (*arr).capacity; diff --git a/crates/perry-runtime/src/value/dyn_index.rs b/crates/perry-runtime/src/value/dyn_index.rs index a07b8269cf..41495f01a7 100644 --- a/crates/perry-runtime/src/value/dyn_index.rs +++ b/crates/perry-runtime/src/value/dyn_index.rs @@ -200,10 +200,23 @@ pub extern "C" fn js_dyn_index_get(value: f64, index: f64) -> f64 { format!("{}", index) }; let key = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); - return crate::object::js_object_get_field_by_name_f64( + let v = crate::object::js_object_get_field_by_name_f64( raw_ptr as *const crate::object::ObjectHeader, key, ); + // An indexed property inherited from the canonical + // `Object.prototype` (incl. a defineProperty accessor) shows + // through any object/function receiver — e.g. `Array[1]` after + // `Object.defineProperty(Object.prototype, "1", { get })` + // (test262 filter/15.4.4.20-9-b-6). + if v.to_bits() == crate::value::TAG_UNDEFINED + && idx_i32 >= 0 + && index == (idx_i32 as f64) + && crate::array::object_prototype_has_index_prop(idx_i32 as u32) + { + return crate::array::sort_object_prototype_index_get(idx_i32 as u32); + } + return v; } } if idx_i32 < 0 { @@ -231,6 +244,11 @@ pub extern "C" fn js_dyn_index_get(value: f64, index: f64) -> f64 { pub extern "C" fn js_dyn_index_set(obj: f64, index: f64, value: f64) -> f64 { let bits = obj.to_bits(); let jsval = JSValue::from_bits(bits); + // `Object.prototype[i] = v` (computed write) makes the index visible + // through every array's hole/OOB reads — flip the global flag. + if jsval.is_pointer() { + crate::array::note_object_prototype_index_write((bits & POINTER_MASK) as usize); + } if jsval.is_string() || jsval.is_short_string() { return value; } diff --git a/crates/perry-runtime/src/value/dynamic_object.rs b/crates/perry-runtime/src/value/dynamic_object.rs index 68b7024559..0986bb360a 100644 --- a/crates/perry-runtime/src/value/dynamic_object.rs +++ b/crates/perry-runtime/src/value/dynamic_object.rs @@ -138,7 +138,24 @@ pub extern "C" fn js_value_length_f64(value: f64) -> f64 { ) .unwrap_or(0) as f64; } - // BigInts, Promises, Errors, plain Objects, Maps: no `.length`. + // A plain object CAN carry a `length` property — notably a + // variable whose static type was inferred `Array` but was + // reassigned to an array-like object (`var x = []; … x = {0:0}; + // x.splice(1,1); x.length` — test262 splice/S15.4.4.12_A4_T1 + // #10). Read it like any field; absent stays the 0 fallback. + crate::gc::GC_TYPE_OBJECT => { + let key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); + let v = crate::object::js_object_get_field_by_name_f64( + handle as *const crate::object::ObjectHeader, + key, + ); + if v.to_bits() == crate::value::TAG_UNDEFINED { + return 0.0; + } + let n = crate::builtins::js_number_coerce(v); + return if n.is_nan() { 0.0 } else { n }; + } + // BigInts, Promises, Errors, Maps: no `.length`. // Return 0 to match Perry's existing fallback for missing fields // (JS would produce `undefined`, but the generic PropertyGet slow // path already degrades to 0 here).