fix: 数组长度赋值走统一检查并支持部分截断

This commit is contained in:
2026-09-18 03:01:53 +00:00
parent b45f979db7
commit 41bebaa05a
5 changed files with 484 additions and 127 deletions
+152 -30
View File
@@ -9,37 +9,81 @@ use crate::builtins_debug;
use crate::builtins_error; use crate::builtins_error;
use super::common::{ use super::common::{
array_ptr, arraylike_get, arraylike_get_or_err, check_array_create_len, clamp_relative, create_new_array, array_ptr, array_type_error, arraylike_get, arraylike_get_or_err, check_array_create_len, clamp_relative,
get_this_array_ref, get_this_arraylike, js_array_index, to_integer_or_infinity_bounded, 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}; use super::from::{array_species_create, create_data_property_or_throw, from_engine_error};
/// `Array.prototype.push(...items)`:追加元素到尾部,返回新长度。 /// `Array.prototype.push(...items)`:追加元素到尾部,返回新长度。
///
/// # 步骤
/// 1. 读 `LengthOfArrayLike(this)` 作起始索引。
/// 2. 每个实参按当前索引生成属性键,以严格模式 `[[Set]]` 写入(继承 setter /
/// 不可写元素按规范抛 TypeError),写入成功后才递增索引。
/// 3. 以 `Set(this, "length", len, true)` 收尾,length 不可写时抛 TypeError。
///
/// # 副作用
/// - 修改元素区与 length;元素写入可经原型链触发用户 setter。
pub fn array_push<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult { pub fn array_push<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("Array.prototype.push called with {} args", args.len()); builtins_debug!("Array.prototype.push called with {} args", args.len());
let arr_ptr = array_ptr!(vm, args); 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;
for &arg_reg in args.iter().skip(1) { for &arg_reg in args.iter().skip(1) {
let val = vm.promote_if_needed_for_write_ptr(arr_ptr, vm.reg(arg_reg)); let key = vm.string_key_si(&len.to_string());
// 元素写入须维护 array_prop_count(set_prop_at 对数组自动更新)。 if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, key, vm.reg(arg_reg), recv, true) {
let arr = unsafe { &mut *arr_ptr }; return NativeResult::Err(from_engine_error(vm, &err));
let idx = arr.prop_count(); }
arr.set_prop_at(idx, val); len += 1;
} }
let len = unsafe { &*arr_ptr }.prop_count();
NativeResult::Ok(JsValue::int(len as i32)) // length 收尾同样走 Set:不可写 length 在此抛 TypeError。
let length_si = vm.string_key_si("length");
if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, length_si, js_array_index(len), recv, true) {
return NativeResult::Err(from_engine_error(vm, &err));
}
NativeResult::Ok(js_array_index(len))
} }
/// `Array.prototype.pop()`:移除并返回末位元素;空数组返回 undefined。 /// `Array.prototype.pop()`:移除并返回末位元素;空数组返回 undefined。
///
/// # 步骤
/// 1. 读 `LengthOfArrayLike(this)`;为 0 时以 `Set(this, "length", +0, true)` 收尾。
/// 2. 否则按 Get / DeletePropertyOrThrow / Set length 的顺序处理末位元素。
///
/// # 副作用
/// - 删除末位元素并收缩 length;Get 可触发原型链 getter,length 不可写时抛 TypeError。
pub fn array_pop<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult { pub fn array_pop<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("Array.prototype.pop called with {} args", args.len()); builtins_debug!("Array.prototype.pop called with {} args", args.len());
let arr_ptr = array_ptr!(vm, args); let arr_ptr = array_ptr!(vm, args);
let arr = unsafe { &mut *arr_ptr }; let recv = JsValue::from_js_object(arr_ptr);
let len = arr.prop_count(); let len = unsafe { &*arr_ptr }.logical_len() as usize;
let length_si = vm.string_key_si("length");
if len == 0 { if len == 0 {
if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, length_si, JsValue::int(0), recv, true) {
return NativeResult::Err(from_engine_error(vm, &err));
}
return NativeResult::Ok(JsValue::undefined()); return NativeResult::Ok(JsValue::undefined());
} }
let last = arr.get_prop_at(len - 1); let index = len - 1;
arr.set_prop_count_fast(len - 1); let key = vm.string_key_si(&index.to_string());
let last = match vm.ordinary_get(unsafe { &*arr_ptr }, key, recv) {
Ok(v) => v,
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) },
}
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(last)
} }
@@ -410,40 +454,118 @@ pub fn array_flat<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
} }
/// `Array.prototype.shift()`:移除并返回首元素,其余元素前移;空数组返回 undefined。 /// `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)` 收尾。
///
/// # 副作用
/// - 元素整体前移并收缩 length;Get/Set 可经原型链触发用户代码。
pub fn array_shift<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult { pub fn array_shift<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("Array.prototype.shift called with {} args", args.len()); builtins_debug!("Array.prototype.shift called with {} args", args.len());
let arr_ptr = array_ptr!(vm, args); let arr_ptr = array_ptr!(vm, args);
let arr = unsafe { &mut *arr_ptr }; let recv = JsValue::from_js_object(arr_ptr);
let len = arr.prop_count(); let len = unsafe { &*arr_ptr }.logical_len() as usize;
let length_si = vm.string_key_si("length");
if len == 0 { if len == 0 {
if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, length_si, JsValue::int(0), recv, true) {
return NativeResult::Err(from_engine_error(vm, &err));
}
return NativeResult::Ok(JsValue::undefined()); return NativeResult::Ok(JsValue::undefined());
} }
let first = arr.get_prop_at(0); let first_key = vm.string_key_si("0");
for i in 1..len { let first = match vm.ordinary_get(unsafe { &*arr_ptr }, first_key, recv) {
let v = arr.get_prop_at(i); Ok(v) => v,
arr.set_prop_at(i - 1, v); Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)),
};
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"));
}
_ => unsafe { (*arr_ptr).mark_hole_at(k - 1) },
}
}
}
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));
} }
arr.set_prop_count_fast(len - 1);
NativeResult::Ok(first) NativeResult::Ok(first)
} }
/// `Array.prototype.unshift(...items)`:插入元素到头部,返回新长度。 /// `Array.prototype.unshift(...items)`:插入元素到头部,返回新长度。
///
/// # 步骤
/// 1. 读 `LengthOfArrayLike(this)`;`len + 参数数` 超出 2^53-1 时抛 TypeError。
/// 2. 参数数大于 0 时自高到低搬移已有元素(源存在则 Get + 严格 Set,缺失则
/// DeletePropertyOrThrow),再把实参逐个严格 Set 到头部。
/// 3. 以 `Set(this, "length", len+参数数, true)` 收尾。
///
/// # 副作用
/// - 元素整体后移并扩容 length;Get/Set 可经原型链触发用户代码。
pub fn array_unshift<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult { pub fn array_unshift<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("Array.prototype.unshift called with {} args", args.len()); builtins_debug!("Array.prototype.unshift called with {} args", args.len());
let arr_ptr = array_ptr!(vm, args); let arr_ptr = array_ptr!(vm, args);
let arr = unsafe { &mut *arr_ptr }; let recv = JsValue::from_js_object(arr_ptr);
let len = arr.prop_count(); let len = unsafe { &*arr_ptr }.logical_len() as usize;
let n_items = args.len().saturating_sub(1); let n_items = args.len().saturating_sub(1);
for i in (0..len as usize).rev() { let length_si = vm.string_key_si("length");
let v = arr.get_prop_at(i); if n_items > 0 {
arr.set_prop_at(i + n_items, v); if len as f64 + n_items as f64 > 9_007_199_254_740_991.0 {
return NativeResult::Err(array_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"));
}
_ => unsafe { (*arr_ptr).mark_hole_at(to_idx) },
}
}
}
for (j, &arg_reg) in args.iter().skip(1).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) {
return NativeResult::Err(from_engine_error(vm, &err));
}
}
} }
for (j, &arg_reg) in args.iter().skip(1).enumerate() {
arr.set_prop_at(j, vm.reg(arg_reg)); let new_len = len + n_items;
if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, length_si, js_array_index(new_len), recv, true) {
return NativeResult::Err(from_engine_error(vm, &err));
} }
let new_len = len as usize + n_items; NativeResult::Ok(js_array_index(new_len))
arr.set_prop_count_fast(new_len);
NativeResult::Ok(JsValue::int(new_len as i32))
} }
/// `Array.prototype.fill(value, start, end)`:用给定值填充区间,返回 this。 /// `Array.prototype.fill(value, start, end)`:用给定值填充区间,返回 this。
+2 -1
View File
@@ -96,7 +96,8 @@ impl oxide_runtime_api::VmHost for Vm {
fn ordinary_set( fn ordinary_set(
&mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, strict: bool, &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, strict: bool,
) -> Result<(), String> { ) -> Result<(), String> {
self.ordinary_set(obj, prop_name_si, val, receiver, strict) // builtin 调用边界:写失败返回格式化 Err,不就地展开(见 ordinary_set_builtin)。
self.ordinary_set_builtin(obj, prop_name_si, val, receiver, strict)
} }
fn define_data_property( fn define_data_property(
&mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, attributes: PropAttributes, &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, attributes: PropAttributes,
+140 -95
View File
@@ -308,19 +308,33 @@ impl Vm {
&mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, strict: bool, &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, strict: bool,
) -> Result<(), String> { ) -> Result<(), String> {
let val = self.promote_if_needed_for_write_ptr(obj as *mut JsObject, val); let val = self.promote_if_needed_for_write_ptr(obj as *mut JsObject, val);
self.ordinary_set_inner(obj, prop_name_si, val, receiver, false, strict) self.ordinary_set_inner(obj, prop_name_si, val, receiver, false, strict, false)
}
/// builtin 内部写入口:失败一律返回格式化 `Err` 由调用边界恢复为异常对象,
/// 不就地 `unwind`——builtin 执行在重入的字节码上下文中,就地展开会跳入
/// 调用方 catch 并让 builtin 继续,随后覆盖异常寄存器。
pub(crate) fn ordinary_set_builtin(
&mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, strict: bool,
) -> Result<(), String> {
let val = self.promote_if_needed_for_write_ptr(obj as *mut JsObject, val);
self.ordinary_set_inner(obj, prop_name_si, val, receiver, false, strict, true)
} }
/// 分发期入口:调用方(dispatch_set_prop 等)已对值做过 promote。 /// 分发期入口:调用方(dispatch_set_prop 等)已对值做过 promote。
pub(crate) fn ordinary_set_dispatch( pub(crate) fn ordinary_set_dispatch(
&mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, strict: bool, &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, strict: bool,
) -> Result<(), String> { ) -> Result<(), String> {
self.ordinary_set_inner(obj, prop_name_si, val, receiver, true, strict) self.ordinary_set_inner(obj, prop_name_si, val, receiver, true, strict, false)
} }
#[expect(
clippy::too_many_arguments,
reason = "写路径须透传 receiver、帧化与 builtin 三组调用契约标志"
)]
fn ordinary_set_inner( fn ordinary_set_inner(
&mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, use_frame_push: bool, &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, use_frame_push: bool,
strict: bool, strict: bool, builtin: bool,
) -> Result<(), String> { ) -> Result<(), String> {
vm_trace!( vm_trace!(
"ordinary_set_inner: shape={} prop_si={} frame_push={}", "ordinary_set_inner: shape={} prop_si={} frame_push={}",
@@ -332,7 +346,7 @@ impl Vm {
// sloppy 静默 no-op;Reflect.set 以 strict=true 调用并投影为 false。 // sloppy 静默 no-op;Reflect.set 以 strict=true 调用并投影为 false。
if obj.is_module_namespace() { if obj.is_module_namespace() {
if strict { if strict {
return self.raise_type_error("Cannot assign to a module namespace export"); return self.write_protection_failure(builtin, "Cannot assign to a module namespace export");
} }
return Ok(()); return Ok(());
} }
@@ -349,86 +363,18 @@ impl Vm {
index as usize, index as usize,
use_frame_push, use_frame_push,
strict, strict,
builtin,
); );
} }
return oxide_builtins::typed_array::typed_array_element_set(self, obj, index, val); return oxide_builtins::typed_array::typed_array_element_set(self, obj, index, val);
} }
} }
// 数组 length 赋值:ArraySetLength 语义(ToUint32 + 调整元素区)。 // 数组 length 赋值:走 ArraySetLength 语义(两次数值强转、可写性判定与
// length 不得落影子命名属性:否则 `arr.length = N` 后 prop_count / // 元素区调整),不得落影子命名属性——否则 prop_count / 迭代 / 内置方法
// 迭代 / 内置方法看到的长度与元素区不一致。 // 看到的长度与元素区不一致。
let length_si = self.length_si; let length_si = self.length_si;
if obj.is_array() && prop_name_si == length_si { if obj.is_array() && prop_name_si == length_si {
// 冻结数组的 length 属性不可写(writable=false),赋值直接失败; return self.set_array_length_value(obj, val, strict, builtin);
// 两模式统一抛 TypeError(length 冻结检查的 strict/sloppy 差异不在本路径范围)。
if obj.is_frozen() {
return self.raise_type_error("Cannot assign to read only property 'length'");
}
// defineProperty 显式收窄的不可写 length:严格模式抛 TypeError,sloppy 静默。
if !obj.is_length_writable() {
if strict {
return self.raise_type_error("Cannot assign to read only property 'length'");
}
return Ok(());
}
let pc_before = self.pc;
let number_len = self.coerce_number_bounded(val)?;
// ToPrimitive 抛错(valueOf/toString throw)已被 unwind 定向到外围 catch 时
// pc 指向 catch 入口:主 dispatch 约定异常后 opcode 不得继续 raise,直接
// 返回由 dispatch 继续执行 catch,避免二次抛错覆盖原异常。
if self.pc != pc_before {
return Ok(());
}
let raw_new_len = if number_len == 0.0 || !number_len.is_finite() {
0
} else {
number_len.trunc().rem_euclid(4_294_967_296.0) as u32 as usize
};
// ToUint32 截断结果与 ToNumber 不等价(1.5 / NaN / Infinity / 负数 /
// 2**32 等输入)时抛 RangeError:赋值后 length 读不回原值。
if raw_new_len as f64 != number_len {
return self.raise_error_kind("RangeError", "Invalid array length");
}
let old_logical = obj.logical_len() as usize;
let old_count = obj.array_prop_count as usize;
// ArraySetLength:增长(newLen > oldLen)要求对象可扩展,不可扩展时
// 整个赋值失败且不修改(length 失败两模式均静默 no-op,不抛)。
if raw_new_len > old_logical && !obj.is_extensible() {
return Ok(());
}
// ArraySetLength:收缩时若 [newLen, oldLen) 内存在不可配置元素,整个收缩
// 失败且不做任何修改(length 失败两模式均静默 no-op,不抛)。
if raw_new_len < old_logical {
for idx in raw_new_len..old_count {
if let Some(meta) = obj.prop_meta_at(idx) {
if !meta.attributes.configurable() {
return Ok(());
}
}
}
}
// 元素区物理槽数以 `MAX_DENSE_PROPS` 为上限,超限长度不扩槽。
let new_len_u = raw_new_len.min(oxide_types::object::MAX_DENSE_PROPS);
obj.set_prop_count(new_len_u);
// 扩出的槽是稀疏 hole:存在性检查与原型链读取须视同不存在,
// 故逐个打 hole 标记。
for idx in old_count..new_len_u {
obj.mark_hole_at(idx);
}
// 超过稠密上限的长度另记逻辑长度槽:`a.length` 读回真实赋值
// 而非封顶后的物理槽数。
if raw_new_len > oxide_types::object::MAX_DENSE_PROPS {
obj.set_array_len_override(raw_new_len as u32);
} else {
obj.clear_array_len_override();
}
// 对象自身属性区可能存在影子 length 命名属性(shape 槽),同步写入
// 新逻辑长度,保证 IC 快路径读到新值。
if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), length_si) {
let store_idx = obj.array_prop_count as usize + pos as usize;
obj.set_prop_storage(store_idx, obj.logical_len_value());
}
return Ok(());
} }
if let Some(pos) = self.get_own_property_slot(obj, prop_name_si) { if let Some(pos) = self.get_own_property_slot(obj, prop_name_si) {
if let Some(meta) = obj.prop_meta_at(pos) { if let Some(meta) = obj.prop_meta_at(pos) {
@@ -436,7 +382,7 @@ impl Vm {
if meta.set.is_undefined() { if meta.set.is_undefined() {
// 无 setter:严格抛错,sloppy 静默 no-op。 // 无 setter:严格抛错,sloppy 静默 no-op。
if strict { if strict {
return self.raise_type_error("property has no setter"); return self.write_protection_failure(builtin, "property has no setter");
} }
return Ok(()); return Ok(());
} }
@@ -445,7 +391,7 @@ impl Vm {
if !meta.attributes.writable() { if !meta.attributes.writable() {
// 只读数据属性:严格抛错,sloppy 静默 no-op。 // 只读数据属性:严格抛错,sloppy 静默 no-op。
if strict { if strict {
return self.raise_type_error("cannot assign to read-only property"); return self.write_protection_failure(builtin, "cannot assign to read-only property");
} }
return Ok(()); return Ok(());
} }
@@ -461,7 +407,7 @@ impl Vm {
if meta.set.is_undefined() { if meta.set.is_undefined() {
// 继承无 setter:严格抛错,sloppy 静默 no-op。 // 继承无 setter:严格抛错,sloppy 静默 no-op。
if strict { if strict {
return self.raise_type_error("property has no setter"); return self.write_protection_failure(builtin, "property has no setter");
} }
return Ok(()); return Ok(());
} }
@@ -470,17 +416,30 @@ impl Vm {
if !meta.attributes.writable() { if !meta.attributes.writable() {
// 继承只读数据属性:严格抛错,sloppy 静默 no-op(不遮蔽)。 // 继承只读数据属性:严格抛错,sloppy 静默 no-op(不遮蔽)。
if strict { if strict {
return self.raise_type_error("cannot assign to read-only property"); return self.write_protection_failure(builtin, "cannot assign to read-only property");
} }
return Ok(()); return Ok(());
} }
} }
// 数组索引增长(index >= 当前 length)先过 length 可写性检查:与 define 侧
// `define_array_index_element` 同序,早于对象可扩展性检查;length 不可写时
// 严格抛 TypeError、sloppy 静默。索引小于 length 的已有元素写入不受此限。
if let Some(index) = self.array_index_from_property_key(prop_name_si) {
if obj.is_array() && index >= obj.logical_len() && !obj.is_length_writable() {
return self.array_length_write_failure(
strict,
builtin,
"Cannot add property, array length is not writable",
);
}
}
// 新属性(自身与原型链均无同名):须对象可扩展(OrdinarySet 的 extensible // 新属性(自身与原型链均无同名):须对象可扩展(OrdinarySet 的 extensible
// 检查),不可扩展时赋值失败(严格抛错,sloppy 静默 no-op)。 // 检查),不可扩展时赋值失败(严格抛错,sloppy 静默 no-op)。
if !obj.is_extensible() { if !obj.is_extensible() {
if strict { if strict {
return self.raise_type_error("object is not extensible"); return self.write_protection_failure(builtin, "object is not extensible");
} }
return Ok(()); return Ok(());
} }
@@ -488,13 +447,92 @@ impl Vm {
Ok(()) Ok(())
} }
/// 数组 length 赋值路径:`a.length = v` 的 `[[Set]]`,按 ArraySetLength 语义
/// 调整元素区(与 define 侧 [`Self::define_array_length`] 共用截断/扩洞逻辑)。
///
/// # 步骤
/// 1. `ToUint32` 与 `ToNumber` 两次强转各自执行一次(均可触发用户代码)。
/// 2. 两次结果不等(非整数、负数、NaN/Infinity、2^32 等)抛 RangeError。
/// 3. 强转完成后按当前可写位判定:不可写时严格抛 TypeError、sloppy 静默,且
/// 不做任何截断(写不可写数据描述符先于 ArraySetLength 失败)。
/// 4. 收缩求最高不可配置阻挡索引:有则以「阻挡索引 + 1」部分截断后按模式返回
/// 失败;无则完整截断/扩 hole 到目标长度。
///
/// # 边界与前提
/// - 仅由 `ordinary_set_inner` 对 `is_array()` 对象且键为 length 时调用。
/// - 可写位判定必须放在两次强转之后:用户代码可能在强转期收窄 length 可写位。
///
/// # 副作用
/// - 修改元素区与元素元数据、可能置/清 `array_len_override`、bump 世代。
fn set_array_length_value(
&mut self, obj: &mut JsObject, val: JsValue, strict: bool, builtin: bool,
) -> Result<(), String> {
// ToNumber(BigInt) 抛 TypeError:在通用强转近似接受 BigInt 之前拦截。
if val.is_bigint() {
return self.write_protection_failure(builtin, "Cannot convert a BigInt value to a number");
}
let pc_before = self.pc;
let new_len = self.coerce_uint32_bounded(val)?;
// 强转抛错在主 dispatch 下已 unwind 到外围 catch,此时 opcode 不得继续,
// 直接返回由 dispatch 执行 catch,避免二次抛错覆盖原异常。
if self.pc != pc_before {
return Ok(());
}
let number_len = self.coerce_number_bounded(val)?;
if self.pc != pc_before {
return Ok(());
}
if new_len as f64 != number_len {
if builtin {
return Err(self.error_message_text("RangeError", "Invalid array length"));
}
return self.raise_error_kind("RangeError", "Invalid array length");
}
// 两次强转后重新判定:强转期间用户代码可能已把 length 收窄为不可写。
if !obj.is_length_writable() {
return self.array_length_write_failure(strict, builtin, "Cannot assign to read only property 'length'");
}
let old_logical = obj.logical_len();
let target_len = Self::array_length_shrink_target(obj, new_len, old_logical);
Self::apply_array_length(obj, target_len);
obj.bump_generation();
if target_len != new_len {
return self.array_length_write_failure(strict, builtin, "Cannot assign to read only property 'length'");
}
Ok(())
}
/// 属性写保护失败的 strict/sloppy 分派:严格模式抛 TypeError(非 builtin 就地
/// 展开到最近 catch),sloppy 静默 no-op。
fn array_length_write_failure(&mut self, strict: bool, builtin: bool, msg: &str) -> Result<(), String> {
if !strict {
return Ok(());
}
self.write_protection_failure(builtin, msg)
}
/// TypeError 的两态出口:非 builtin 就地抛可捕获异常并展开,builtin 内部返回
/// 格式化 `Err` 由调用边界恢复为异常对象(builtin 在重入字节码上下文中,就地
/// 展开会跳入调用方 catch 并让 builtin 继续,随后覆盖异常寄存器)。
fn write_protection_failure(&mut self, builtin: bool, msg: &str) -> Result<(), String> {
if builtin {
Err(self.error_message_text("TypeError", msg))
} else {
self.raise_type_error(msg)
}
}
/// TypedArray 整数索引在 `receiver` ≠ TA 时的 [[Set]] 语义:越界或非对象 /// TypedArray 整数索引在 `receiver` ≠ TA 时的 [[Set]] 语义:越界或非对象
/// receiver 直接返回 true(不写、不 ToNumber);界内对象 receiver 走普通 set /// receiver 直接返回 true(不写、不 ToNumber);界内对象 receiver 走普通 set
/// 把属性落到 receiver 自身。 /// 把属性落到 receiver 自身。
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn set_to_receiver( fn set_to_receiver(
&mut self, ta_obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, index: usize, &mut self, ta_obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, index: usize,
use_frame_push: bool, strict: bool, use_frame_push: bool, strict: bool, builtin: bool,
) -> Result<(), String> { ) -> Result<(), String> {
let Some((_, length)) = oxide_builtins::typed_array::typed_array_integer_index(self, ta_obj, prop_name_si) let Some((_, length)) = oxide_builtins::typed_array::typed_array_integer_index(self, ta_obj, prop_name_si)
else { else {
@@ -509,7 +547,7 @@ impl Vm {
} }
let promoted = self.promote_if_needed_for_write_ptr(receiver_ptr, val); let promoted = self.promote_if_needed_for_write_ptr(receiver_ptr, val);
let receiver_obj = unsafe { &mut *receiver_ptr }; let receiver_obj = unsafe { &mut *receiver_ptr };
self.ordinary_set_inner(receiver_obj, prop_name_si, promoted, receiver, use_frame_push, strict) self.ordinary_set_inner(receiver_obj, prop_name_si, promoted, receiver, use_frame_push, strict, builtin)
} }
/// 调用或帧化访问器 setter:native(或禁止帧化的调用方)同步执行, /// 调用或帧化访问器 setter:native(或禁止帧化的调用方)同步执行,
@@ -1104,17 +1142,8 @@ impl Vm {
return Err("cannot redefine non-configurable property".to_string()); return Err("cannot redefine non-configurable property".to_string());
} }
// 收缩按 ArraySetLength 删除循环语义:自最高索引向下删除可配置元素,遇最高 // 收缩按 ArraySetLength 删除循环语义求目标长度(部分截断 / 完整截断)。
// 不可配置索引 P 时停止并把 length 收敛到 P+1(其上元素已在循环中删除)。 let target_len = Self::array_length_shrink_target(obj, new_len, old_logical);
// 仅当 [newLen, oldLen) 全可配置时才完整截断到 newLen。
let target_len = if new_len < old_logical {
match Self::highest_non_configurable_index(obj, new_len, old_logical) {
Some(blocker) => blocker + 1,
None => new_len,
}
} else {
new_len
};
Self::apply_array_length(obj, target_len); Self::apply_array_length(obj, target_len);
obj.set_length_non_writable(!attributes.writable()); obj.set_length_non_writable(!attributes.writable());
@@ -1126,6 +1155,22 @@ impl Vm {
Ok(()) Ok(())
} }
/// 数组 length 收缩的目标长度:`[new_len, old_len)` 内存在不可配置元素时取
/// 最高阻挡索引 + 1(部分截断,其上可配置元素已删除),否则完整截断到 `new_len`。
///
/// # 边界与前提
/// - `new_len >= old_logical` 时直接返回 `new_len`(非收缩路径)。
fn array_length_shrink_target(obj: &JsObject, new_len: u32, old_logical: u32) -> u32 {
if new_len < old_logical {
match Self::highest_non_configurable_index(obj, new_len, old_logical) {
Some(blocker) => blocker + 1,
None => new_len,
}
} else {
new_len
}
}
/// 返回 `[from, old_len)` 内最高的不可配置自身元素索引;无则 `None`。 /// 返回 `[from, old_len)` 内最高的不可配置自身元素索引;无则 `None`。
/// ///
/// # 边界与前提 /// # 边界与前提
@@ -0,0 +1,189 @@
//! 数组 length 虚拟属性的赋值侧回归:`a.length = v` 的两次数值强转、strict/sloppy
//! 可写位语义、部分截断、增长、异常原值传播,以及 push/pop/shift/unshift 与
//! `a[i] = v` 字节码路径对不可写 length 的行为。
use std::sync::Arc;
use oxide_compiler::compiler::Compiler;
use oxide_parser::Allocator;
use oxide_vm::vm::Vm;
/// 求值并取字符串结果内容(字符串值走 lookup_str,非字符串走 Display)。
fn eval_str(source: &str) -> String {
let allocator = Allocator::default();
let program = oxide_parser::parse(&allocator, source).expect("parse ok");
let module = Compiler::new().compile(&program).expect("compile ok");
let mut vm = Vm::new();
let val = vm.run(&Arc::new(module)).expect("run ok");
if val.is_string() {
vm.lookup_str(val).unwrap_or_default()
} else {
format!("{val}")
}
}
#[test]
fn set_length_truncates_and_grows() {
assert_eq!(eval_str("var a=[1,2,3]; a.length=1; ''+a.length+':'+(2 in a)"), "1:false");
assert_eq!(eval_str("var a=[1]; a.length=3; ''+a.length+':'+(1 in a)"), "3:false");
}
#[test]
fn set_length_non_writable_strict_throws_sloppy_silent() {
// sloppy:静默 no-op,length 不变。
assert_eq!(
eval_str("var a=[1,2,3]; Object.defineProperty(a,'length',{writable:false}); a.length=1; ''+a.length"),
"3"
);
// strict:TypeError,length 不变。
assert_eq!(
eval_str("var a=[1,2,3]; Object.defineProperty(a,'length',{writable:false}); (function(){'use strict';try{a.length=1;return 'no-throw';}catch(e){return e.name;}})()+':'+a.length"),
"TypeError:3"
);
// 同值赋值同样失败:不可写数据描述符先于 ArraySetLength 返回 false。
assert_eq!(
eval_str("var a=[1,2,3]; Object.defineProperty(a,'length',{writable:false}); (function(){'use strict';try{a.length=3;return 'no-throw';}catch(e){return e.name;}})()+':'+a.length"),
"TypeError:3"
);
}
#[test]
fn set_length_frozen_strict_throws_sloppy_silent() {
// 冻结数组的 length 同样不可写:sloppy 静默而非恒抛。
assert_eq!(eval_str("var a=Object.freeze([1,2,3]); a.length=1; ''+a.length"), "3");
assert_eq!(
eval_str("var a=Object.freeze([1,2,3]); (function(){'use strict';try{a.length=1;return 'no-throw';}catch(e){return e.name;}})()+':'+a.length"),
"TypeError:3"
);
}
#[test]
fn set_length_shrink_partial_truncation_at_blocker() {
// strict:阻挡索引 1,length 收敛到 2,index 2 起删除,并抛 TypeError。
assert_eq!(
eval_str("var a=[0,1,2,3,4]; Object.defineProperty(a,'1',{configurable:false}); var r; (function(){'use strict';try{a.length=1;r='no-throw';}catch(e){r=e.name;}})(); r+':'+a.length+':'+a.hasOwnProperty('1')+':'+a.hasOwnProperty('2')"),
"TypeError:2:true:false"
);
// sloppy:同样部分截断,静默失败。
assert_eq!(
eval_str("var a=[0,1,2,3,4]; Object.defineProperty(a,'1',{configurable:false}); a.length=0; ''+a.length+':'+a.hasOwnProperty('1')+':'+a.hasOwnProperty('2')"),
"2:true:false"
);
}
#[test]
fn set_length_growth_ignores_extensibility() {
// ArraySetLength 的增长不要求对象可扩展:preventExtensions 后仍成功。
assert_eq!(
eval_str("var a=[1]; Object.preventExtensions(a); a.length=3; ''+a.length+':'+(1 in a)+':'+(2 in a)"),
"3:false:false"
);
}
#[test]
fn set_length_invalid_values_throw_range_error() {
assert_eq!(
eval_str("var a=[1,2,3]; var r=[]; [1.5,-1,4294967296,NaN].forEach(function(v){try{a.length=v;r.push('no-throw');}catch(e){r.push(e.name);}}); r.join(',')"),
"RangeError,RangeError,RangeError,RangeError"
);
}
#[test]
fn set_length_coerces_twice_before_writable_check() {
// ToUint32 + ToNumber 各触发一次 ToPrimitive(number);第二次强转收窄可写位后
// 由可写位判定拒绝,且不做任何截断。
assert_eq!(
eval_str("var a=[1,2,3], n=0; var v={}; v[Symbol.toPrimitive]=function(h){n++;Object.defineProperty(a,'length',{writable:false});return 0;}; var r; (function(){'use strict';try{a.length=v;r='no-throw';}catch(e){r=e.name;}})(); r+':'+n+':'+a.length"),
"TypeError:2:3"
);
// Reflect.set 以严格模式调用并投影为 false,两次强转同样执行。
assert_eq!(
eval_str("var a=[1,2,3], n=0; var v={}; v[Symbol.toPrimitive]=function(h){n++;Object.defineProperty(a,'length',{writable:false});return 0;}; Reflect.set(a,'length',v)+':'+n+':'+a.length"),
"false:2:3"
);
}
#[test]
fn set_length_coercion_exception_propagates_original_value() {
// 强转期 valueOf / Symbol.toPrimitive 抛出的自定义错误须原值重抛(identity 保留)。
assert_eq!(
eval_str("var err=new Error('boom'); var v={valueOf:function(){throw err;}}; var a=[]; var c=null; try{a.length=v;}catch(e){c=e;} (c===err)+':'+c.name"),
"true:Error"
);
assert_eq!(
eval_str("var err=new RangeError('sym'); var v={}; v[Symbol.toPrimitive]=function(h){throw err;}; var a=[]; var c=null; try{a.length=v;}catch(e){c=e;} (c===err)+':'+c.name"),
"true:RangeError"
);
}
#[test]
fn set_length_bigint_and_symbol_throw_type_error() {
assert_eq!(
eval_str("var a=[]; var r=[]; try{a.length=1n;r.push('no-throw');}catch(e){r.push(e.name);} try{a.length=Symbol();r.push('no-throw');}catch(e){r.push(e.name);} r.join(',')"),
"TypeError,TypeError"
);
}
#[test]
fn set_elem_beyond_non_writable_length_respects_strict() {
// `a[i] = v` 字节码路径:越界写须过 length 可写性检查,sloppy 静默、strict 抛。
assert_eq!(
eval_str(
"var a=[]; Object.defineProperty(a,'length',{writable:false}); var i=3; a[i]=9; ''+a.length+':'+(3 in a)"
),
"0:false"
);
assert_eq!(
eval_str("var a=[]; Object.defineProperty(a,'length',{writable:false}); var i=3; var r; (function(){'use strict';try{a[i]=9;r='no-throw';}catch(e){r=e.name;}})(); r+':'+a.length+':'+(3 in a)"),
"TypeError:0:false"
);
}
#[test]
fn mutators_on_non_writable_length_throw() {
// 空数组 + 不可写 length:四个变更方法收尾的 Set length 均抛 TypeError。
assert_eq!(
eval_str("var a=[]; Object.defineProperty(a,'length',{writable:false}); var r=[]; try{a.push();r.push('no-throw');}catch(e){r.push(e.name);} r.push(a.length); r.join(',')"),
"TypeError,0"
);
assert_eq!(
eval_str(
"var a=[]; Object.defineProperty(a,'length',{writable:false}); try{a.pop();'no-throw';}catch(e){e.name}"
),
"TypeError"
);
assert_eq!(
eval_str(
"var a=[]; Object.defineProperty(a,'length',{writable:false}); try{a.shift();'no-throw';}catch(e){e.name}"
),
"TypeError"
);
assert_eq!(
eval_str("var a=[]; Object.defineProperty(a,'length',{writable:false}); try{a.unshift();'no-throw';}catch(e){e.name}"),
"TypeError"
);
// frozen 空数组同样四方法皆抛。
assert_eq!(
eval_str("var a=Object.freeze([]); var r=[]; try{a.push();r.push('no-throw');}catch(e){r.push(e.name);} try{a.pop();r.push('no-throw');}catch(e){r.push(e.name);} try{a.shift();r.push('no-throw');}catch(e){r.push(e.name);} try{a.unshift();r.push('no-throw');}catch(e){r.push(e.name);} r.join(',')"),
"TypeError,TypeError,TypeError,TypeError"
);
}
#[test]
fn push_element_write_uses_prototype_setter() {
// 继承的索引 setter 在元素写入时执行并收窄 length,收尾 Set length 失败。
assert_eq!(
eval_str("var a=[]; var calls=0; Object.defineProperty(Array.prototype,'0',{set:function(v){Object.defineProperty(a,'length',{writable:false});calls++;}}); var r; try{a.push(1);r='no-throw';}catch(e){r=e.name;} r+':'+a.length+':'+a.hasOwnProperty(0)+':'+calls"),
"TypeError:0:false:1"
);
}
#[test]
fn mutators_still_work_with_writable_length() {
// 重写后正常路径回归:push/pop/shift/unshift 的元素搬移与返回值。
assert_eq!(eval_str("var a=[]; a.push(1,2); ''+a.length+':'+a[1]"), "2:2");
assert_eq!(
eval_str("var a=[1,2,3]; var p=a.pop(); var s=a.shift(); var u=a.unshift(9,8); p+':'+s+':'+u+':'+a.join(',')"),
"3:1:3:9,8,2"
);
}
@@ -235,7 +235,7 @@ fn frozen_array_write_and_length_throws_in_strict() {
); );
} }
// ── sloppy Object.freeze 数组:元素写静默失败(length 冻结检查仍抛错) ── // ── sloppy Object.freeze 数组:元素写与 length 写均静默失败(严格模式才抛) ──
#[test] #[test]
fn frozen_array_element_write_silently_fails_in_sloppy() { fn frozen_array_element_write_silently_fails_in_sloppy() {
let (_vm, r) = eval_keep_vm( let (_vm, r) = eval_keep_vm(