fix: splice reverse copyWithin at 按规范保洞与走 Get

This commit is contained in:
2026-09-19 12:35:06 +00:00
parent f5330da5f0
commit 5d99f5eb7c
2 changed files with 413 additions and 48 deletions
+231 -48
View File
@@ -15,6 +15,35 @@ use super::common::{
};
use super::from::{array_species_create, create_data_property_or_throw, from_engine_error};
/// 按规范读单个元素:HasProperty 门控(自身与原型链任一层命中才算存在,
/// 数组元素区 hole 视同缺失),命中时返回 Get 值(用户 getter 的异常原样
/// 上抛),缺失返回 `None`。
fn gated_get<H: VmHost>(
vm: &mut H, arr_ptr: *mut JsObject, index: usize, recv: JsValue,
) -> Result<Option<JsValue>, JsValue> {
let key_si = vm.string_key_si(&index.to_string());
if !vm.has_property(unsafe { &*arr_ptr }, key_si) {
return Ok(None);
}
let val = match vm.ordinary_get(unsafe { &*arr_ptr }, key_si, recv) {
Ok(v) => v,
Err(msg) => return Err(from_engine_error(vm, &msg)),
};
Ok(Some(val))
}
/// DeletePropertyOrThrow 臂:元素为不可配置自有属性时删除失败抛 TypeError,
/// 否则置洞(length 与元素区大小不变)。
fn delete_prop_or_throw<H: VmHost>(vm: &mut H, arr_ptr: *mut JsObject, index: usize) -> Result<(), JsValue> {
match unsafe { &*arr_ptr }.prop_meta_at(index) {
Some(meta) if !meta.attributes.configurable() => Err(array_type_error(vm, "Cannot delete property")),
_ => {
unsafe { (*arr_ptr).mark_hole_at(index) };
Ok(())
}
}
}
/// `Array.prototype.push(...items)`:追加元素到尾部,返回新长度。
///
/// # 步骤
@@ -165,11 +194,24 @@ pub fn array_slice<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
/// `Array.prototype.splice(start, deleteCount, ...items)`:删除并/或插入元素,
/// 返回被删除元素组成的新数组。
///
/// # 步骤
/// 1. 折算 `start` / `deleteCount`(负数自尾部、夹到界内)。
/// 2. 先建 removed 数组:逐被删下标 HasProperty 门控 + Get,命中
/// CreateDataPropertyOrThrow、缺失保洞;结果对象钉入返回寄存器(其后的
/// 搬移/插入各带用户调用窗口,Rust 局部指针不属 GC 根集)。
/// 3. 双向搬移:源存在则 Get + 严格 Set 到目标,源缺失则目标 DeletePropertyOrThrow
/// (不可配置抛 TypeError);扩容方向下超出当前元素数的目标先记录,length
/// 写入后补置洞。
/// 4. 插入项逐个严格 Set,length 快写收尾。
///
/// # 副作用
/// - 元素被删/插/移;Get/Set 可经原型链触发用户代码。
pub fn array_splice<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("Array.prototype.splice called with {} args", args.len());
let arr_ptr = array_ptr!(vm, args);
let arr = unsafe { &mut *arr_ptr };
let n = arr.prop_count() as usize;
let recv = JsValue::from_js_object(arr_ptr);
let n = unsafe { &*arr_ptr }.prop_count() as usize;
let start = if args.len() > 1 {
let v = vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN);
@@ -192,40 +234,84 @@ pub fn array_splice<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let insert_count = if args.len() > 3 { args.len() - 3 } else { 0 };
let mut removed: Vec<JsValue> = Vec::new();
for i in 0..delete_count {
removed.push(arr.get_prop_at(start + i));
// removed 先行构建并钉入返回寄存器:搬移/插入窗口的用户 setter 可触发
// 回收,结果对象指针须保 GC 根,返回时从钉位读回。
let removed_arr = create_new_array(vm, delete_count);
vm.set_reg(0, JsValue::from_js_object(removed_arr));
for k in 0..delete_count {
match gated_get(vm, arr_ptr, start + k, recv) {
Ok(Some(val)) => {
let key_si = vm.string_key_si(&k.to_string());
if let Err(err) = create_data_property_or_throw(vm, unsafe { &mut *removed_arr }, key_si, val) {
return NativeResult::Err(err);
}
}
// 源缺失位保洞(removed 预填 present-undefined,须显式置洞)。
Ok(None) => unsafe { (*removed_arr).mark_hole_at(k) },
Err(err) => return NativeResult::Err(err),
}
}
// 目标超出当前元素数的洞位无法即时置(越界 no-op),length 写入后补。
let mut pending_holes: Vec<usize> = Vec::new();
if insert_count > delete_count {
let shift = insert_count - delete_count;
for i in (start + delete_count..n).rev() {
let val = arr.get_prop_at(i);
arr.set_prop_at(i + shift, val);
let to = i + shift;
match gated_get(vm, arr_ptr, i, recv) {
Ok(Some(val)) => {
let to_si = vm.string_key_si(&to.to_string());
if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, to_si, val, recv, true) {
return NativeResult::Err(from_engine_error(vm, &err));
}
}
Ok(None) => {
if let Err(err) = delete_prop_or_throw(vm, arr_ptr, to) {
return NativeResult::Err(err);
}
if to >= n {
pending_holes.push(to);
}
}
Err(err) => return NativeResult::Err(err),
}
}
} else if insert_count < delete_count {
let shift = delete_count - insert_count;
for i in start + delete_count..n {
let val = arr.get_prop_at(i);
arr.set_prop_at(i - shift, val);
let to = i - shift;
match gated_get(vm, arr_ptr, i, recv) {
Ok(Some(val)) => {
let to_si = vm.string_key_si(&to.to_string());
if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, to_si, val, recv, true) {
return NativeResult::Err(from_engine_error(vm, &err));
}
}
Ok(None) => {
if let Err(err) = delete_prop_or_throw(vm, arr_ptr, to) {
return NativeResult::Err(err);
}
}
Err(err) => return NativeResult::Err(err),
}
}
}
for i in 0..insert_count {
arr.set_prop_at(start + i, vm.reg(args[3 + i]));
let to_si = vm.string_key_si(&(start + i).to_string());
if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, to_si, vm.reg(args[3 + i]), recv, true) {
return NativeResult::Err(from_engine_error(vm, &err));
}
}
let new_len = n + insert_count - delete_count;
arr.set_prop_count_fast(new_len);
let removed_arr = create_new_array(vm, removed.len());
unsafe {
for (i, val) in removed.iter().enumerate() {
(*removed_arr).set_prop_at(i, *val);
}
(*removed_arr).set_prop_count(removed.len());
unsafe { (*arr_ptr).set_prop_count_fast(new_len) };
for h in pending_holes {
unsafe { (*arr_ptr).mark_hole_at(h) };
}
NativeResult::Ok(JsValue::from_js_object(removed_arr))
NativeResult::Ok(vm.reg(0))
}
/// `Array.prototype.concat(...items)`:连接 this 与参数(数组参数展开)返回新数组。
@@ -409,21 +495,84 @@ pub fn array_includes<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
/// `Array.prototype.reverse()`:原地反转元素顺序,返回 this。
///
/// # 步骤
/// 1. 逐对交换 `[lower, upper]`(`upper = length - lower - 1`,`lower` 到
/// `floor(length/2)` 为止):两端各自 HasProperty 门控,存在端 Get 取值。
/// 2. 双存在:两端严格 Set 互写;单存在:值 Set 到缺失端 + 存在端
/// DeletePropertyOrThrow;双缺失不动。
///
/// # 副作用
/// - 元素对调/置洞;Get/Set 可经原型链触发用户代码。
pub fn array_reverse<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("Array.prototype.reverse called with {} args", args.len());
let arr_ptr = array_ptr!(vm, args);
let arr = unsafe { &mut *arr_ptr };
let n = arr.prop_count() as usize;
let mut i = 0;
let mut j = n.saturating_sub(1);
while i < j {
let tmp = arr.get_prop_at(i);
arr.set_prop_at(i, arr.get_prop_at(j));
arr.set_prop_at(j, tmp);
i += 1;
j = j.saturating_sub(1);
let recv = JsValue::from_js_object(arr_ptr);
let this_val = vm.reg(args[0]);
let n = unsafe { &*arr_ptr }.prop_count() as usize;
let middle = n / 2;
let mut lower = 0usize;
while lower < middle {
let upper = n - lower - 1;
let lower_si = vm.string_key_si(&lower.to_string());
let upper_si = vm.string_key_si(&upper.to_string());
// 先下后上读值(getter 副作用序对齐规范);hole 与原型缺失判端不存在。
let lower_val = if vm.has_property(unsafe { &*arr_ptr }, lower_si) {
match vm.ordinary_get(unsafe { &*arr_ptr }, lower_si, recv) {
Ok(v) => Some(v),
Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)),
}
} else {
None
};
let upper_val = if vm.has_property(unsafe { &*arr_ptr }, upper_si) {
match vm.ordinary_get(unsafe { &*arr_ptr }, upper_si, recv) {
Ok(v) => Some(v),
Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)),
}
} else {
None
};
match (lower_val, upper_val) {
(Some(lv), Some(ov)) => {
// 下值跨上端读与两次 Set 共三个用户调用窗口:先钉返回寄存器
// (Rust 局部不属 GC 根集),写后从钉位读回。
vm.set_reg(0, lv);
if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, lower_si, ov, recv, true) {
return NativeResult::Err(from_engine_error(vm, &err));
}
if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, upper_si, vm.reg(0), recv, true) {
return NativeResult::Err(from_engine_error(vm, &err));
}
}
(None, Some(ov)) => {
if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, lower_si, ov, recv, true) {
return NativeResult::Err(from_engine_error(vm, &err));
}
if let Err(err) = delete_prop_or_throw(vm, arr_ptr, upper) {
return NativeResult::Err(err);
}
}
(Some(lv), None) => {
vm.set_reg(0, lv);
let old_count = unsafe { &*arr_ptr }.prop_count() as usize;
if let Err(err) = delete_prop_or_throw(vm, arr_ptr, lower) {
return NativeResult::Err(err);
}
if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, upper_si, vm.reg(0), recv, true) {
return NativeResult::Err(from_engine_error(vm, &err));
}
// 读值期 getter 截断元素区后,上端写在区域外越界扩展会把间隙
// 填成 present-undefined:被删端落入新扩展段时须补置洞。
if lower >= old_count {
unsafe { (*arr_ptr).mark_hole_at(lower) };
}
}
(None, None) => {}
}
lower += 1;
}
NativeResult::Ok(vm.reg(args[0]))
NativeResult::Ok(this_val)
}
/// `Array.prototype.flat(depth)`:按深度递归展开嵌套数组(含循环引用保护)返回新数组。
@@ -644,14 +793,27 @@ pub fn array_fill<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
NativeResult::Ok(vm.reg(args[0]))
}
/// `Array.prototype.copyWithin(target, start, end)`:在数组内部复制元素区间,返回 this。
/// `Array.prototype.copyWithin(target, start, end)`:在数组内部复制元素区间,
/// 返回 this。
///
/// # 步骤
/// 1. `target` / `start` / `end` 负数自尾部折算并夹到 [0, length],
/// `count = min(end - start, length - target)`。
/// 2. 源区间与目标重叠且目标领先时反向迭代(否则正向复制会先覆盖源),
/// 其余正向。
/// 3. 逐位:源 HasProperty 命中则 Get + 严格 Set 到目标,缺失则目标
/// DeletePropertyOrThrow(不可配置抛 TypeError)。
///
/// # 副作用
/// - 元素被覆写/置洞;Get/Set 可经原型链触发用户代码。
pub fn array_copy_within<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("Array.prototype.copyWithin called with {} args", args.len());
let arr_ptr = array_ptr!(vm, args);
let arr = unsafe { &mut *arr_ptr };
let len = arr.prop_count() as isize;
let recv = JsValue::from_js_object(arr_ptr);
let this_val = vm.reg(args[0]);
let len = unsafe { &*arr_ptr }.prop_count() as isize;
if len == 0 {
return NativeResult::Ok(vm.reg(args[0]));
return NativeResult::Ok(this_val);
}
let rel_target = if args.len() > 1 {
oxide_runtime_api::to_integer_or_infinity(vm.reg(args[1])) as isize
@@ -668,26 +830,47 @@ pub fn array_copy_within<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
} else {
len
};
let target = (if rel_target < 0 { (len + rel_target).max(0) } else { rel_target.min(len) }) as usize;
let start = (if rel_start < 0 { (len + rel_start).max(0) } else { rel_start.min(len) }) as usize;
let end = (if rel_end < 0 { (len + rel_end).max(0) } else { rel_end.min(len) }) as usize;
let len_usize = len as usize;
for (to, from) in (target..).zip(start..end) {
if to >= len_usize {
break;
}
let v = arr.get_prop_at(from);
arr.set_prop_at(to, v);
let target = (if rel_target < 0 { (len + rel_target).max(0) } else { rel_target.min(len) }) as isize;
let start = (if rel_start < 0 { (len + rel_start).max(0) } else { rel_start.min(len) }) as isize;
let end = (if rel_end < 0 { (len + rel_end).max(0) } else { rel_end.min(len) }) as isize;
let count = (end - start).min(len - target);
if count <= 0 {
return NativeResult::Ok(this_val);
}
NativeResult::Ok(vm.reg(args[0]))
let (mut from, mut to, dir) = if start < target && target < start + count {
(start + count - 1, target + count - 1, -1isize)
} else {
(start, target, 1isize)
};
for _ in 0..count {
match gated_get(vm, arr_ptr, from as usize, recv) {
Ok(Some(v)) => {
// 复制值跨 Set 用户调用窗口:先钉返回寄存器再写,写后从钉位读回。
vm.set_reg(0, v);
let to_si = vm.string_key_si(&to.to_string());
if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, to_si, vm.reg(0), recv, true) {
return NativeResult::Err(from_engine_error(vm, &err));
}
}
Ok(None) => {
if let Err(err) = delete_prop_or_throw(vm, arr_ptr, to as usize) {
return NativeResult::Err(err);
}
}
Err(err) => return NativeResult::Err(err),
}
from += dir;
to += dir;
}
NativeResult::Ok(this_val)
}
/// `Array.prototype.at(index)`:按索引取元素,支持负索引;越界返回 undefined。
/// 元素读走规范 Get(洞位落原型链,原型索引访问器触发),密集数组直读快路径。
pub fn array_at<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("Array.prototype.at called with {} args", args.len());
let arr_ptr = array_ptr!(vm, args);
let arr = unsafe { &*arr_ptr };
let len = arr.prop_count() as i32;
let len = unsafe { &*arr_ptr }.prop_count() as i32;
let mut index = if args.len() > 1 {
vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN) as i32
} else {
@@ -699,7 +882,7 @@ pub fn array_at<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if index < 0 || index >= len {
return NativeResult::Ok(JsValue::undefined());
}
NativeResult::Ok(arr.get_prop_at(index))
NativeResult::Ok(arraylike_get_or_err!(vm, arr_ptr, index as usize))
}
/// `Array.prototype.lastIndexOf(searchElement, fromIndex)`:从后往前查找首个匹配索引。
@@ -0,0 +1,182 @@
//! splice/reverse/copyWithin/at 四方法洞语义与原型链读语义引擎钉:
//! 源洞保洞/换洞/删洞、重叠反向复制、元素读走 Get。
use std::sync::Arc;
use oxide_compiler::compiler::Compiler;
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
fn eval_value(source: &str) -> Result<(Vm, JsValue), String> {
let allocator = oxide_parser::Allocator::default();
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?;
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?;
let mut vm = Vm::new();
let result = vm.run(&Arc::new(module))?;
Ok((vm, result))
}
fn eval_str(source: &str) -> Result<String, String> {
let (vm, result) = eval_value(source)?;
vm.lookup_str(result)
.ok_or_else(|| "completion value is not a string".to_string())
}
// splice 被删收集:源洞位在 removed 保洞,present 值原样落入。
#[test]
fn test_splice_removed_keeps_holes() {
let out = eval_str(
"(() => { const a = new Array(3); a[1] = 1; a[2] = 3; \
const r = a.splice(0, 3); \
return r.length + '|' + (0 in r) + '|' + r[1]; })()",
)
.unwrap();
assert_eq!(out, "3|false|1");
}
// splice 被删收集走 Get:洞位落原型链,访问器值落入 removed 且该位为 present。
#[test]
fn test_splice_removed_get_through_proto() {
let out = eval_str(
"(() => { Object.defineProperty(Array.prototype, 0, { get: () => 'G0' }); \
try { const a = new Array(2); a[1] = 1; const r = a.splice(0, 2); \
return r[0] + '|' + (0 in r) + '|' + r.length; } \
finally { delete Array.prototype[0]; } })()",
)
.unwrap();
assert_eq!(out, "G0|true|2");
}
// splice 扩容:洞洞源 removed 全洞;数组体洞位随搬移保留、插入位 present。
#[test]
fn test_splice_grow_keeps_array_holes() {
let out = eval_str(
"(() => { const a = new Array(3); const r = a.splice(0, 2, 'a'); \
return (0 in r) + '|' + (1 in r) + '|' + r.length + '|' + \
(0 in a) + '|' + (1 in a) + '|' + a.length; })()",
)
.unwrap();
assert_eq!(out, "false|false|2|true|false|2");
}
// splice 中插:插入点之后的既有洞位随搬移落位,`3 in` 为 false。
#[test]
fn test_splice_insert_mid_hole_moves() {
let out = eval_str(
"(() => { const a = new Array(4); a[0] = 1; a[1] = 2; a[3] = 4; \
a.splice(2, 0, 'x'); \
return (3 in a) + '|' + (2 in a) + '|' + a[2] + '|' + a.length; })()",
)
.unwrap();
assert_eq!(out, "false|true|x|5");
}
// reverse 洞对 present:值位互换、洞位互换,洞不物化。
#[test]
fn test_reverse_hole_and_value_swap() {
let out = eval_str(
"(() => { const a = new Array(3); a[0] = 1; a[2] = 3; a.reverse(); \
return (1 in a) + '|' + a[0] + '|' + a[2]; })()",
)
.unwrap();
assert_eq!(out, "false|3|1");
}
// reverse 洞对洞:双缺失端不动,洞位互换后各归其位。
#[test]
fn test_reverse_hole_to_hole() {
let out = eval_str(
"(() => { const a = new Array(3); a[1] = 2; a[2] = 3; a.reverse(); \
return (2 in a) + '|' + a[0] + '|' + a[1]; })()",
)
.unwrap();
assert_eq!(out, "false|3|2");
}
// reverse 原型链存在性:自身洞 + 原型 present 视同双存在,两端互写为 present。
#[test]
fn test_reverse_proto_present_both_sides() {
let out = eval_str(
"(() => { Array.prototype[1] = 1; \
try { const x = [0]; x.length = 2; x.reverse(); \
return (0 in x) + '|' + (1 in x) + '|' + x[0] + '|' + x[1]; } \
finally { delete Array.prototype[1]; } })()",
)
.unwrap();
assert_eq!(out, "true|true|1|0");
}
// copyWithin 自拷贝:源洞位 DeleteProperty 自身,洞保洞。
#[test]
fn test_copy_within_self_copy_hole_stays_hole() {
let out = eval_str(
"(() => { const a = new Array(3); a[0] = 1; a[2] = 3; a.copyWithin(0); \
return (1 in a) + '|' + a[0] + '|' + a[2]; })()",
)
.unwrap();
assert_eq!(out, "false|1|3");
}
// copyWithin 重叠正向陷阱:目标领先且重叠须反向迭代,源洞目标位删洞。
#[test]
fn test_copy_within_overlap_reverse_direction() {
let out = eval_str(
"(() => { const a = new Array(4); a[1] = 1; a[2] = 3; a[3] = 4; \
a.copyWithin(1, 0, 2); \
return (0 in a) + '|' + (1 in a) + '|' + (2 in a) + '|' + a[2]; })()",
)
.unwrap();
assert_eq!(out, "false|false|true|1");
}
// copyWithin 重叠方向判别:密集数组反向复制结果与正向不同。
#[test]
fn test_copy_within_dense_overlap() {
let out = eval_str("(() => { const a = [0, 1, 2, 3]; a.copyWithin(1, 0, 2); return a.join(','); })()").unwrap();
assert_eq!(out, "0,0,1,3");
}
// at 元素读走 Get:洞位落原型链,索引访问器触发。
#[test]
fn test_at_get_through_proto() {
let out = eval_str(
"(() => { Object.defineProperty(Array.prototype, 0, { get: () => 'AG' }); \
try { const a = new Array(2); a[1] = 5; \
return a.at(0) + '|' + a.at(-2) + '|' + a.at(1); } \
finally { delete Array.prototype[0]; } })()",
)
.unwrap();
assert_eq!(out, "AG|AG|5");
}
// reverse 读值期 getter 截断元素区:上端写越界扩展不得物化被删端,
// 终态被删端保洞、值端 present。
#[test]
fn test_reverse_getter_truncation_pair() {
let out = eval_str(
"(() => { const a = ['first', 'second']; \
Object.defineProperty(a, 0, { get: () => { a.length = 0; return 'first'; } }); \
a.reverse(); \
return (0 in a) + '|' + (1 in a) + '|' + a[1] + '|' + a.length; })()",
)
.unwrap();
assert_eq!(out, "false|true|first|2");
}
// 防过度修护栏:fill 对洞位无条件 Set,洞位变 present-0,不改。
#[test]
fn test_fill_materializes_hole_unchanged() {
let out = eval_str(
"(() => { const a = new Array(3); a[0] = 1; a[2] = 3; a.fill(0); \
return (1 in a) + '|' + a[1]; })()",
)
.unwrap();
assert_eq!(out, "true|0");
}
// at 越界返回 undefined(无原型链参与,防误改读路径的回归护栏)。
#[test]
fn test_at_out_of_bounds_undefined() {
let (_vm, result) = eval_value("[1, 2].at(9)").unwrap();
assert!(result.is_undefined());
}