fix: at 极负索引越界返 undefined 并钉位装箱体跨窗保根

This commit is contained in:
2026-09-19 17:06:58 +00:00
parent e8f2a54a67
commit be927a5df3
3 changed files with 93 additions and 7 deletions
+52 -7
View File
@@ -193,8 +193,10 @@ pub fn array_slice<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
/// 返回被删除元素组成的新数组。
///
/// # 步骤
/// 1. `ToObject` 装箱基元 this(null/undefined 抛 TypeError);长度按规范
/// `ToLength(Get(O, "length"))` 读取(上限 2^53-1)。
/// 1. `ToObject` 装箱基元 this(null/undefined 抛 TypeError),装箱体钉入返回
/// 寄存器跨用户窗口保 GC 根(removed 数组钉入后让位;返回值为 removed
/// 数组,非装箱体);长度按规范 `ToLength(Get(O, "length"))` 读取
/// (上限 2^53-1)。
/// 2. `start` / `deleteCount` 走 ToIntegerOrInfinity 折算(负数自尾部、夹到
/// 界内);无实参时删除数恒 0,仅给 start 时删到尾部。新长度超 2^53-1
/// 抛 TypeError。
@@ -222,6 +224,14 @@ pub fn array_splice<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
Ok(v) => v,
Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)),
};
// 装箱体钉入返回寄存器:它只存于 Rust 局部,this 寄存器持原始基元(不在根集),
// 跨 length getter / 折算 / 构造器读用户窗口须保持 GC 根。返回寄存器是调用方
// 跨 native 调用不保活值的唯一槽(其余槽位调用方帧可能持活值,钉入即覆写);
// call 转发形态的实参寄存器集可占该槽,占位时跳过钉位(装箱体按接收者值
// 传递保活)。removed 数组钉入后此槽让位,其后窗口装箱体按接收者值传递保活。
if !args.contains(&0) {
vm.set_reg(0, this_val);
}
let (arr_ptr, n, is_array) = match get_this_arraylike(vm, this_val) {
Ok(v) => v,
Err(e) => return NativeResult::Err(e),
@@ -380,6 +390,8 @@ pub fn array_splice<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
unsafe { (*arr_ptr).mark_hole_at(h) };
}
// 返回被删元素组成的 removed 数组(自返回槽读回);装箱体钉位仅跨窗口保根,
// 不作返回值。
NativeResult::Ok(vm.reg(0))
}
@@ -566,7 +578,8 @@ pub fn array_includes<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
/// `Array.prototype.reverse()`:原地反转元素顺序,返回 this。
///
/// # 步骤
/// 1. `ToObject` 装箱基元 this(null/undefined 抛 TypeError);长度按规范
/// 1. `ToObject` 装箱基元 this(null/undefined 抛 TypeError),装箱体钉入返回
/// 寄存器跨用户窗口保 GC 根(交换环换值临时钉复用此槽);长度按规范
/// `ToLength(Get(O, "length"))` 读取(上限 2^53-1)。
/// 2. 逐对交换 `[lower, upper]`(`upper = length - lower - 1`,`lower` 到
/// `floor(length/2)` 为止):两端各自 HasProperty 门控,存在端 Get 取值。
@@ -582,6 +595,14 @@ pub fn array_reverse<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
Ok(v) => v,
Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)),
};
// 装箱体钉入返回寄存器:它只存于 Rust 局部,this 寄存器持原始基元(不在根集),
// 跨 length getter / 折算用户窗口须保持 GC 根。返回寄存器是调用方跨 native
// 调用不保活值的唯一槽;call 转发形态的实参寄存器集可占该槽,占位时跳过
// 钉位(装箱体按接收者值传递保活)。交换环换值临时钉复用此槽,循环期装箱体
// 按接收者值传递保活。
if !args.contains(&0) {
vm.set_reg(0, this_val);
}
let (arr_ptr, n, is_array) = match get_this_arraylike(vm, this_val) {
Ok(v) => v,
Err(e) => return NativeResult::Err(e),
@@ -853,8 +874,9 @@ pub fn array_unshift<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
/// `Array.prototype.fill(value, start, end)`:用给定值填充区间,返回 this。
///
/// # 步骤
/// 1. `ToObject` 装箱基元 this(null/undefined 抛 TypeError);长度按规范
/// `ToLength(Get(O, "length"))` 读取(上限 2^53-1)。
/// 1. `ToObject` 装箱基元 this(null/undefined 抛 TypeError),装箱体钉入返回
/// 寄存器跨用户窗口保 GC 根;长度按规范 `ToLength(Get(O, "length"))`
/// 读取(上限 2^53-1)。
/// 2. `start` / `end` 走 ToIntegerOrInfinity 折算(负数自尾部、夹到界内);
/// `end` 为 undefined 取 len。
/// 3. 区间内每格无条件严格 `Set`(无 HasProperty 门控,洞位物化为 present)。
@@ -868,6 +890,13 @@ pub fn array_fill<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
Ok(v) => v,
Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)),
};
// 装箱体钉入返回寄存器:它只存于 Rust 局部,this 寄存器持原始基元(不在根集),
// 跨 length getter / 折算 / 元素 Set 用户窗口须保持 GC 根。返回寄存器是调用方
// 跨 native 调用不保活值的唯一槽,本方法全程独占,收尾自钉位读回;call 转发
// 形态的实参寄存器集可占该槽,占位时跳过钉位。
if !args.contains(&0) {
vm.set_reg(0, this_val);
}
let (arr_ptr, n, _is_array) = match get_this_arraylike(vm, this_val) {
Ok(v) => v,
Err(e) => return NativeResult::Err(e),
@@ -906,7 +935,8 @@ pub fn array_fill<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
/// 返回 this。
///
/// # 步骤
/// 1. `ToObject` 装箱基元 this(null/undefined 抛 TypeError);长度按规范
/// 1. `ToObject` 装箱基元 this(null/undefined 抛 TypeError),装箱体钉入返回
/// 寄存器跨用户窗口保 GC 根(复制环复制值临时钉复用此槽);长度按规范
/// `ToLength(Get(O, "length"))` 读取(上限 2^53-1)。
/// 2. `target` / `start` / `end` 走 ToIntegerOrInfinity 折算(负数自尾部、夹到
/// [0, length]);`end` 为 undefined 取 len;`count = min(end - start,
@@ -925,6 +955,14 @@ pub fn array_copy_within<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
Ok(v) => v,
Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)),
};
// 装箱体钉入返回寄存器:它只存于 Rust 局部,this 寄存器持原始基元(不在根集),
// 跨 length getter / 折算用户窗口须保持 GC 根。返回寄存器是调用方跨 native
// 调用不保活值的唯一槽;call 转发形态的实参寄存器集可占该槽,占位时跳过
// 钉位(装箱体按接收者值传递保活)。复制环复制值临时钉复用此槽,循环期装箱体
// 按接收者值传递保活。
if !args.contains(&0) {
vm.set_reg(0, this_val);
}
let (arr_ptr, n, _is_array) = match get_this_arraylike(vm, this_val) {
Ok(v) => v,
Err(e) => return NativeResult::Err(e),
@@ -1018,7 +1056,14 @@ pub fn array_at<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
};
let rel = if rel.is_nan() || rel == 0.0 { 0.0 } else { rel.trunc() };
let index = if rel < 0.0 {
(len as f64 + rel).max(0.0) as u64
// k = len + relative 在 64 位整型域折算:极负 rel 饱和为 i64::MIN 时 k 必 < 0
//(规范 k < 0 返 undefined),仅 rel ∈ [-len, 0) 落入界内下标;浮点域钳零会
// 把越界负索引静默映射回首元素。
let k = (len as i64) + (rel as i64);
if k < 0 {
return NativeResult::Ok(JsValue::undefined());
}
k as u64
} else {
rel.min(len as f64) as u64
};
@@ -515,6 +515,18 @@ fn test_at_negative_index_uses_logical_length() {
assert_eq!(out, "x|true");
}
// 引擎钉:at 极负索引 k = len + relative < 0 返 undefined(不钳零回首元素);
// at(-len) 为首元素、at(-len-1) 为 undefined。
#[test]
fn test_at_extreme_negative_index_undefined() {
let out = eval_str(
"(() => { const a = [7, 8]; \
return String(a.at(-1e20) === undefined) + '|' + a.at(-2) + '|' + String(a.at(-3) === undefined); })()",
)
.unwrap();
assert_eq!(out, "true|7|true");
}
// 引擎钉:基元 this 经 ToObject 装箱——reverse 返回 Boolean 包装体、splice 返回真数组。
#[test]
fn test_splice_reverse_primitive_this_boxed() {
+29
View File
@@ -583,3 +583,32 @@ fn disposable_stack_value_string_survives_runtime_gc() {
assert_eq!(text, "stackbox".repeat(8));
assert!(vm.session_gc_stats().total_collections > 0, "churn 应触发执行期字符串 GC");
}
/// 低阈值下基元 this 的装箱体(Number 包装对象)跨执行期 GC 完整存活:
/// reverse/fill/copyWithin/splice 把基元 this 装箱后跨 length getter 用户窗口
/// 使用,装箱体身份(instanceof Number + 被包值)在收集后保持完整(装箱体
/// 此前仅存于 Rust 局部,不在根集)。
#[test]
fn arraylike_boxed_this_identity_survives_runtime_gc() {
let mut vm = vm_with_threshold(512);
let module = compile(
"var g = {}; for (var i = 0; i < 20; i++) { g['k' + i] = 'v'.repeat(64); } \
var savedLen = 2; \
Object.defineProperty(Number.prototype, 'length', { \
configurable: true, \
get: function () { g['y' + Math.random()] = 'w'.repeat(32); return savedLen; }, \
set: function (v) { savedLen = v; } }); \
var r1 = Array.prototype.reverse.call(5); \
var r2 = Array.prototype.fill.call(5, 'v', 0, 2); \
var r3 = Array.prototype.copyWithin.call(5, 0, 0, 1); \
var r4 = Array.prototype.splice.call(5, 0); \
delete Number.prototype.length; \
[r1 instanceof Number, r1.valueOf(), r2 instanceof Number, r2.valueOf(), \
r3 instanceof Number, r3.valueOf(), Array.isArray(r4), r4.length].join('|')",
);
let result = vm.run(&Arc::new(module)).expect("run");
let text = vm.lookup_str(result).expect("结果应为字符串").to_string();
assert_eq!(text, "true|5|true|5|true|5|true|2");
assert!(vm.session_gc_stats().total_collections > 0, "执行期应触发字符串 GC");
}