From 53029dbcc7930c03e81dd9113520e5b2273cf29d Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Tue, 22 Sep 2026 13:07:40 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=95=B0=E7=BB=84=20push/pop/shift/unsh?= =?UTF-8?q?ift=20=E6=8C=89=E8=A7=84=E8=8C=83=E6=8E=A5=E6=94=B6=20arraylike?= =?UTF-8?q?=20=E4=B8=8E=E5=AD=97=E7=AC=A6=E4=B8=B2=E6=8E=A5=E6=94=B6?= =?UTF-8?q?=E8=80=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/array/element.rs | 213 ++++++++++++------ .../tests/array_hole_semantics_tests.rs | 168 +++++++++++++- crates/oxide_vm/src/vm_props.rs | 9 + 3 files changed, 320 insertions(+), 70 deletions(-) diff --git a/crates/oxide_builtins/src/array/element.rs b/crates/oxide_builtins/src/array/element.rs index 0f310ecc..60b1c6bd 100644 --- a/crates/oxide_builtins/src/array/element.rs +++ b/crates/oxide_builtins/src/array/element.rs @@ -48,19 +48,39 @@ fn delete_prop_or_throw(vm: &mut H, obj: &mut JsObject, key_si: u32) /// `Array.prototype.push(...items)`:追加元素到尾部,返回新长度。 /// /// # 步骤 -/// 1. 读 `LengthOfArrayLike(this)` 作起始索引。 -/// 2. 每个实参按当前索引生成属性键,以严格模式 `[[Set]]` 写入(继承 setter / +/// 1. `ToObject` 装箱基元 this(null/undefined 抛 TypeError),装箱体钉入返回 +/// 寄存器跨用户窗口保 GC 根;长度按规范 `ToLength(Get(O, "length"))` +/// 读取(上限 2^53-1)。 +/// 2. `len + 参数数 > 2^53-1` 抛 TypeError("Invalid array length")。 +/// 3. 每个实参按当前索引生成属性键,以严格模式 `[[Set]]` 写入(继承 setter / /// 不可写元素按规范抛 TypeError),写入成功后才递增索引。 -/// 3. 以 `Set(this, "length", len, true)` 收尾,length 不可写时抛 TypeError。 +/// 4. 以 `Set(this, "length", len, true)` 收尾,length 不可写时抛 TypeError。 /// /// # 副作用 /// - 修改元素区与 length;元素写入可经原型链触发用户 setter。 pub fn array_push(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("Array.prototype.push called with {} args", args.len()); - let arr_ptr = array_ptr!(vm, args); - let recv = JsValue::from_js_object(arr_ptr); - let mut len = unsafe { &*arr_ptr }.logical_len() as usize; - + // ToObject:基元 this 装箱(null/undefined 抛 TypeError)。 + let this_val = match oxide_runtime_api::to_object(vm.reg(args[0]), vm) { + Ok(v) => v, + Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)), + }; + // 装箱体钉入返回寄存器:它只存于 Rust 局部,this 寄存器持原始基元(不在根集), + // 跨 length getter / 元素 Set 用户窗口须保持 GC 根。call 转发形态的实参寄存器集 + // 可占该槽,占位时跳过钉位(装箱体按接收者值传递保活)。 + if !args.contains(&0) { + vm.set_reg(0, this_val); + } + let (arr_ptr, mut len, _is_array) = match get_this_arraylike(vm, this_val) { + Ok(v) => v, + Err(e) => return NativeResult::Err(e), + }; + let recv = this_val; + let n_items = args.len().saturating_sub(1); + // len + 参数数超 2^53-1 抛 TypeError(node 消息 "Invalid array length")。 + if len + n_items > 9_007_199_254_740_991 { + return NativeResult::Err(crate::error::create_type_error(vm, "Invalid array length")); + } for &arg_reg in args.iter().skip(1) { let key = vm.string_key_si(&len.to_string()); if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, key, vm.reg(arg_reg), recv, true) { @@ -80,16 +100,34 @@ pub fn array_push(vm: &mut H, args: &[u8]) -> NativeResult { /// `Array.prototype.pop()`:移除并返回末位元素;空数组返回 undefined。 /// /// # 步骤 -/// 1. 读 `LengthOfArrayLike(this)`;为 0 时以 `Set(this, "length", +0, true)` 收尾。 -/// 2. 否则按 Get / DeletePropertyOrThrow / Set length 的顺序处理末位元素。 +/// 1. `ToObject` 装箱基元 this(null/undefined 抛 TypeError),装箱体钉入返回 +/// 寄存器跨用户窗口保 GC 根;长度按规范 `ToLength(Get(O, "length"))` +/// 读取(上限 2^53-1)。 +/// 2. 长度为 0 时以 `Set(this, "length", +0, true)` 收尾返回 undefined。 +/// 3. 否则 Get 末位元素(钉入返回寄存器跨后续窗口保 GC 根), +/// DeletePropertyOrThrow 末位(不可配置自有索引抛 TypeError), +/// 以 `Set(this, "length", len-1, true)` 收尾,返回末位元素。 /// /// # 副作用 /// - 删除末位元素并收缩 length;Get 可触发原型链 getter,length 不可写时抛 TypeError。 pub fn array_pop(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("Array.prototype.pop called with {} args", args.len()); - let arr_ptr = array_ptr!(vm, args); - let recv = JsValue::from_js_object(arr_ptr); - let len = unsafe { &*arr_ptr }.logical_len() as usize; + // ToObject:基元 this 装箱(null/undefined 抛 TypeError)。 + let this_val = match oxide_runtime_api::to_object(vm.reg(args[0]), vm) { + Ok(v) => v, + Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)), + }; + // 装箱体钉入返回寄存器:它只存于 Rust 局部,this 寄存器持原始基元(不在根集), + // 跨 length getter 用户窗口须保持 GC 根;末位元素钉入后此槽让位,其后装箱体 + // 按接收者值传递保活。call 转发形态的实参寄存器集可占该槽,占位时跳过钉位。 + if !args.contains(&0) { + vm.set_reg(0, this_val); + } + let (arr_ptr, len, _is_array) = match get_this_arraylike(vm, this_val) { + Ok(v) => v, + Err(e) => return NativeResult::Err(e), + }; + let recv = this_val; let length_si = vm.string_key_si("length"); if len == 0 { if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, length_si, JsValue::int(0), recv, true) { @@ -104,18 +142,17 @@ pub fn array_pop(vm: &mut H, args: &[u8]) -> NativeResult { Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)), }; - // DeletePropertyOrThrow:不可配置元素删除失败抛 TypeError,否则标记为 hole。 - match unsafe { &*arr_ptr }.prop_meta_at(index) { - Some(meta) if !meta.attributes.configurable() => { - return NativeResult::Err(array_type_error(vm, "Cannot delete property")); - } - _ => unsafe { (*arr_ptr).mark_hole_at(index) }, + // 末位元素可能来自原型链 getter 的全新临时对象:钉入返回寄存器 + // 跨 delete / Set length 用户窗口保 GC 根,收尾自钉位读回。 + vm.set_reg(0, last); + // DeletePropertyOrThrow:不可配置自有索引删除失败抛 TypeError,hole / 缺失位空操作。 + if let Err(err) = delete_prop_or_throw(vm, unsafe { &mut *arr_ptr }, key) { + return NativeResult::Err(err); } - if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, length_si, js_array_index(index), recv, true) { return NativeResult::Err(from_engine_error(vm, &err)); } - NativeResult::Ok(last) + NativeResult::Ok(vm.reg(0)) } /// `Array.prototype.slice(start, end)`:复制区间元素返回新数组(支持负索引)。 @@ -900,18 +937,35 @@ pub fn array_flat(vm: &mut H, args: &[u8]) -> NativeResult { /// `Array.prototype.shift()`:移除并返回首元素,其余元素前移;空数组返回 undefined。 /// /// # 步骤 -/// 1. 读 `LengthOfArrayLike(this)`;为 0 时以 `Set(this, "length", +0, true)` 收尾。 -/// 2. Get 首元素后,把 `1..len` 逐位前移:源存在则 Get + 严格 Set 到前一格, -/// 源缺失(hole)则 DeletePropertyOrThrow 目标格。 -/// 3. 以 `Set(this, "length", len-1, true)` 收尾。 +/// 1. `ToObject` 装箱基元 this(null/undefined 抛 TypeError),装箱体钉入返回 +/// 寄存器跨用户窗口保 GC 根;长度按规范 `ToLength(Get(O, "length"))` +/// 读取(上限 2^53-1)。 +/// 2. 长度为 0 时以 `Set(this, "length", +0, true)` 收尾返回 undefined。 +/// 3. Get 首元素并钉入返回寄存器;`1..len` 逐位前移:HasProperty 命中 +/// (自身与原型链,hole 视同缺失)则 Get + 严格 Set 到前一格,缺失则 +/// DeletePropertyOrThrow 目标格。 +/// 4. 末位 DeletePropertyOrThrow 后以 `Set(this, "length", len-1, true)` 收尾。 /// /// # 副作用 /// - 元素整体前移并收缩 length;Get/Set 可经原型链触发用户代码。 pub fn array_shift(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("Array.prototype.shift called with {} args", args.len()); - let arr_ptr = array_ptr!(vm, args); - let recv = JsValue::from_js_object(arr_ptr); - let len = unsafe { &*arr_ptr }.logical_len() as usize; + // ToObject:基元 this 装箱(null/undefined 抛 TypeError)。 + let this_val = match oxide_runtime_api::to_object(vm.reg(args[0]), vm) { + Ok(v) => v, + Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)), + }; + // 装箱体钉入返回寄存器:它只存于 Rust 局部,this 寄存器持原始基元(不在根集), + // 跨 length getter 用户窗口须保持 GC 根;首位元素钉入后此槽让位,其后装箱体 + // 按接收者值传递保活。call 转发形态的实参寄存器集可占该槽,占位时跳过钉位。 + if !args.contains(&0) { + vm.set_reg(0, this_val); + } + let (arr_ptr, len, _is_array) = match get_this_arraylike(vm, this_val) { + Ok(v) => v, + Err(e) => return NativeResult::Err(e), + }; + let recv = this_val; let length_si = vm.string_key_si("length"); if len == 0 { if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, length_si, JsValue::int(0), recv, true) { @@ -930,28 +984,30 @@ pub fn array_shift(vm: &mut H, args: &[u8]) -> NativeResult { vm.set_reg(0, first); for k in 1..len { - let from = vm.string_key_si(&k.to_string()); let to = vm.string_key_si(&(k - 1).to_string()); // HasProperty 走原型链存在性判定(hole 视同缺失),命中才 Get + Set。 - if vm.resolve_property(unsafe { &*arr_ptr }, from).is_some() { - let val = match vm.ordinary_get(unsafe { &*arr_ptr }, from, recv) { - Ok(v) => v, - Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)), - }; - if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, to, val, recv, true) { - return NativeResult::Err(from_engine_error(vm, &err)); - } - } else { - // DeletePropertyOrThrow(O, to):不可配置元素删除失败抛 TypeError。 - match unsafe { &*arr_ptr }.prop_meta_at(k - 1) { - Some(meta) if !meta.attributes.configurable() => { - return NativeResult::Err(array_type_error(vm, "Cannot delete property")); + match gated_get(vm, arr_ptr, k, recv) { + Ok(Some(val)) => { + if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, to, val, recv, true) { + return NativeResult::Err(from_engine_error(vm, &err)); } - _ => unsafe { (*arr_ptr).mark_hole_at(k - 1) }, } + Ok(None) => { + // DeletePropertyOrThrow(O, to):不可配置元素删除失败抛 TypeError。 + if let Err(err) = delete_prop_or_throw(vm, unsafe { &mut *arr_ptr }, to) { + return NativeResult::Err(err); + } + } + Err(err) => return NativeResult::Err(err), } } + // 末位 DeletePropertyOrThrow:真数组上该位已由 length 写入截断覆盖, + // arraylike 上该位不在搬移路径内,须显式删除。 + let last_key = vm.string_key_si(&(len - 1).to_string()); + if let Err(err) = delete_prop_or_throw(vm, unsafe { &mut *arr_ptr }, last_key) { + return NativeResult::Err(err); + } if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, length_si, js_array_index(len - 1), recv, true) { return NativeResult::Err(from_engine_error(vm, &err)); } @@ -962,50 +1018,69 @@ pub fn array_shift(vm: &mut H, args: &[u8]) -> NativeResult { /// `Array.prototype.unshift(...items)`:插入元素到头部,返回新长度。 /// /// # 步骤 -/// 1. 读 `LengthOfArrayLike(this)`;`len + 参数数` 超出 2^53-1 时抛 RangeError。 -/// 2. 参数数大于 0 时自高到低搬移已有元素(源存在则 Get + 严格 Set,缺失则 -/// DeletePropertyOrThrow),再把实参逐个严格 Set 到头部。 -/// 3. 以 `Set(this, "length", len+参数数, true)` 收尾。 +/// 1. `ToObject` 装箱基元 this(null/undefined 抛 TypeError),装箱体钉入返回 +/// 寄存器跨用户窗口保 GC 根;长度按规范 `ToLength(Get(O, "length"))` +/// 读取(上限 2^53-1)。 +/// 2. 参数数大于 0 且 `len + 参数数 > 2^53-1` 抛 TypeError(现行规范文本, +/// 非 RangeError)。 +/// 3. 参数数大于 0 时自高到低搬移已有元素:HasProperty 命中(自身与原型链, +/// hole 视同缺失)则 Get + 严格 Set(搬移值钉入返回寄存器跨 Set 窗口保 +/// GC 根),缺失则 DeletePropertyOrThrow,再把实参逐个严格 Set 到头部。 +/// 4. 以 `Set(this, "length", len+参数数, true)` 收尾。 /// /// # 副作用 /// - 元素整体后移并扩容 length;Get/Set 可经原型链触发用户代码。 pub fn array_unshift(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("Array.prototype.unshift called with {} args", args.len()); - let arr_ptr = array_ptr!(vm, args); - let recv = JsValue::from_js_object(arr_ptr); - let len = unsafe { &*arr_ptr }.logical_len() as usize; + // ToObject:基元 this 装箱(null/undefined 抛 TypeError)。 + let this_val = match oxide_runtime_api::to_object(vm.reg(args[0]), vm) { + Ok(v) => v, + Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)), + }; + // 装箱体钉入返回寄存器:它只存于 Rust 局部,this 寄存器持原始基元(不在根集), + // 跨 length getter / 搬移窗口用户窗口须保持 GC 根;搬移值钉入后此槽让位, + // 其后装箱体按接收者值传递保活。call 转发形态的实参寄存器集可占该槽, + // 占位时跳过钉位。 + if !args.contains(&0) { + vm.set_reg(0, this_val); + } + let (arr_ptr, len, _is_array) = match get_this_arraylike(vm, this_val) { + Ok(v) => v, + Err(e) => return NativeResult::Err(e), + }; + let recv = this_val; let n_items = args.len().saturating_sub(1); let length_si = vm.string_key_si("length"); + // 实参值先读入局部:搬移循环把搬移值钉入返回寄存器,可能覆写实参寄存器 + // 槽,读取须先于钉位完成。 + let arg_values: Vec = args.iter().skip(1).map(|&r| vm.reg(r)).collect(); if n_items > 0 { - if len as f64 + n_items as f64 > 9_007_199_254_740_991.0 { - // 长度上限越界按规范为 RangeError。 - return NativeResult::Err(crate::error::create_range_error(vm, "Invalid array length")); + // len + 参数数超 2^53-1 抛 TypeError(规范现行文本,node 实测同形)。 + if len + n_items > 9_007_199_254_740_991 { + return NativeResult::Err(crate::error::create_type_error(vm, "Invalid array length")); } for k in (1..=len).rev() { - let from = vm.string_key_si(&(k - 1).to_string()); let to_idx = k - 1 + n_items; let to = vm.string_key_si(&to_idx.to_string()); - if vm.resolve_property(unsafe { &*arr_ptr }, from).is_some() { - let val = match vm.ordinary_get(unsafe { &*arr_ptr }, from, recv) { - Ok(v) => v, - Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)), - }; - if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, to, val, recv, true) { - return NativeResult::Err(from_engine_error(vm, &err)); - } - } else { - // DeletePropertyOrThrow(O, to):不可配置元素删除失败抛 TypeError。 - match unsafe { &*arr_ptr }.prop_meta_at(to_idx) { - Some(meta) if !meta.attributes.configurable() => { - return NativeResult::Err(array_type_error(vm, "Cannot delete property")); + match gated_get(vm, arr_ptr, k - 1, recv) { + Ok(Some(val)) => { + // 搬移值跨 Set 用户调用窗口:先钉返回寄存器再写,写后从钉位读回。 + vm.set_reg(0, val); + if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, to, vm.reg(0), recv, true) { + return NativeResult::Err(from_engine_error(vm, &err)); } - _ => unsafe { (*arr_ptr).mark_hole_at(to_idx) }, } + Ok(None) => { + if let Err(err) = delete_prop_or_throw(vm, unsafe { &mut *arr_ptr }, to) { + return NativeResult::Err(err); + } + } + Err(err) => return NativeResult::Err(err), } } - for (j, &arg_reg) in args.iter().skip(1).enumerate() { + for (j, val) in arg_values.iter().enumerate() { let key = vm.string_key_si(&j.to_string()); - if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, key, vm.reg(arg_reg), recv, true) { + if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, key, *val, recv, true) { return NativeResult::Err(from_engine_error(vm, &err)); } } diff --git a/crates/oxide_builtins/tests/array_hole_semantics_tests.rs b/crates/oxide_builtins/tests/array_hole_semantics_tests.rs index dd244e6b..2f985497 100644 --- a/crates/oxide_builtins/tests/array_hole_semantics_tests.rs +++ b/crates/oxide_builtins/tests/array_hole_semantics_tests.rs @@ -1,5 +1,7 @@ //! splice/reverse/copyWithin/at 四方法洞语义与原型链读语义引擎钉: -//! 源洞保洞/换洞/删洞、重叠反向复制、元素读走 Get。 +//! 源洞保洞/换洞/删洞、重叠反向复制、元素读走 Get; +//! push/pop/shift/unshift arraylike 接收者族:ToObject 通用入口、 +//! ToLength 长度源、2^53-1 上限、DeletePropertyOrThrow 删位、末位显式删。 use std::sync::Arc; @@ -1440,3 +1442,167 @@ fn test_slice_plain_array_guard() { .unwrap(); assert_eq!(out, "true|1,2,3"); } + +// ── push/pop/shift/unshift arraylike 接收者族收口钉(node v20.19.2 实测值钉死) ── + +// 引擎钉:push arraylike 接收者——length 经 ToLength 折算(1.5→1),尾部写值, +// length 置新长度。 +#[test] +fn test_push_arraylike_receiver_length_coerce() { + let out = eval_str( + "(() => { const o = { length: 1.5, 0: 'a' }; \ + const r = Array.prototype.push.call(o, -13); \ + return r + '|' + o[1] + '|' + o.length; })()", + ) + .unwrap(); + assert_eq!(out, "2|-13|2"); +} + +// 引擎钉:push arraylike valueOf length——ToObject 后 ToLength 取整定界。 +#[test] +fn test_push_arraylike_value_of_length() { + let out = eval_str( + "(() => { const r = Array.prototype.push.call({ length: { valueOf() { return 3; } } }); \ + return String(r); })()", + ) + .unwrap(); + assert_eq!(out, "3"); +} + +// 引擎钉:push ToObject 装箱基元 this——Boolean 包装体 length 0,返回 0。 +#[test] +fn test_push_primitive_this_boxed() { + let out = eval_str("(() => { return String(Array.prototype.push.call(true)); })()").unwrap(); + assert_eq!(out, "0"); +} + +// 引擎钉:push 2^53-1 预检——len + 参数数越限抛 TypeError。 +#[test] +fn test_push_integer_limit_throws_type_error() { + let out = eval_str( + "(() => { const o = {}; o.length = 9007199254740991; \ + let t = '?'; try { Array.prototype.push.call(o, 1); } catch (e) { t = e.name; } \ + return t; })()", + ) + .unwrap(); + assert_eq!(out, "TypeError"); +} + +// 引擎钉:pop arraylike 2^53 长度——ToLength 钳 2^53-1,pop 末位(undefined), +// length 收 2^53-2。 +#[test] +fn test_pop_arraylike_integer_limit() { + let out = eval_str( + "(() => { const o = { length: 9007199254740992 }; \ + const r = Array.prototype.pop.call(o); \ + return String(r === undefined) + '|' + o.length; })()", + ) + .unwrap(); + assert_eq!(out, "true|9007199254740990"); +} + +// 引擎钉:pop 末位非可配置自有索引——DeletePropertyOrThrow 抛 TypeError。 +#[test] +fn test_pop_last_slot_non_configurable_throws() { + let out = eval_str( + "(() => { const o = { length: 1, 0: 5 }; \ + Object.defineProperty(o, '0', { value: 5, writable: true, enumerable: true, configurable: false }); \ + let t = '?'; try { Array.prototype.pop.call(o); } catch (e) { t = e.name; } \ + return t; })()", + ) + .unwrap(); + assert_eq!(out, "TypeError"); +} + +// 引擎钉:pop 原型链继承末位元素——Get 读继承值,末位非自有删位空操作, +// 继承读持续可读。 +#[test] +fn test_pop_inherited_last_element() { + let out = eval_str( + "(() => { Array.prototype[1] = 1; \ + try { const x = [0]; x.length = 2; const r = x.pop(); \ + return r + '|' + x.length + '|' + x[1]; } \ + finally { delete Array.prototype[1]; } })()", + ) + .unwrap(); + assert_eq!(out, "1|1|1"); +} + +// 引擎钉:shift arraylike 接收者——缺失中位删目标格、present 位前移、 +// 末位显式删除。 +#[test] +fn test_shift_arraylike_receiver() { + let out = eval_str( + "(() => { const o = { 0: 0, 3: 3, length: 4 }; \ + const r = Array.prototype.shift.call(o); \ + return r + '|' + o.length + '|' + (3 in o) + '|' + o[2]; })()", + ) + .unwrap(); + assert_eq!(out, "0|3|false|3"); +} + +// 引擎钉:shift ToObject 装箱基元 this——Boolean 包装体 length 0,返回 undefined。 +#[test] +fn test_shift_primitive_this_boxed() { + let out = eval_str("(() => { return String(Array.prototype.shift.call(true)); })()").unwrap(); + assert_eq!(out, "undefined"); +} + +// 引擎钉:unshift 无参 2^53-1 极限——argCount 0 不查限,返回 2^53-1。 +#[test] +fn test_unshift_no_args_integer_limit_no_check() { + let out = eval_str( + "(() => { const o = {}; o.length = 9007199254740991; \ + const r = Array.prototype.unshift.call(o); \ + return String(r === 9007199254740991) + '|' + o.length; })()", + ) + .unwrap(); + assert_eq!(out, "true|9007199254740991"); +} + +// 引擎钉:unshift 2^53-1 预检——len + 参数数越限抛 TypeError(现行规范文本, +// 非 RangeError)。 +#[test] +fn test_unshift_integer_limit_throws_type_error() { + let out = eval_str( + "(() => { const o = {}; o.length = 9007199254740991; \ + let t = '?'; try { Array.prototype.unshift.call(o, 1); } catch (e) { t = e.name; } \ + return t; })()", + ) + .unwrap(); + assert_eq!(out, "TypeError"); +} + +// 引擎钉:unshift arraylike 接收者——已有元素右移、实参填头、length 扩容。 +#[test] +fn test_unshift_arraylike_receiver() { + let out = eval_str( + "(() => { const o = { length: 1, 0: 'a' }; \ + const r = Array.prototype.unshift.call(o, 'x'); \ + return r + '|' + o[0] + '|' + o[1]; })()", + ) + .unwrap(); + assert_eq!(out, "2|x|a"); +} + +// 引擎钉:unshift ToObject 装箱基元 this——Boolean 包装体 length 0,两实参返回 2。 +#[test] +fn test_unshift_primitive_this_boxed() { + let out = eval_str("(() => { return String(Array.prototype.unshift.call(false, 7, 8)); })()").unwrap(); + assert_eq!(out, "2"); +} + +// 引擎钉:pop 末位 getter 返回冻结哨兵对象跨 delete/Set-length 用户窗口—— +// 身份保持(钉位语义的可观察契约)。 +#[test] +fn test_pop_getter_sentinel_identity_across_window() { + let out = eval_str( + "(() => { const a = [10, 20]; const sentinel = Object.freeze({ tag: 'S' }); \ + Object.defineProperty(a, 1, { get() { const junk = []; for (let i = 0; i < 100000; i++) junk.push({ i }); return sentinel; }, \ + configurable: true }); \ + const r = a.pop(); \ + return String(r === sentinel) + '|' + a.length; })()", + ) + .unwrap(); + assert_eq!(out, "true|1"); +} diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs index 6cbb3bde..ec5fb4ea 100644 --- a/crates/oxide_vm/src/vm_props.rs +++ b/crates/oxide_vm/src/vm_props.rs @@ -383,6 +383,15 @@ impl Vm { if obj.is_array() && prop_name_si == length_si { return self.set_array_length_value(obj, val, strict, builtin); } + // 字符串 exotic 的 length 是 [[Writable]]: false 的固有数据属性(规范 + // String exotic [[Set]]):写不成立,严格抛 TypeError、sloppy 静默 + // no-op——不得落命名属性区。 + if obj.is_string_obj() && prop_name_si == length_si { + if strict { + return self.write_protection_failure(builtin, "cannot assign to read-only property"); + } + return Ok(()); + } if let Some(pos) = self.get_own_property_slot(obj, prop_name_si) { if let Some(meta) = obj.prop_meta_at(pos) { if meta.is_accessor {