diff --git a/crates/oxide_builtins/src/array/common.rs b/crates/oxide_builtins/src/array/common.rs index 9142ebbd..6ffe7e68 100644 --- a/crates/oxide_builtins/src/array/common.rs +++ b/crates/oxide_builtins/src/array/common.rs @@ -177,6 +177,14 @@ pub(crate) fn unit_string_value(vm: &mut H, unit: u16) -> JsValue { } } +/// 索引位置存在性判定(规范 HasProperty:自身与原型链任一层命中即存在, +/// 数组元素区 hole 视同缺失,TypedArray 整数索引按视图长度判在界)。 +pub(crate) fn arraylike_index_present(vm: &mut H, ptr: *mut JsObject, i: usize) -> bool { + // SAFETY: ptr 来自 this/参数对象,指向存活对象。 + let key_si = vm.string_key_si(&i.to_string()); + vm.has_property(unsafe { &*ptr }, key_si) +} + /// 元素读取失败时直接把异常作为 NativeResult::Err 返回。 macro_rules! arraylike_get_or_err { ($vm:expr, $ptr:expr, $i:expr) => { diff --git a/crates/oxide_builtins/src/array/element.rs b/crates/oxide_builtins/src/array/element.rs index 6434c3d5..11810858 100644 --- a/crates/oxide_builtins/src/array/element.rs +++ b/crates/oxide_builtins/src/array/element.rs @@ -9,8 +9,9 @@ use crate::builtins_debug; use crate::builtins_error; use super::common::{ - array_ptr, array_type_error, arraylike_get, arraylike_get_or_err, check_array_create_len, clamp_relative, - create_new_array, get_this_array_ref, get_this_arraylike, js_array_index, to_integer_or_infinity_bounded, + array_ptr, array_type_error, arraylike_get, arraylike_get_or_err, arraylike_index_present, check_array_create_len, + clamp_relative, create_new_array, get_this_array_ref, get_this_arraylike, js_array_index, + to_integer_or_infinity_bounded, }; use super::from::{array_species_create, create_data_property_or_throw, from_engine_error}; @@ -125,28 +126,38 @@ pub fn array_slice(vm: &mut H, args: &[u8]) -> NativeResult { Ok(p) => p, Err(err) => return NativeResult::Err(err), }; - // HasProperty + Get + CreateDataPropertyOrThrow 逐元素拷贝。 + // HasProperty + Get + CreateDataPropertyOrThrow 逐元素拷贝;洞位只在真数组 + // 目标上留洞(目标预填 present-undefined,须显式置洞)。 let mut k = start; let mut out_idx = 0usize; + let mut holes: Vec = Vec::new(); while k < end { - let key_str = vm.new_string(&k.to_string()); - let key_si = vm.property_key_si(key_str); - if vm.resolve_property(unsafe { &*arr_ptr }, key_si).is_some() { + if arraylike_index_present(vm, arr_ptr, k) { let elem = arraylike_get_or_err!(vm, arr_ptr, k); let out_key = vm.new_string(&out_idx.to_string()); let out_key_si = vm.property_key_si(out_key); if let Err(err) = create_data_property_or_throw(vm, unsafe { &mut *a_ptr }, out_key_si, elem) { return NativeResult::Err(err); } - out_idx += 1; + } else { + holes.push(out_idx); } + out_idx += 1; k += 1; } - // 收尾 Set(A, "length", final):普通 Set 语义(可触发继承 setter/异常)。 + // 收尾 Set(A, "length", final):终长 = 区间长度(end - start),洞位计入; + // 普通 Set 语义(可触发继承 setter/异常)。 let a_val = JsValue::from_js_object(a_ptr); + if unsafe { &*a_ptr }.is_array() { + for p in holes { + unsafe { + (*a_ptr).mark_hole_at(p); + } + } + } let length_key = vm.new_string("length"); let length_si = vm.property_key_si(length_key); - if let Err(err) = vm.ordinary_set(unsafe { &mut *a_ptr }, length_si, js_array_index(out_idx), a_val, true) { + if let Err(err) = vm.ordinary_set(unsafe { &mut *a_ptr }, length_si, js_array_index(count), a_val, true) { return NativeResult::Err(from_engine_error(vm, &err)); } NativeResult::Ok(JsValue::from_js_object(a_ptr)) @@ -225,12 +236,19 @@ pub fn array_concat(vm: &mut H, args: &[u8]) -> NativeResult { Ok(v) => v, Err(e) => return NativeResult::Err(e), }; + // this 与数组实参的洞位按缺失复制:结果对应位留洞(记录洞下标,收尾置洞)。 let mut all: Vec = Vec::new(); + let mut holes: Vec = Vec::new(); if let Err(err) = check_array_create_len(vm, n) { return NativeResult::Err(err); } for i in 0..n { - all.push(arraylike_get_or_err!(vm, arr_ptr, i)); + if arraylike_index_present(vm, arr_ptr, i) { + all.push(arraylike_get_or_err!(vm, arr_ptr, i)); + } else { + holes.push(all.len()); + all.push(JsValue::undefined()); + } } for &arg_reg in args.iter().skip(1) { let val = vm.reg(arg_reg); @@ -239,10 +257,16 @@ pub fn array_concat(vm: &mut H, args: &[u8]) -> NativeResult { if !o_ptr.is_null() { let o = unsafe { &*o_ptr }; let on = o.prop_count() as usize; - // 真数组时展开元素。 + // 真数组时展开元素(洞位同样留洞)。 if o.is_array() && on > 0 { + let dense = o.array_elements_meta_vec().is_none(); for i in 0..on { - all.push(o.get_prop_at(i)); + if dense || !o.prop_meta_at(i).is_some_and(|m| m.is_hole()) { + all.push(o.get_prop_at(i)); + } else { + holes.push(all.len()); + all.push(JsValue::undefined()); + } } continue; } @@ -255,6 +279,9 @@ pub fn array_concat(vm: &mut H, args: &[u8]) -> NativeResult { for (i, val) in all.iter().enumerate() { (*new_arr).set_prop_at(i, *val); } + for h in holes { + (*new_arr).mark_hole_at(h); + } (*new_arr).set_prop_count(all.len()); } NativeResult::Ok(JsValue::from_js_object(new_arr)) @@ -329,6 +356,10 @@ pub fn array_index_of(vm: &mut H, args: &[u8]) -> NativeResult { 0 }; for i in from_index..n { + // 洞位不参与比较:HasProperty 缺失即跳过(洞不是 undefined 元素)。 + if !arraylike_index_present(vm, ptr, i) { + continue; + } let elem = arraylike_get_or_err!(vm, ptr, i); if oxide_runtime_api::strict_equality(elem, target) { return NativeResult::Ok(js_array_index(i)); @@ -426,8 +457,15 @@ pub fn array_flat(vm: &mut H, args: &[u8]) -> NativeResult { let o = unsafe { &*ptr }; if o.is_array() { seen.push(ptr); + // 嵌套展开丢弃洞位:flat 结果恒为紧凑数组。 let on = o.prop_count() as usize; - let sub: Vec = (0..on).map(|i| o.get_prop_at(i)).collect(); + let dense = o.array_elements_meta_vec().is_none(); + let mut sub: Vec = Vec::with_capacity(on); + for i in 0..on { + if dense || !o.prop_meta_at(i).is_some_and(|m| m.is_hole()) { + sub.push(o.get_prop_at(i)); + } + } let flat = flatten(&sub, remaining_depth - 1, seen); seen.pop(); out.extend(flat); @@ -440,7 +478,14 @@ pub fn array_flat(vm: &mut H, args: &[u8]) -> NativeResult { out } - let all: Vec = (0..n).map(|i| arr.get_prop_at(i)).collect(); + // 顶层同样丢弃洞位(接收者恒为真数组,元数据表判洞免字符串键)。 + let dense = arr.array_elements_meta_vec().is_none(); + let mut all: Vec = Vec::with_capacity(n); + for i in 0..n { + if dense || !arr.prop_meta_at(i).is_some_and(|m| m.is_hole()) { + all.push(arr.get_prop_at(i)); + } + } let mut seen = vec![arr_ptr]; let flat = flatten(&all, depth, &mut seen); let new_arr = create_new_array(vm, flat.len()); @@ -689,6 +734,10 @@ pub fn array_last_index_of(vm: &mut H, args: &[u8]) -> NativeResult { return NativeResult::Ok(JsValue::int(-1)); } for i in (0..=from_index_isize as usize).rev() { + // 洞位不参与比较:HasProperty 缺失即跳过(洞不是 undefined 元素)。 + if !arraylike_index_present(vm, ptr, i) { + continue; + } let elem = arraylike_get_or_err!(vm, ptr, i); if oxide_runtime_api::strict_equality(elem, search) { return NativeResult::Ok(js_array_index(i)); diff --git a/crates/oxide_builtins/src/array/iterate.rs b/crates/oxide_builtins/src/array/iterate.rs index ad4aec1a..8e849699 100644 --- a/crates/oxide_builtins/src/array/iterate.rs +++ b/crates/oxide_builtins/src/array/iterate.rs @@ -8,8 +8,9 @@ use crate::builtins_debug; use crate::builtins_error; use super::common::{ - array_ptr_len3, array_type_error, arraylike_get, arraylike_get_or_err, check_array_create_len, create_new_array, - get_this_arraylike, invoke_native_callback, js_array_index, require_callback, unexpected_tail_call_error, + array_ptr_len3, array_type_error, arraylike_get, arraylike_get_or_err, arraylike_index_present, + check_array_create_len, create_new_array, get_this_arraylike, invoke_native_callback, js_array_index, + require_callback, unexpected_tail_call_error, }; /// `Array.prototype.forEach(callback, thisArg)`:对每个元素调用 callback,返回 undefined。 @@ -26,6 +27,10 @@ pub fn array_for_each(vm: &mut H, args: &[u8]) -> NativeResult { }; let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; for i in 0..n { + // 洞位跳过:回调不得在缺失索引上触发。 + if !arraylike_index_present(vm, arr_ptr, i) { + continue; + } let elem = arraylike_get_or_err!(vm, arr_ptr, i); match invoke_native_callback(vm, callback_val, this_val, &[elem, js_array_index(i), o_val]) { NativeResult::Ok(_) => {} @@ -54,6 +59,13 @@ pub fn array_map(vm: &mut H, args: &[u8]) -> NativeResult { return NativeResult::Err(err); } for i in 0..n { + // 洞位跳过回调,并在结果对应位留洞(结果数组预填 present-undefined,须显式置洞)。 + if !arraylike_index_present(vm, arr_ptr, i) { + unsafe { + (*new_arr).mark_hole_at(i); + } + continue; + } let elem = arraylike_get_or_err!(vm, arr_ptr, i); match invoke_native_callback(vm, callback_val, this_val, &[elem, js_array_index(i), o_val]) { NativeResult::Ok(mapped) => unsafe { @@ -91,6 +103,10 @@ pub fn array_filter(vm: &mut H, args: &[u8]) -> NativeResult { return NativeResult::Err(err); } for i in 0..n { + // 洞位跳过:结果保持紧凑,不复制缺失索引。 + if !arraylike_index_present(vm, arr_ptr, i) { + continue; + } let elem = arraylike_get_or_err!(vm, arr_ptr, i); match invoke_native_callback(vm, callback_val, this_val, &[elem, js_array_index(i), o_val]) { NativeResult::Ok(result_val) => { @@ -118,10 +134,6 @@ pub fn array_reduce(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("Array.prototype.reduce called with {} args", args.len()); let (arr_ptr, n, _is_array) = array_ptr_len3!(vm, args); let o_val = vm.reg(args[0]); - if n == 0 && args.len() < 3 { - builtins_error!("Array.prototype.reduce: invalid receiver"); - return NativeResult::Err(array_type_error(vm, "Reduce of empty array with no initial value")); - } if args.len() < 2 { builtins_error!("Array.prototype.reduce: invalid receiver"); return NativeResult::Err(array_type_error(vm, "callback is not a function")); @@ -140,11 +152,30 @@ pub fn array_reduce(vm: &mut H, args: &[u8]) -> NativeResult { accumulator = vm.reg(args[2]); start_idx = 0; } else { - accumulator = arraylike_get_or_err!(vm, arr_ptr, 0); - start_idx = 1; + // FindFirstKey:无初值时取首个存在索引作累加器;全洞(含空数组)抛 TypeError。 + let mut found = None; + for i in 0..n { + if arraylike_index_present(vm, arr_ptr, i) { + found = Some(i); + break; + } + } + let idx = match found { + Some(idx) => idx, + None => { + builtins_error!("Array.prototype.reduce: invalid receiver"); + return NativeResult::Err(array_type_error(vm, "Reduce of empty array with no initial value")); + } + }; + accumulator = arraylike_get_or_err!(vm, arr_ptr, idx); + start_idx = idx + 1; } let this_val = JsValue::undefined(); for i in start_idx..n { + // 洞位跳过:累加器保持上一 present 元素的结果。 + if !arraylike_index_present(vm, arr_ptr, i) { + continue; + } let elem = arraylike_get_or_err!(vm, arr_ptr, i); match invoke_native_callback(vm, callback_val, this_val, &[accumulator, elem, js_array_index(i), o_val]) { NativeResult::Ok(result) => accumulator = result, @@ -217,6 +248,10 @@ pub fn array_some(vm: &mut H, args: &[u8]) -> NativeResult { }; let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; for i in 0..n { + // 洞位跳过:回调不得在缺失索引上触发。 + if !arraylike_index_present(vm, arr_ptr, i) { + continue; + } let elem = arraylike_get_or_err!(vm, arr_ptr, i); match invoke_native_callback(vm, callback_val, this_val, &[elem, js_array_index(i), o_val]) { NativeResult::Ok(result_val) => { @@ -255,6 +290,10 @@ pub fn array_every(vm: &mut H, args: &[u8]) -> NativeResult { }; let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; for i in 0..n { + // 洞位跳过:回调不得在缺失索引上触发。 + if !arraylike_index_present(vm, arr_ptr, i) { + continue; + } let elem = arraylike_get_or_err!(vm, arr_ptr, i); match invoke_native_callback(vm, callback_val, this_val, &[elem, js_array_index(i), o_val]) { NativeResult::Ok(result_val) => { @@ -297,6 +336,10 @@ pub fn array_flat_map(vm: &mut H, args: &[u8]) -> NativeResult { return NativeResult::Err(err); } for i in 0..n { + // 洞位跳过:回调不得在缺失索引上触发。 + if !arraylike_index_present(vm, arr_ptr, i) { + continue; + } let elem = arraylike_get_or_err!(vm, arr_ptr, i); match invoke_native_callback(vm, callback_val, this_val, &[elem, js_array_index(i), o_val]) { NativeResult::Ok(result) => { @@ -305,9 +348,13 @@ pub fn array_flat_map(vm: &mut H, args: &[u8]) -> NativeResult { if !r_ptr.is_null() { let r = unsafe { &*r_ptr }; if r.is_array() { + // 嵌套数组展开丢弃洞位:结果为紧凑数组。 let rn = r.prop_count() as usize; + let dense = r.array_elements_meta_vec().is_none(); for j in 0..rn { - flat.push(r.get_prop_at(j)); + if dense || !r.prop_meta_at(j).is_some_and(|m| m.is_hole()) { + flat.push(r.get_prop_at(j)); + } } continue; } @@ -394,8 +441,8 @@ pub fn array_find_last(vm: &mut H, args: &[u8]) -> NativeResult { }; let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; for i in (0..n).rev() { - let elem = unsafe { (*arr_ptr).get_prop_at(i) }; - match invoke_native_callback(vm, callback_val, this_val, &[elem, JsValue::int(i), o_val]) { + let elem = arraylike_get_or_err!(vm, arr_ptr, i as usize); + match invoke_native_callback(vm, callback_val, this_val, &[elem, js_array_index(i as usize), o_val]) { NativeResult::Ok(r) => { if oxide_runtime_api::to_boolean(r) { return NativeResult::Ok(elem); @@ -433,15 +480,35 @@ pub fn array_reduce_right(vm: &mut H, args: &[u8]) -> NativeResult { let (mut acc, start_idx): (JsValue, i32) = if args.len() > 2 { (vm.reg(args[2]), n as i32 - 1) } else { - if n == 0 { - builtins_error!("Array.prototype.reduceRight: invalid receiver"); - return NativeResult::Err(array_type_error(vm, "Reduce of empty array with no initial value")); + // FindFirstKey 自尾:无初值时取末尾首个存在索引作累加器;全洞抛 TypeError。 + let mut found = None; + for i in (0..n).rev() { + if arraylike_index_present(vm, arr_ptr, i) { + found = Some(i); + break; + } } - (unsafe { (*arr_ptr).get_prop_at(n - 1) }, n as i32 - 2) + let idx = match found { + Some(idx) => idx, + None => { + builtins_error!("Array.prototype.reduceRight: invalid receiver"); + return NativeResult::Err(array_type_error(vm, "Reduce of empty array with no initial value")); + } + }; + (arraylike_get_or_err!(vm, arr_ptr, idx), idx as i32 - 1) }; for i in (0..=start_idx).rev() { - let elem = unsafe { (*arr_ptr).get_prop_at(i as usize) }; - match invoke_native_callback(vm, callback_val, JsValue::undefined(), &[acc, elem, JsValue::int(i), o_val]) { + // 洞位跳过:累加器保持上一 present 元素的结果。 + if !arraylike_index_present(vm, arr_ptr, i as usize) { + continue; + } + let elem = arraylike_get_or_err!(vm, arr_ptr, i as usize); + match invoke_native_callback( + vm, + callback_val, + JsValue::undefined(), + &[acc, elem, js_array_index(i as usize), o_val], + ) { NativeResult::Ok(r) => acc = r, NativeResult::Err(err) => { builtins_error!("Array.prototype.reduceRight: invalid receiver"); diff --git a/crates/oxide_builtins/src/array/sort_iterator.rs b/crates/oxide_builtins/src/array/sort_iterator.rs index d9a99b40..e7382110 100644 --- a/crates/oxide_builtins/src/array/sort_iterator.rs +++ b/crates/oxide_builtins/src/array/sort_iterator.rs @@ -73,7 +73,8 @@ pub(crate) fn parse_sort_comparator(vm: &mut H, candidate: JsValue) - /// 原地排序数组元素并返回原数组(`Array.prototype.sort`)。 /// /// 未提供比较回调时按 ToString 结果的字符串字典序排序;底层 `sort_by` 为稳定 -/// 排序,比较结果相等(含 NaN)的元素保持原有相对次序。 +/// 排序,比较结果相等(含 NaN)的元素保持原有相对次序。洞位不参与比较: +/// present 值稳定排序后自索引 0 紧凑写回,尾部空位转为洞。 /// /// # 边界与前提 /// - 接收者不是数组时抛 TypeError; @@ -82,7 +83,13 @@ pub fn array_sort(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("Array.prototype.sort called with {} args", args.len()); let arr_ptr = array_ptr!(vm, args); let len = unsafe { (*arr_ptr).prop_count() as usize }; - let mut vals: Vec = (0..len).map(|i| unsafe { (*arr_ptr).get_prop_at(i) }).collect(); + // 洞位不参与比较:只收集 present 元素(接收者恒为真数组,元数据表判洞免字符串键)。 + let arr_ref = unsafe { &*arr_ptr }; + let dense = arr_ref.array_elements_meta_vec().is_none(); + let mut vals: Vec = (0..len) + .filter(|&i| dense || !arr_ref.prop_meta_at(i).is_some_and(|m| m.is_hole())) + .map(|i| arr_ref.get_prop_at(i)) + .collect(); let comparator = if args.len() > 1 { match parse_sort_comparator(vm, vm.reg(args[1])) { Ok(c) => c, @@ -98,10 +105,14 @@ pub fn array_sort(vm: &mut H, args: &[u8]) -> NativeResult { builtins_error!("Array.prototype.sort: invalid receiver"); return NativeResult::Err(err); } + // present 值稳定排序后从索引 0 紧凑写回,尾部空位转为洞。 let arr = unsafe { &mut *arr_ptr }; for (i, &v) in vals.iter().enumerate() { arr.set_prop_at(i, v); } + for i in vals.len()..len { + arr.mark_hole_at(i); + } NativeResult::Ok(vm.reg(args[0])) } diff --git a/crates/oxide_builtins/src/typed_array.rs b/crates/oxide_builtins/src/typed_array.rs index 0f29ac1d..4f2079c7 100644 --- a/crates/oxide_builtins/src/typed_array.rs +++ b/crates/oxide_builtins/src/typed_array.rs @@ -233,7 +233,7 @@ pub fn typed_array_element_get(vm: &mut H, obj: &JsObject, index: u32 /// 若对象是 TypedArray 且属性键是整数索引,返回 `(索引, 视图长度)`;否则 `None`。 /// 供 VM 在 receiver ≠ TA 时裁决整数索引的写前语义(越界直接返回,界内落到 receiver)。 -pub fn typed_array_integer_index(vm: &mut H, obj: &JsObject, prop_name_si: u32) -> Option<(usize, usize)> { +pub fn typed_array_integer_index(vm: &H, obj: &JsObject, prop_name_si: u32) -> Option<(usize, usize)> { let ptr = typed_array_data_ptr(obj)?; if ptr.is_null() { return None; diff --git a/crates/oxide_runtime_api/src/host.rs b/crates/oxide_runtime_api/src/host.rs index 937ae1f3..10a30b01 100644 --- a/crates/oxide_runtime_api/src/host.rs +++ b/crates/oxide_runtime_api/src/host.rs @@ -103,6 +103,9 @@ pub trait VmHost { /// 供建键入口(fromEntries/json/rest excluded)与 `property_key_si` 的字符串分支统一口径。 fn string_key_si(&mut self, s: &str) -> u32; fn resolve_property(&self, obj: &JsObject, prop_name_si: u32) -> Option; + /// 存在性判定(规范 HasProperty):自身与原型链任一层 P 为自有属性即存在; + /// 数组元素区 hole 视同缺失,TypedArray 整数索引按视图长度判在界。 + fn has_property(&self, obj: &JsObject, prop_name_si: u32) -> bool; fn get_own_property_slot(&self, obj: &JsObject, prop_name_si: u32) -> Option; // 属性访问 diff --git a/crates/oxide_vm/src/vm/props.rs b/crates/oxide_vm/src/vm/props.rs index 1b7d493b..25ae1f09 100644 --- a/crates/oxide_vm/src/vm/props.rs +++ b/crates/oxide_vm/src/vm/props.rs @@ -212,6 +212,43 @@ impl Vm { None } + /// 存在性判定(规范 HasProperty):自身与原型链逐级检查,任一层 P 为自有 + /// 属性即存在。数组 length 虚拟属性恒存在;数组元素区 hole 视同缺失; + /// TypedArray 整数索引按视图长度判在界;其余按 shape 槽判定。 + /// + /// 与 `resolve_property` 的差异:原型链上每层都检查数组元素区与 TypedArray + /// 元素(`resolve_property` 只在顶层检查元素区,链上仅走 shape 槽), + /// 继承自父数组/TypedArray 的索引属性在此判存在。 + pub(crate) fn has_property(&self, obj: &JsObject, prop_name_si: u32) -> bool { + let length_si = self.length_si; + let mut current = Some(obj); + let mut depth = 0usize; + while let Some(obj) = current { + if obj.is_array() && prop_name_si == length_si { + return true; + } + if obj.is_typed_array_obj() { + if let Some((index, len)) = + oxide_builtins::typed_array::typed_array_integer_index(self, obj, prop_name_si) + { + if index < len { + return true; + } + } + } + if self.get_own_property_slot(obj, prop_name_si).is_some() { + return true; + } + if depth >= MAX_PROTO_CHAIN_DEPTH { + break; + } + depth += 1; + let proto = obj.proto(); + current = proto.is_object().then(|| unsafe { &*proto.as_js_object_ptr() }); + } + false + } + /// 查找自身属性槽下标:数组 length 虚拟属性返回 `None`,元素区返回下标, /// shape 槽按数组(元素区之后偏移)与普通对象(槽位即下标)各自定位; /// 未命中的槽返回 `None`。 diff --git a/crates/oxide_vm/src/vm/vmhost.rs b/crates/oxide_vm/src/vm/vmhost.rs index 419c590b..1f163922 100644 --- a/crates/oxide_vm/src/vm/vmhost.rs +++ b/crates/oxide_vm/src/vm/vmhost.rs @@ -87,6 +87,9 @@ impl oxide_runtime_api::VmHost for Vm { fn resolve_property(&self, obj: &JsObject, prop_name_si: u32) -> Option { self.resolve_property(obj, prop_name_si) } + fn has_property(&self, obj: &JsObject, prop_name_si: u32) -> bool { + self.has_property(obj, prop_name_si) + } fn get_own_property_slot(&self, obj: &JsObject, prop_name_si: u32) -> Option { self.get_own_property_slot(obj, prop_name_si) } diff --git a/crates/oxide_vm/tests/array_length_set_tests.rs b/crates/oxide_vm/tests/array_length_set_tests.rs index f28bd901..73d6003c 100644 --- a/crates/oxide_vm/tests/array_length_set_tests.rs +++ b/crates/oxide_vm/tests/array_length_set_tests.rs @@ -263,3 +263,60 @@ fn new_array_holes_pop_shift() { assert_eq!(eval_str("var a=new Array(1); var p=a.pop(); (p===undefined)+':'+a.length"), "true:0"); assert_eq!(eval_str("var a=new Array(1); var s=a.shift(); (s===undefined)+':'+a.length"), "true:0"); } + +#[test] +fn read_paths_skip_holes_per_spec() { + // iterate 族:洞位不触发回调;map 结果在源洞位留洞、长度不变。 + assert_eq!(eval_str("var n=0; new Array(2).forEach(function(){n++;}); ''+n"), "0"); + assert_eq!( + eval_str("var n=0; var r=[1,,3].map(function(x){n++;return x;}); n+':'+(1 in r)+':'+r.length"), + "2:false:3" + ); + assert_eq!( + eval_str("var r=new Array(3).map(function(x){return x;}); (0 in r)+':'+r.length"), + "false:3" + ); + assert_eq!(eval_str("new Array(2).filter(function(){return true;}).length"), "0"); + // reduce/reduceRight:无初值取首个/末尾 present 位作累加器,洞位跳过;全洞抛 TypeError。 + assert_eq!( + eval_str("[1,,].reduce(function(a,b){return b;})+':'+(function(){try{new Array(1).reduce(function(){});return 'no-throw';}catch(e){return e.name;}})()"), + "1:TypeError" + ); + assert_eq!( + eval_str("[,1,2].reduceRight(function(a,b){return a+b;})+':'+(function(){try{new Array(1).reduceRight(function(){});return 'no-throw';}catch(e){return e.name;}})()"), + "3:TypeError" + ); + // flatMap:洞位不触发回调;嵌套数组展开丢弃洞位(紧凑结果)。 + assert_eq!( + eval_str("new Array(1).flatMap(function(x){return [x];}).length+':'+[1].flatMap(function(){return [1,,3];}).join(',')+':'+[1].flatMap(function(){return [1,,3];}).length"), + "0:1,3:2" + ); + // indexOf/lastIndexOf:洞位不参与比较。 + assert_eq!(eval_str("[1,,3].indexOf(undefined)+':'+[1,,3].lastIndexOf(undefined)"), "-1:-1"); + // slice:终长 = end - start;源洞位在结果中留洞。 + assert_eq!( + eval_str("new Array(3).slice().length+':'+[1,,3].slice(0,2).length+':'+(1 in [1,,3].slice(0,2))"), + "3:2:false" + ); + // concat:this 与数组实参的洞位均在结果中保洞。 + assert_eq!( + eval_str("[1].concat([,2]).length+':'+(1 in [1].concat([,2]))+':'+[1].concat(new Array(1)).length"), + "3:false:2" + ); + // flat:顶层与嵌套洞位均丢弃(紧凑结果)。 + assert_eq!(eval_str("[,1].flat().length+':'+[1,[2,,3],4].flat().join(',')"), "1:1,2,3,4"); + // sort:洞位不参与比较;present 值紧凑写回,尾部转洞。 + assert_eq!( + eval_str("var a=[,1,3,2].sort(); (0 in a)+':'+(3 in a)+':'+a.join(',')"), + "true:false:1,2,3," + ); + assert_eq!( + eval_str("var b=[5,,1,3].sort(function(x,y){return x-y;}); b.join(',')+':'+(3 in b)"), + "1,3,5,:false" + ); + // 防过度门控:find 族无门控(洞位照常调回调)、includes 无门控(洞 Get 得 undefined)。 + assert_eq!( + eval_str("[,1].findIndex(function(){return true;})+':'+[1,,3].includes(undefined)+':'+[,1].findIndex(function(x){return x===1;})"), + "0:true:1" + ); +} diff --git a/crates/oxide_vm/tests/builtins_array_tests.rs b/crates/oxide_vm/tests/builtins_array_tests.rs index a2212352..cef15bfe 100644 --- a/crates/oxide_vm/tests/builtins_array_tests.rs +++ b/crates/oxide_vm/tests/builtins_array_tests.rs @@ -392,19 +392,19 @@ fn array_fill_single() { } #[test] -fn array_iteration_hole_reads_undefined() { - // 稀疏数组(字面量空洞 / delete)元素读取落到 undefined:引擎对 hole 不跳过 - // 回调而是读 undefined(pre-existing 语义,直读 fast path 与慢路径一致)。 +fn array_iteration_holes_skipped_per_spec() { + // 稀疏数组(字面量空洞 / delete):洞位不触发回调,map 结果在洞位留洞, + // join 把洞位渲染为空串。 let (vm, result) = eval("[10,,30].map(x=>x*2).join(',')").unwrap(); - assert_eq!(to_str(&vm, result), "20,NaN,60"); + assert_eq!(to_str(&vm, result), "20,,60"); - // delete 产生的 hole 经 map 读 undefined,不残留洞前值。 + // delete 产生的洞位经 map 同样跳过,不残留洞前值。 let (vm, result) = eval("var a=[1,2,3]; delete a[1]; a.map(x=>x+1).join(',')").unwrap(); - assert_eq!(to_str(&vm, result), "2,NaN,4"); + assert_eq!(to_str(&vm, result), "2,,4"); - // reduce 对 hole 累加 undefined。 + // reduce 跳过洞位:初值直接接到首个 present 元素。 let (vm, result) = eval("var a=[1,,3]; a.reduce((s,x)=>s+','+x,'')").unwrap(); - assert_eq!(to_str(&vm, result), ",1,undefined,3"); + assert_eq!(to_str(&vm, result), ",1,3"); } #[test]